iOS故事板:优缺点分析,最佳做法



苹果创建了Storyboard,以便开发人员可以可视化iOS应用程序的屏幕及其之间的关系。 并非每个人都喜欢此工具,这是有充分理由的。 我见过许多批评Storyboard的文章,但我没有考虑到最佳实践,对所有优点和缺点进行详尽而公正的分析。 最后,我决定自己写一篇这样的文章。

我将尝试详细分析使用情节提要的缺点和优势。 在权衡它们之后,您可以做出有意义的决定,以确定项目中是否需要它们。 这个决定不必太激进。 如果在某些情况下情节提要板产生问题,在其他情况下则应说明情节提要的使用:它有助于有效地解决任务并编写简单,易于维护的代码。

让我们从缺点开始,分析所有缺点是否仍然相关。

缺点


1.合并更改时,情节提要板很难管理冲突


故事板是一个XML文件。 它比代码可读性差,因此解决其中的冲突更加困难。 但是这种复杂性还取决于我们如何使用情节提要。 如果遵循以下规则,则可以大大简化您的任务:

  • 不要将整个UI放在一个Storyboard中,而是将其分成几个较小的Storyboard。 这将使在Storyboard上的开发人员之间分配工作没有冲突的风险,并且在不可避免的情况下-将简化解决它们的任务。
  • 如果需要在多个地方使用同一View,请在具有其自己的Xib文件的单独子类中选择它。
  • 进行提交的频率更高,因为处理零散的更改要容易得多。

使用多个情节提要板而不是一个,使我们无法在一个文件中查看应用程序的整个地图。 但这通常不是必需的-仅此刻我们正在研究的特定部分就足够了。

2.故事板可防止代码重用


如果我们谈论的是在项目中仅使用没有Xib的情节提要,那么肯定会出现问题。 但是,我认为Xib是使用Storyboard时的必要元素。 多亏了他们,您可以轻松创建可重用的视图,这些视图在代码中也很方便使用。

首先,创建XibView基类,该基类负责在Storyboard中渲染在UIView创建的UIView

 @IBDesignable class XibView: UIView { var contentView: UIView? } 

XibView将把XibViewUIView加载到contentView ,并将其添加为其子视图。 我们在setup()方法中执行此操作:

 private func setup() { guard let view = loadViewFromNib() else { return } view.frame = bounds view.autoresizingMask = [.flexibleWidth, .flexibleHeight] addSubview(view) contentView = view } 

loadViewFromNib()方法如下所示:

 private func loadViewFromNib() -> UIView? { let nibName = String(describing: type(of: self)) let nib = UINib(nibName: nibName, bundle: Bundle(for: XibView.self)) return nib.instantiate(withOwner: self, options: nil).first as? UIView } 

setup()方法应在初始化程序中调用:

 override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } 

XibViewXibView准备就绪。 外观在XibView文件中呈现的已重用视图将继承自XibView

 final class RedView: XibView { } 


如果现在将新的UIView添加到Storyboard并将其类设置为RedView ,那么一切将成功显示:

用代码创建RedView实例的方式通常是这样的:

 let redView = RedView() 

并非所有人都知道的另一个有用的细节是能够将颜色添加到.xcassets目录中。 这使您可以在使用它们的所有情节提要和Xib中进行全局更改。

要添加颜色,请单击左下方的“ +”,然后选择“新颜色集”:

指定所需的名称和颜色:

创建的颜色将出现在“命名的颜色”部分中:

另外,它可以在代码中获得:

 innerView.backgroundColor = UIColor(named: "BackgroundColor") 

3.您不能对在Storyboard中创建的UIViewControllers使用自定义初始化程序


对于情节提要,我们无法在UIViewControllers的初始化程序中传递依赖项。 通常看起来像这样:

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard segue.identifier == "detail", let detailVC = segue.destination as? DetailViewController else { return } let object = Object() detailVC.object = object } 

