概览
在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。
如上图所示,无论顶部 TabView 容器里子视图高度如何变化,TabView 本身的高度都能“随遇而安”。如何用最简单、最现代化、最有趣且最切中要害的方法让容器尺寸与子视图的高度“如影随形”呢?
在本篇博文中,您将学到如下内容: 4. 最复杂的方法:anchorPreference
- 最简单的方法:onGeometryChange
- 最有“创意”的方法:Canvas
相信学完本课后,小伙伴们必能脑洞大开、格局打开,用“千姿百态”的方法让问题的解决一发入魂、九转功成!
那还等什么呢?Let‘s go!!!;)
4. 最复杂的方法:anchorPreference
除了 GeometryReader 以外,在 SwiftUI 中同样还有一个“六朝元老” anchorPreference 修改器方法:
anchorPreference 方法可以将任何视图的几何属性(geometry value)反向传递给父视图。这些几何属性里不多不少刚好包含了一个 bounds 属性可以被用来获取视图的尺寸。不过,anchorPreference 方法不能“独自为战”,它必须与 GeometryReader 携手才能完成任务。
首先,我们需要创建一个 HeightKey 将 anchorPreference 修改器方法的“侦测”结果与特定键对应起来,可以看到它必须遵循 PreferenceKey 协议:
struct : PreferenceKey {
static var defaultValue = [Anchor<CGRect>]()
static func reduce(value: inout [Anchor<CGRect>], nextValue: () -> [Anchor<CGRect>]) {
value.append(contentsOf: nextValue())
}
}
接着,我们可以 happy 地获取每个 likeIdiomCard 视图的高度了:
TabView {
ForEach(likeIdioms.chunked(into: 2), id: \.self) { idiomChunk in
HStack {
ForEach(idiomChunk) { idiom in
likeIdiomCard(idiom)
.anchorPreference(key: HeightKey.self, value: .bounds) {
[$0]
}
}
if idiomChunk.count == 1 {
Rectangle()
.foregroundStyle(.clear)
}
}
}
}
.backgroundPreferenceValue(HeightKey.self) { datas in
GeometryReader { proxy in
if let max = datas.map({ proxy[$0].size.height }).max(), max > maxHeight {
maxHeight = max
}
return Text("Stub").opacity(0)
}
}
.tabViewStyle(.page)
.frame(height: maxHeight)
如您所见,我们先是调用 anchorPreference 方法获取了每个 likeIdiomCard 视图的 bounds 几何属性,随后我们在 GeometryReader 闭包中将高度抽取出来,并将最大值赋予 maxHeight,最后我们在百忙之中还不忘返回一个 “Stub” 占位视图,真乃殚思极虑也。
大家可以看到,使用 anchorPreference 方法貌似并不是那么“惬意”:
- 需要 PreferenceKey;
- 需要 GeometryReader 配合;
最大的问题是:anchorPreference 方法只是获取到了所需尺寸的数据,我们还需使用 backgroundPreferenceValue 或 onPreferenceChange 等方法来“接收”这些数据,这显得有些横生枝节。
所幸的是,苹果可能对此也有些卑陬失色,所以在 SwiftUI 的后续版本中做出了改善和简化。
5. 最简单的方法:onGeometryChange
正如上面所说的那样,苹果在 SwiftUI 4.0(iOS 16)中专门新增了一个 onGeometryChange 修改器方法来让视图几何信息的获取轻车简从:
现在,利用 onGeometryChange 方法,我们获取和接收视图尺寸的逻辑都能“如胶似漆”般的融合在一起了:
likeIdiomCard(idiom)
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.height
} action: { value in
if maxHeight < value {
maxHeight = value
}
}
从上面的代码可以看到:我们在 onGeometryChange 第一个闭包中获取到了视图的高度,随后立即在第二个闭包中完成了 maxHeight 的设置任务。这不妥妥的就是之前 anchorPreference 方法的加强和简化版吗?:)
6. 最有“创意”的方法:Canvas
除了使用修改器方法来获取指定视图的尺寸信息以外,我们还可以“灵机一动”使用 SwiftUI 3.0 推出的 Canvas 原生视图来“抽丁拔楔”:
从 Canvas 视图的定义可以看到,它的特长其实是高性能的实现 SwiftUI 界面的绘制工作。不过,这并不影响我们“乖巧的”利用其绘制回调闭包中的尺寸信息来“旁敲侧击”得到其对应视图的高度:
likeIdiomCard(idiom)
.background {
Canvas { context, size in
if size.height > maxHeight {
maxHeight = size.height
}
let rect = CGRect(origin: .zero, size: size)
context.fill(Path(rect), with: .color(.clear))
}
}
正如大家在上面代码中看到的那样,我们利用 Canvas 视图回调闭包中的 size 传入实参得到了 likeIdiomCard 视图的高度,并将其最大值赋给了 maxHeight 状态。注意,我们同样需要在最后绘制一个“占位”路径(Path)以“避免尴尬”。
在下一篇博文中,我们将会介绍另一种最让编译器“头疼”的方法并谈谈如何避免渲染反噬(Recursive rendering)。一言为定,等你们哦!
总结
在本篇博文中,我们分别介绍了另外 3 种“最复杂”、“最简单”以及最有“创意”的方法来让 SwiftUI 视图自适应尺寸这一问题“冰解的破”。
感谢观赏,我们下一篇再见!8-)