洋葱控制器。 我们将屏幕分成几部分

原子设计和系统设计在设计中很流行:这是指从控件到屏幕的所有组件都由组件组成的情况。 程序员编写单独的控件并不难,但是如何处理整个屏幕呢?


让我们看一下新年的例子:


  • 让我们把所有东西粘在一起;
  • 分为控制器:选择导航,模板和内容;
  • 在其他屏幕上重用代码。


一堆


这个新年的屏幕上讨论了比萨店的特殊营业时间。 这非常简单,因此将其设置为控制器将不会构成犯罪:



但是 下次,当我们需要类似的屏幕时,我们将不得不再次重复所有操作,然后对所有屏幕进行相同的更改。 好吧,没有修改就不会发生。


因此,将其划分为多个部分并用于其他屏幕更为合理。 我强调了三个:


  • 导航
  • 在屏幕底部具有内容区域和操作位置的模板,
  • 中心的独特内容。

在其自己的UIViewController选择每个部分。


集装箱导航


导航容器最引人注目的示例是UINavigationControllerUITabBarController 。 每个控件在其自己的控件下在屏幕上占据一个小条,并为另一个UIViewController保留剩余空间。


在我们的情况下,将为所有模式屏幕提供一个容器,其中只有一个关闭按钮。


有什么意义?

如果要向右移动按钮,则只需要在一个控制器中进行更改即可。


或者,如果我们决定用特殊的动画显示所有模态窗口,并通过滑动以交互方式关闭,如AppStore故事卡中所示。 然后,仅需要为此控制器设置UIViewControllerTransitioningDelegate



您可以使用container view来分隔控制器:它将在父container view中创建一个UIView并将子控制器的UIView插入其中。



container view拉伸到屏幕边缘。 Safe area将自动应用于子控制器:



屏幕图案


内容在屏幕上显而易见:图片,标题,文本。 该按钮似乎是其中的一部分,但内容在不同的iPhone上是动态的,并且该按钮是固定的。 可以看到两个任务不同的系统:一个显示内容,另一个嵌入并对齐内容。 它们应分为两个控制器。



第一个负责屏幕的布局:内容应居中,并且按钮应钉在屏幕底部。 第二个将绘制内容。



没有模板,所有控制器都是相似的,但是元素却在起舞。

最后一个屏幕上的按钮不同-它取决于内容。 委派将帮助解决问题:控制器模板将从内容中请求控件,并将其显示在其UIStackView


 // OnboardingViewController.swift protocol OnboardingViewControllerDatasource { var supportingViews: [UIView] { get } } // NewYearContentViewController.swift extension NewYearContentViewController: OnboardingViewControllerDatasource { var supportingViews: [UIView] { return [view().doneButton] } } 

为什么要查看()?

UIViewController可以在上一篇文章Controller中阅读有关如何使用UIViewController专门化UIView ,请UIViewController 我们取出UIView中的代码。


可以通过相关对象将按钮附加到控制器。 它们的IBOutletIBAction存储在内容控制器中,只是元素未添加到层次结构中。



您可以在UIStoryboardSegue的准备阶段从内容中获取元素并将其添加到模板中:


 // OnboardingViewController.swift override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let buttonsDatasource = segue.destination as? OnboardingViewControllerDatasource { view().supportingViews = buttonsDatasource.supportingViews } } 

在设置器中,我们向UIStackView添加控件:


 // OnboardingView.swift var supportingViews: [UIView] = [] { didSet { for view in supportingViews { stackView.addArrangedSubview(view) } } } 

结果,我们的控制器分为三个部分:导航,模板和内容。 在图片中,所有container view显示为灰色:



动态控制器尺寸


内容控制器具有自己的最大大小,受内部constraints


Container view基于自动调整Autoresizing mask添加了Autoresizing mask ,它们与内容的内部尺寸冲突。 该问题已在代码中解决:在内容控制器中,您需要从Autoresizing mask指示它不受存储库的影响:


 // NewYearContentViewController.swift override func loadView() { super.loadView() view.translatesAutoresizingMaskIntoConstraints = false } 