使用某种常量来表示标识符或诸如SwiftGenR.swift之类的工具甚至是Perform可以更好地完成此代码。 但是通过这种方式,我们仅摆脱了字符串文字并添加了语法糖,并且无法解决出现的问题:

  • 我如何知道上例中的DetailViewController如何配置的? 如果您不熟悉该项目,并且不具备此知识,则必须打开一个包含该控制器说明的文件并进行研究。
  • 在初始化后设置DetailViewController属性,这意味着它们必须是可选的。 有必要处理任何属性为nil ,否则应用程序可能在最不适当的时刻崩溃。 您可以将属性标记为隐式扩展的可选( var object: Object! ),但是本质不会改变。
  • 属性必须标记为var ,而不是let 。 因此,当外部有人想要更改它们时,可能会出现这种情况。 DetailViewController应该处理这种情况。

本文介绍了一种解决方案。

4.随着情节提要的增长,在其中进行导航变得更加困难


如前所述,您无需将所有内容都放在一个Storyboard中,最好将其分解为几个较小的内容。 随着情节提要参考的出现它变得非常简单。
将故事板引用从对象库添加到故事板:

我们在“ 属性”检查器中设置了必填字段值-这是情节提要板文件的名称,如有必要,还指定了“ 参考ID” ,该ID与所需屏幕的情节提要ID相对应。 默认情况下, 初始视图控制器将加载:

如果您在“情节提要”字段中指定了无效的名称或引用了不存在的情节提要ID,则Xcode将在编译阶段就此警告您。

5.加载情节提要时Xcode变慢


如果情节提要板包含大量具有众多约束的屏幕,则加载它确实需要一些时间。 但是话又说回来,最好将大型Storyboard分成较小的Storyboard。 另外,它们加载得更快,并且使用它们变得更加方便。

6.故事板很脆弱,错误可能导致应用程序在运行时崩溃


主要弱点:

  • UITableViewCellUICollectionViewCell错误。
  • segues标识符中的错误。
  • 使用不再存在的UIView子类。
  • IBActionsIBOutlets与代码的同步。

所有这些以及其他一些问题可能导致应用程序在运行时崩溃,这意味着此类错误很可能会落入发行版中。 例如,当我们在情节提要中设置单元格标识符或segue时,无论使用什么位置,都应将它们复制到代码中。 通过在一个位置更改标识符,必须在其余所有位置都更改它。 您可能会简单地忘记它或输入错误,但仅在应用程序运行时了解错误。

您可以通过消除代码中的字符串文字来减少错误的可能性。 为此,可以为UITableViewCellUICollectionViewCell分配单元格类本身的名称:例如, ItemTableViewCell标识符将为字符串“ ItemTableViewCell”。 在代码中,我们得到这样的单元格:

 let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ItemTableViewCell.self)) as! ItemTableViewCell 

您可以将相应的泛型函数添加到UITableView

 extension UITableView { open func dequeueReusableCell<T>() -> T where T: UITableViewCell { return dequeueReusableCell(withIdentifier: String(describing: T.self)) as! T } } 

然后变得更容易获得该单元格:

 let cell: ItemTableViewCell = tableView.dequeueReusableCell() 

如果您突然忘记在情节提要中指定单元格标识符的值,则Xcode将显示警告,因此您不应忽略它们。

至于segues标识符,您可以为它们使用枚举。 让我们创建一个特殊的协议:

 protocol SegueHandler { associatedtype SegueIdentifier: RawRepresentable } 

支持该协议的UIViewController将需要定义一个具有相同名称的嵌套类型。 它列出了此UIViewController可以处理的所有segues标识符:

 extension StartViewController: SegueHandler { enum SegueIdentifier: String { case signIn, signUp } } 

另外,在SegueHandler协议扩展中, SegueHandler定义了两个函数:一个接受UIStoryboardSegue并返回相应的SegueIdentifier值,另一个SegueIdentifier输入简单地调用performSegue

 extension SegueHandler where Self: UIViewController, SegueIdentifier.RawValue == String { func performSegue(withIdentifier segueIdentifier: SegueIdentifier, sender: AnyObject?) { performSegue(withIdentifier: segueIdentifier.rawValue, sender: sender) } func segueIdentifier(for segue: UIStoryboardSegue) -> SegueIdentifier { guard let identifier = segue.identifier, let identifierCase = SegueIdentifier(rawValue: identifier) else { fatalError("Invalid segue identifier \(String(describing: segue.identifier)).") } return identifierCase } } 

