PHBStackLayout
这是什么?
tl;dr -- 这是一个 μFramework,为基于 UIStackView 布局的 Swift 提供声明式语法。
背景 -- UIStackView 布局
UIStackViews 使布局构建变得非常强大。例如,假设我想为一个包含以下组件的表格视图单元格创建布局
- 左侧的图片,垂直居中
- 图片右侧的标题,与单元格顶部对齐
- 标题下方的副标题
- 单元格右侧的向下箭头图标
我可以通过大量 thing.anchor.constraint(equalTo: otherThing.anchor).isActive = true
-语句来实现这种布局。但是,根据您的约束自动布局经验,这些语句的清晰度并不总是很高 -- 尤其是当您不是只需要沿着单一方向排列组件时。
在我看来,如果像以下这样做布局,布局的意图会变得更为清晰
- 创建一个水平 UIStackView,从左到右包含图片、文本和向下箭头
- 创建一个垂直 UIStackView,从上到下包含标题和副标题
这样的实现更符合对布局的直观理解,它由一个主左到右布局和一个嵌套的上到下半布局组成。这种实现创建了一个自适应的、基于 AutoLayout 的 UI,无需任何约束。
作为额外的好处,视图堆栈处理隐藏的子视图的能力比基于约束的布局要好得多。如果一个堆栈视图排列的子视图被隐藏,整个布局将像这个视图根本不在布局中一样进行相应调整。例如,在上面的表格视图单元格中,如果我在某些情况下不希望单元格有一个向下箭头图标,我只需简单地将标签或图像视图的isHidden
属性设置为true,文本视图现在将从单元格的右侧边缘填充所有可用空间。
声明式语法包装器
UIStackView布局的一个问题是创建、配置和维护所有这些堆栈视图的繁琐。我想要创建一个包装器来帮我处理所有这些样板代码。这样的包装器也提供了一个机会来公开一些额外的功能,这些功能在创建UI时可能很有用,例如在元素之间添加间距或在元素周围添加内边距。由于我们正在摆脱约束,包装器还可以以方便的方式在父视图中安装布局。
这就是这个:一个在外观舒适且功能强大的语法下包装基于UIStackView的布局的框架。
关于SwiftUI呢?
SwiftUI是苹果公司最新的声明式UI范式。据所有报道,它将永远解决繁琐的布局代码问题。PHBStackLayout相形见绌。我为什么还要创建它?简单来说,因为我现在需要一些帮助我处理UI代码的东西,
- SwiftUI还没有准备好
- 我需要支持 SwiftUI 正式发布时不会支持的 iOS 版本
用法
示例
在深入研究细节之前,让我们先看看一个示例。
假设有一些子视图(一个图像视图、一个标题标签和一个副标题标签),考虑以下UITableViewCell子类中的布局代码
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let layout = StackLayout.inset(by: 8, direction: .vertical, of:
.inset(by: 9, direction: .horizontal, of:
.cols(alignment: .center, of: [
.view(self.imageView),
.spacing(of: 12: direction: .horizontal),
.rows(alignment: .left, of: [
.view(self.headingLabel),
.spacing(of: 4, direction: .vertical),
.view(self.subheadingLabel) ])])))
layout.install(in: self.contentView)
}
这创建并安装了一个布局,其中
- 所有内容都将其顶部和底部内缩了8点,左侧和右侧内缩了9点
- 图片视图位于左侧,垂直居中
- 标题标签位于图片视图右侧,偏移12点,位于单元格顶部
- 副标题标签位于标题标签下方4点,也相对于图片视图偏移12点,位于单元格底部
注意布局结构如何作为树状结构,以及子布局级别是如何通过代码中的缩进来表示的。
StackLayout树
堆叠布局是一种树,其中每个节点都是StackLayout
对象。
StackLayout
结构提供了5个静态方法来创建这样的节点
view
spacing
inset
rows
cols
尽管可以将这些视为方法,但它们也可以被视为节点的类型。其中一些包含子节点,而其他则作为叶节点。
最终,每个节点都与一个UIView
相关联。对于父节点,该视图包含与子节点相关联的子视图。对于叶节点,该视图没有子视图。任何布局树中的节点都可以通过访问其视图来安装到任何视图中(比如视图控制器根视图)。
布局节点
节点类型包括:
view
一个view
节点是一个代表单个视图的叶节点。它通过提供你希望节点表示的UIView子类来创建。
例如,为单个标签创建布局节点
let layout = StackLayout.view(UILabel())
rows
rows
节点是一个父节点,它以垂直堆叠的方式排列其子节点。它是通过提供要堆叠的子节点以及可选指定它们的水平对齐方式来创建的。
例如,创建两个标签行布局
let layout = StackLayout.rows(of: [
.view(UILabel()), // First row
.view(UILabel()) // Second row
])
在底层,rows
方法创建并配置了一个UIStackView1,配置其轴和对齐方式,然后将给定子节点的相关视图作为堆叠视图的排列子视图添加。
cols
cols
节点在功能上相当于rows
节点,但一个cols
节点按水平方式排列其子节点。它是通过提供要堆叠的子节点以及可选指定它们的垂直对齐方式来创建的。
例如,创建两个列布局
let layout = StackLayout.cols(of: [
.view(UILabel()), // First column
.view(UILabel()) // Second column
])
与rows
方法类似,底层cols
方法创建并配置了一个UIStackView1。
spacing
spacing
节点是一个代表特定维度和特定方向中空白空间的叶节点。它是通过指定维度和方向来创建的。一个spacing
节点用于在布局中的其他元素之间或周围创建填充。
例如,要创建一个标签两行布局,它们之间有10个点的间隔
let layout = StackLayout.rows(of: [
.view(UILabel()), // First row
.spacing(of: 10, direction: .vertical), // 10 points of spacing
.view(UILabel()) // Second row
])
在底层,spacing
方法创建一个 < abbrev>UIView1 并激活其宽度或高度约束。
inset
inset
节点是一个父节点,在它单个子节点的单侧轴上添加固定的填充。它是通过指定填充的尺寸和方向以及子节点来创建的。
例如,要创建一个带有10个点间距向左和向右的单个标签布局
let layout = StackLayout.inset(
by: 10,
direction: .horizontal,
of: .view(UILabel()))
inset
方法实际上是一个便利方法,用于创建包含三个视图的堆叠视图:中间的给定子视图的视图,以及围绕它的两个空白视图。因此,在底层,它创建了一个具有spacing
、view
和另一个spacing
节点的 rows
或 cols
节点,作为其子节点。
一些技术考虑因素
isUserInteractionEnabled 与 hitTest
一些实际情况下有复杂的交互要求。
例如,考虑一个类似卡的单元格。当用户点击卡片时,应用应导航到某处。卡片有一些子视图(图像、标签等),它们不应干扰整个卡片的交互性。但是,卡片底部有一个按钮,当点击时,应关闭卡片。
在这种情况下,如果将isUserInteractionEnabled
设置为容器视图(如用于布局的堆叠视图)是诱人的但很危险。这可能会使卡的所有子视图在默认情况下不可交互,并阻止子视图拦截应传递给卡片的点击。但实际上,子视图不仅会默认变为不可交互,而且没有方法可以使按钮变为交互式。
实际解决方案是子类化容器视图并覆盖 hitTest
以将交互传递到父类。
因此,在这个框架的实现中,节点不使用 UIView
空间,也不使用 UIStackView
堆叠。相反,使用那些类的自定义不可交互子类。请参阅 NonInteractableViews.swift
。
安装
Cocoapods
PHBStackLayout 通过 CocoaPods 提供使用。要安装它,只需将以下行添加到您的 Podfile
pod 'PHBStackLayout'
要运行示例项目,请克隆仓库,然后首先从 Example 目录运行 pod install
。
作者
phlippieb,[email protected]
许可证
PHBStackLayout 在 MIT 许可证下提供。有关更多信息,请参阅 LICENSE 文件。
脚注
1: 此框架实际上使用 UIView 和 UIStackView 的子类;请参阅 isUserInteractionEnabled vs hitTest