Interface Builder还有两个步骤:


步骤1.UIView指定Intrinsic size 。 实际价值将在发布后显示,但现在我们将放置所有合适的价值。



步骤2.对于内容控制器,指定Simulated Size 。 它可能与过去的大小不符。


出现布局错误,该怎么办?

AutoLayout无法找出如何分解当前大小的元素时,会发生错误。


通常,在更改常数的优先级后问题就消失了。 您需要放下它们,以便其中一个UIView可以比其他UIView进行更多的扩展/收缩。


我们分成几部分并编写代码


我们将控制器分为几个部分,但到目前为止,我们无法重用它们, UIStoryboard的接口很难分部分提取。 如果我们需要将一些数据传输到内容,那么我们将不得不在整个层次结构中加以处理。 它应该是另一种方式:首先获取内容,对其进行配置,然后将其包装在必要的容器中。 像灯泡。


我们的方法出现了三个任务:


  1. 将每个控制器分成自己的UIStoryboard
  2. 拒绝container view ,将控制器添加到代码容器中。
  3. 绑回去。

共享UIStoryboard


您需要创建两个附加的UIStoryboard然后将导航控制器和模板控制器复制粘贴到其中。 Embed segue将中断,但是将传输具有已配置约束的container view 。 必须保存约束,并且必须用常规UIView替换container view


最简单的方法是在UIStoryboard代码中更改Container视图的类型。
  • 以代码形式打开UIStoryboard (文件上下文菜单→以...形式打开→源代码);
  • 将类型从containerView更改为view 。 必须同时更改开始和结束标签。


    同样,可以根据需要将UIView更改为UIScrollView 。 反之亦然。




我们将控制器设置is initial view controller ,然后将UIStoryboard称为控制器。


我们从UIStoryboard加载控制器。

如果控制器的名称与UIStoryboard的名称匹配,则可以将下载内容包装在一种方法中,该方法本身将找到所需的文件:


 protocol Storyboardable { } extension Storyboardable where Self: UIViewController { static func instantiateInitialFromStoryboard() -> Self { let controller = storyboard().instantiateInitialViewController() return controller! as! Self } static func storyboard(fileName: String? = nil) -> UIStoryboard { let storyboard = UIStoryboard(name: fileName ?? storyboardIdentifier, bundle: nil) return storyboard } static var storyboardIdentifier: String { return String(describing: self) } static var storyboardName: String { return storyboardIdentifier } } 

如果在.xib描述了控制器,则标准构造函数将加载而不会发生此类舞动。 xi, .xib只能包含一个控制器,通常这还不够:在一个好的情况下,一个屏幕包含多个屏幕。 因此,我们使用UIStoryborad ,很容易将屏幕分成几部分。


在代码中添加控制器


为了使控制器正常工作,我们需要其生命周期中的所有方法: will/did-appear/disappear


为了正确显示,您需要调用5个步骤:


  willMove(toParent parent: UIViewController?) addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?) 

苹果建议将代码减少到4个步骤,因为addChild()本身将调用willMove(toParent) 。 总结:


  addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?) 

为简单起见,您可以将其全部包装在extension 。 对于我们的情况,我们需要一个带有insertSubview()的版本。


 extension UIViewController { func insertFullframeChildController(_ childController: UIViewController, toView: UIView? = nil, index: Int) { let containerView: UIView = toView ?? view addChild(childController) containerView.insertSubview(childController.view, at: index) containerView.pinToBounds(childController.view) childController.didMove(toParent: self) } } 

要删除,您需要执行相同的步骤,只需要设置nil代替父控制器。 现在removeFromParent()调用didMove(toParent: nil) ,并且不需要布局。 缩短的版本非常不同:


  willMove(toParent: nil) view.removeFromSuperview() removeFromParent() 