现在,在支持新协议的UIViewController中,您可以按以下方式使用prepare(for:sender:)

 extension StartViewController: SegueHandler { enum SegueIdentifier: String { case signIn, signUp } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segueIdentifier(for: segue) { case .signIn: print("signIn") case .signUp: print("signUp") } } } 

并像这样运行segue:

 performSegue(withIdentifier: .signIn, sender: nil) 

如果您向SegueIdentifier添加新的标识符,则Xcode肯定会强制它在switch/case进行处理。

摆脱字符串文字(如标识符segues等)的另一种选择是使用代码生成工具(如R.swift)

7.故事板的灵活性不及代码。


是的,这是真的。 如果任务是创建一个包含情节提要无法处理的动画和效果的复杂屏幕,那么您需要使用代码!

8.情节提要不允许更改特殊的UIViewControllers的类型


例如,当您需要将UITableViewController的类型更改为UICollectionViewController ,您必须删除该对象,添加具有另一种类型的新对象,然后重新配置它。 尽管这种情况并不常见,但值得注意的是,这些更改在代码中的执行速度更快。

9.情节提要向项目添加了两个附加依赖项。 它们可能包含开发人员无法修复的错误。


这是Interface Builder和Storyboards解析器。 这种情况很少见,通常可以通过其他解决方案来规避。

10.复杂的代码审查


请记住,代码审查并不是真正的错误搜索。 是的,它们是在查看代码的过程中发现的,但主要目标是确定从长远来看可能造成问题的弱点。 对于情节提要,这主要是Auto Layout的工作。 不应有任何歧义错位 。 要找到它们,只需在情节提要XML中对“模糊=“是””和“放错位置=“是””行进行搜索,或者仅在Interface Builder中打开情节提要并查找红色和黄色的点:

但是,这可能还不够。 在应用程序运行时,也可以检测到约束之间的冲突。 如果发生类似情况,则控制台中会显示有关此信息。 这种情况并不少见,因此,也应认真对待它们。

其他所有内容(将元素的位置和大小与设计相匹配, IBOutletsIBActions的正确绑定)均不用于代码审查。

另外,重要的是要更频繁地进行提交,这样审阅者就可以更轻松地查看小更改。 他将能够更好地研究细节而不会遗漏任何东西。 反过来,这将对代码审查的质量产生积极影响。

总结


在“情节提要”缺陷列表中,我留下了4个项目(按其值的降序排列):

  1. 合并更改时,情节提要板很难管理冲突。
  2. 故事板的灵活性不及代码。
  3. 故事板非常脆弱,错误可能导致运行时崩溃。
  4. 您不能对在Storyboard中创建的UIViewControllers使用自定义初始化程序。

好处


1.可视化的用户界面和约束


即使您是初学者,并且刚刚开始一个陌生的项目,也可以轻松找到该应用程序的入口点以及如何从中进入所需的屏幕。 您知道每个按钮,标签或文本字段的外观,它们将处于什么位置,约束如何影响它们,它们如何与其他元素交互。 只需单击几下,您就可以轻松创建新的UIView ,自定义其外观和行为。 自动布局使我们可以自然地使用UIView ,就像我们说的那样:“此按钮应该在该标签的左侧,并且具有相同的高度。” 这种用户界面体验是直观而有效的。 您可以尝试举一些示例,在这些示例中,编写良好的代码可以在创建某些UI元素时节省更多时间,但在全局范围内并不会改变太多。 情节提要很好地工作。

另外,请注意自动版式。 这是一个非常强大且有用的工具,如果没有它,将很难创建一个支持所有不同屏幕尺寸的应用程序。 Interface Builder使您无需启动应用程序即可查看使用Auto Layout的结果,并且如果某些约束不适合常规方案,Xcode会立即警告您。 当然,在某些情况下,Interface Builder无法提供某些非常动态和复杂的接口的必要行为,那么您就必须依靠代码。 但是即使在这种情况下,您也可以在Interface Builder中完成大部分操作,并仅用几行代码对其进行补充。

让我们看一些示例,这些示例演示Interface Builder的有用功能。

基于UIStackView动态表


创建一个新的UIViewController ,全屏添加一个UIScrollView

