SwiftUI 让视图自适应高度的 6 种方法(二)

0 阅读4分钟

在这里插入图片描述

概览

在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”‌。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。

在这里插入图片描述

如上图所示,无论顶部 TabView 容器里子视图高度如何变化,TabView 本身的高度都能“随遇而安”。如何用最简单、最现代化、最有趣且最切中要害的方法让容器尺寸与子视图的高度“如影随形”呢?

在本篇博文中,您将学到如下内容: 4. 最复杂的方法:anchorPreference

  1. 最简单的方法:onGeometryChange
  2. 最有“创意”的方法: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-)