布局图


设置约束


为了正确设置控制器的大小,我们将使用AutoLayout 。 我们需要将所有方面都牢牢钉在身边:


 extension UIView { func pinToBounds(_ view: UIView) { view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: topAnchor), view.bottomAnchor.constraint(equalTo: bottomAnchor), view.leadingAnchor.constraint(equalTo: leadingAnchor), view.trailingAnchor.constraint(equalTo: trailingAnchor) ]) } } 

在代码中添加一个子控制器


现在可以将所有内容组合在一起:


 // ModalContainerViewController.swift public func embedController(_ controller: UIViewController) { insertFullframeChildController(controller, index: 0) } 

由于使用频率高,我们可以将所有这些包装在extension


 // ModalContainerViewController.swift extension UIViewController { func wrapInModalContainer() -> ModalContainerViewController { let modalController = ModalContainerViewController.instantiateInitialFromStoryboard() modalController.embedController(self) return modalController } } 

模板控制器也需要类似的方法。 prepare(for segue:)过去是在prepare(for segue:)设置的prepare(for segue:) ,但是现在您可以将其绑定到控制器的embed方法中:


 // OnboardingViewController.swift public func embedController(_ controller: UIViewController, actionsDatasource: OnboardingViewControllerDatasource) { insertFullframeChildController(controller, toView: view().contentContainerView, index: 0) view().supportingViews = actionsDatasource.supportingViews } 

创建一个控制器如下所示:


 // MainViewController.swift @IBAction func showModalControllerDidPress(_ sender: UIButton) { let content = NewYearContentViewController.instantiateInitialFromStoryboard() //     let onboarding = OnboardingViewController.instantiateInitialFromStoryboard() onboarding.embedController(contentController, actionsDatasource: contentController) let modalController = onboarding.wrapInModalContainer() present(modalController, animated: true) } 

将新屏幕连接到模板很简单:


  • 删除与内容无关的内容;
  • 通过实现OnboardingViewControllerDatasource协议指定操作按钮;
  • 编写链接模板和内容的方法。

有关容器的更多信息


状态栏


通常, status bar的可见性必须由具有内容的控制器而不是容器来控制。 有两个property


 // UIView.swift var childForStatusBarStyle: UIViewController? var childForStatusBarHidden: UIViewController? 

使用这些property您可以创建一个控制器链,后者将负责显示status bar


安全区


如果容器按钮与内容重叠,则应增加safeArea区域。 这可以通过以下代码完成:为子控制器设置additinalSafeAreaInsets 。 您可以从embedController()调用它:


 private func addSafeArea(to controller: UIViewController) { if #available(iOS 11.0, *) { let buttonHeight = CGFloat(30) let topInset = UIEdgeInsets(top: buttonHeight, left: 0, bottom: 0, right: 0) controller.additionalSafeAreaInsets = topInset } } 

如果在顶部添加30个点,则该按钮将停止重叠内容,并且safeArea将占据绿色区域:



保证金。 保留超级视图边距


控制器具有标准margins 。 通常,它们从屏幕的每一侧等于16点,仅在加大尺寸上为20点。


根据margins您可以创建常量,不同iPhone的边缘缩进量将有所不同:



当我们将一个UIView放入另一个UIViewmargins减半:降至8点。 为防止这种情况,您需要包括Preserve superview margins 。 然后,子级UIViewmargins将等于父级UIViewmargins 。 适用于全屏容器。


结束


容器控制器是一个强大的工具。 它们简化了代码,分离了任务,并且可以重复使用。 您可以以任何方式编写嵌套控制器:在UIStoryboard ,在UIStoryboard中或仅在代码中。 最重要的是,它们易于创建且易于使用。


GitHub上一篇文章的示例


您是否有值得从中制作模板的屏幕? 分享评论!

Source: https://habr.com/ru/post/zh-CN434470/


All Articles