UIScrollView添加垂直UIStackView ,将其捕捉到边缘并将高度和宽度设置为与UIScrollView相等。 在此高度下,分配优先级=低(250)

接下来,创建所有必需的单元格并将它们添加到UIStackView 。 我们可能会在一个副本中使用普通的UIView ,或者可能是UIView ,为此我们创建了自己的Xib文件。 无论如何,该屏幕的整个UI都位于情节提要中,并且由于正确配置了自动版式,滚动可以完美地进行,以适应内容:



我们还可以使单元格适应其内容的大小。 将UILabel添加到每个单元格,将它们绑定到边缘:

现在已经清楚了如何在运行时查看这些内容。 您可以将任何操作附加到单元格,例如,切换到另一个屏幕。 而所有这些都不需要一行代码。
此外,如果为UIStackViewUIView设置hidden = true ,则它不仅会隐藏,而且不会占用空间。 UIStackView将自动重新计算其大小:



自定格单元


在表的“ 大小”检查器中 ,将“ 行高度=自动”设置为“平均值”,并将其设置为某个平均值:

为此,必须在单元格本身中正确配置约束,并允许基于运行时的内容准确计算单元格高度。 如果不清楚有什么危险, 官方文档中有很好的解释。

结果,启动应用程序,我们将看到一切正确显示:

自动上浆桌


您需要实现此表的行为:



如何实现类似的高度动态变化? 与UILabelUIButtonUIView其他子类不同,使用表要难一些,因为内在内容大小并不取决于表中单元格的大小。 她无法根据内容计算身高,但是有机会帮助她。

请注意,在视频中的某个位置,桌子的高度停止变化,达到某个最大值。 这可以通过将表高度约束设置为值Relation =小于或等于来实现

在此阶段,Interface Builder尚不知道桌子的高度,他只知道其最大值等于200(根据高度限制)。 如前所述,“内在内容大小”不等于表的内容。 但是,我们可以在“ 本征大小”字段中设置占位符:

该值仅在使用Interface Builder时有效。 当然,运行时内在内容大小不必等于此值。 我们只是告诉Interface Builder,一切都在控制之中。

接下来,创建CustomTableView表的新子类:

 final class CustomTableView: UITableView { override var contentSize: CGSize { didSet { invalidateIntrinsicContentSize() } } override var intrinsicContentSize: CGSize { return contentSize } } 

需要代码的情况之一。 每当表的contentSize更改时,我们就在这里调用invalidateIntrinsicContentSize 。 这将允许系统接受新的“内在内容大小”。 反过来,它返回contentSize ,迫使表动态调整其高度并显示一定数量的单元格而无需滚动。 当我们达到高度限制限制时,就会出现滚动。

所有这三个Interface Builder功能都可以相互组合。 它们为内容组织选项增加了更多灵活性,而无需其他约束或任何UIView

2.能够立即看到他们行动的结果


如果您调整UIView大小,将其移动到侧面两个点或更改背景颜色,您将立即看到它在运行时的外观,而无需启动应用程序。 不必怀疑为什么某些按钮没有出现在屏幕上,或者为什么UIView的行为不是所期望的。

使用@IBInspectable会更有趣地揭示此好处。 在RedView添加两个UILabel和两个属性:

 final class RedView: XibView { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var subtitleLabel: UILabel! @IBInspectable var title: String = "" { didSet { titleLabel.text = title } } @IBInspectable var subtitle: String = "" { didSet { subtitleLabel.text = subtitle } } } 

RedView属性检查器中将出现两个新字段- TitleSubtitle ,我们将其标记为@IBInspectable

如果尝试在这些字段中输入值,我们将立即看到一切在运行时的外观:



您可以控制任何东西: cornerRadiusborderWidthborderColor 。 例如,我们扩展了基类UIView

 extension UIView { @IBInspectable var cornerRadius: CGFloat { set { layer.cornerRadius = newValue } get { return layer.cornerRadius } } @IBInspectable var borderWidth: CGFloat { set { layer.borderWidth = newValue } get { return layer.borderWidth } } @IBInspectable var borderColor: UIColor? { set { layer.borderColor = newValue?.cgColor } get { return layer.borderColor != nil ? UIColor(cgColor: layer.borderColor!) : nil } } @IBInspectable var rotate: CGFloat { set { transform = CGAffineTransform(rotationAngle: newValue * .pi/180) } get { return 0 } } } 

我们看到RedView对象的Attributes Inspector获得了另外4个新字段,您现在也可以使用它们:



3.一次预览所有屏幕尺寸


因此,我们在屏幕上放置了必要的元素,调整了它们的外观并添加了必要的约束。 我们如何确定内容是否可以正确显示在不同的屏幕尺寸上? 当然,您可以在每个模拟器上运行该应用程序,但这会花费很多时间。 有一个更好的选择:Xcode具有预览模式,它使您无需启动应用程序即可一次查看多个屏幕尺寸。

我们调用助手编辑器 ,在其中单击过渡栏的第一段,选择“ 预览”->“ Settings.storyboard” (作为示例):

最初,我们只看到一个屏幕,但是我们可以通过单击左下角的“ +”并从列表中选择必要的设备来添加所需的内容:

此外,如果情节提要板支持多种语言,则可以看到所选屏幕在每种语言下的外观:

可以一次为所有屏幕选择语言,也可以分别为每个屏幕选择语言。

4.删除模板UI代码


在不使用Interface Builder的情况下创建用户界面时,会伴随大量的样板代码或需要额外维护工作的超类和扩展。 此代码可以渗透到应用程序的其他部分,从而使其难以阅读和搜索。 使用情节提要和Xibs可以减轻代码负担,使其更加专注于逻辑。

5.尺寸等级


每年都会出现新设备,您需要为其调整用户界面。 特质变化 (尤其是大小类)的概念对此有所帮助,它允许您为屏幕的任何大小和方向创建UI。

尺寸类别将设备屏幕的高度(h)和宽度(w)按照紧凑常规CR )分类。 例如,iPhone 8的纵向尺寸级别为(wC hR) ,横向的尺寸级别为(wC hC) ,而iPhone 8 Plus的尺寸级别分别为(wC hR)(wR hC) 。 其余的设备可以在这里找到。

在每种大小级别的一个Storyboard或Xib中,您可以存储自己的数据集,应用程序将在运行时根据设备和屏幕方向使用适当的数据集,从而标识当前的大小级别。如果某些布局参数对于所有尺寸类别均相同,则可以在“ Any类别(默认情况下已选择)中对其进行配置

例如,根据大小类别配置字体大小。我们选择iPhone 8 Plus设备以纵向显示在Storyboard中,并为font以下条件添加新条件:如果width为Regular(将其余设置为“ Any”),则字体大小应为37:

现在,如果我们更改屏幕方向,则字体大小将增加-一种新的条件将起作用,因为iPhone 8 Plus在横向方向上具有大小级别(wR hC)。在情节提要中,根据大小类,您还可以隐藏视图,启用/禁用约束,更改其值constant等等。在此处阅读有关如何执行所有操作的更多信息

在上面的屏幕截图中,值得注意的是底部面板选择了显示布局的设备。它使您可以快速检查UI在任何设备上以及在任何屏幕方向上的适应性,还可以显示当前配置的大小类别(在设备名称旁边)。除其他事项外,在右侧有一个按钮« 变化为特质”。其目的是仅同时对宽度,高度或宽度和高度的特定类别启用特征变化。例如,选择具有大小等级(wR hR)的iPad ,单击“因性状而异”,然后选中widthheight旁边的框现在,所有后续布局更改将仅适用于具有(wR hR)的设备,直到我们单击“完成变化”

结论


缺点
好处
1个
难以处理的冲突
UI可视化和约束
2
不如代码灵活
即时查看您的操作结果的能力
3
错误可能导致运行时崩溃。
一次预览所有屏幕尺寸
4
您不能使用自定义初始化程序来 UIViewControllers
删除模板UI代码
5
尺寸等级
我们看到情节提要有其优点和缺点。我认为您不应该完全拒绝使用它们。如果使用正确,它们将带来巨大的好处,并有助于有效地解决任务。您只需要学习如何确定优先级并忘记诸如“我不喜欢情节提要”或“我已经习惯这样做”之类的论点。

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


All Articles