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

概览

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

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

在本篇博文中,您将学到如下内容:

  1. 最难满足编译器的方法:visualEffect
    7.1 第一个问题
    7.2 第二个问题
  2. 避免递归渲染(Recursive rendering)的一点考虑

相信学完本课后,小伙伴们必能脑洞大开、格局打开,用“千姿百态”的方法让问题的解决一发入魂、九转功成!

那还等什么呢?Let‘s go!!!;)


7. 最难满足编译器的方法:visualEffect

其实,自从一开始在博文开头抛出这个问题,很多秃头小伙伴们可能就已经想到用 visualEffect 方法了:

实际上,visualEffect 修改器方法的本职工作是在 SwiftUI 视图上更顺畅的应用可视特效(Effects),提供几何数据只是它的“副业”而已。

利用 visualEffect 方法来获取 SwiftUI 视图高度原本很简单:

likeIdiomCard(idiom)
    .visualEffect { content, proxy in
        let height = proxy.size.height
        if height > maxHeight {
            maxHeight = height
        }
        
        return content
    }

不过,上述代码会有两个问题。

7.1 第一个问题

首先,如果我们编译运行则会发现 visualEffect 方法的闭包并不会得到调用。这是因为直接照原样返回 content 貌似并不会触发闭包的回调,仔细想想也可以理解:将心比心,如果新视图的特效和原来如出一辙,为毛还要浪费渲染算力呢?

这个问题很好解决,只需“瞒天过海”让 SwiftUI 渲染引擎以为我们应用了不同的特效即可:

return effect.offset(.zero)

7.2 第二个问题

第二个问题是如果将编译器切换到 Swift 6 或启用 Swift 5 的严格并发模式,那么立马就会触发 Compiler 的“牢骚满腹”:

之前所有的实现都没有类似的问题,visualEffect 为毛那么“难伺候”呢?虽说代码也可以达到目的,但患有强迫症的秃头码农们又怎能善罢甘休!?

其实,编译器如此这般抱怨也不完全是“空穴来风”,因为 SwiftUI 视图的任何状态默认都必须隐式在 MainActor 上访问和修改,但 visualEffect 方法的闭包显然无法做此保证。于是乎,一种简单的方法就是我们自己撸码来确保这一点:

likeIdiomCard(idiom)
    .visualEffect { effect, proxy in
        Task {@MainActor in
            let height = proxy.size.height
            if height > maxHeight {
                maxHeight = height
            }
        }
        
        return effect.offset(.zero)
    }

在上面的代码中,我们在 visualEffect 闭包中创建了一个运行在 MainActor 上的任务(Task),这是通过用 @MainActor 修饰任务闭包来实现的。这样一来,我们对于 maxHeight 状态的读写操作会以“原子”的方式在主线程上执行,不会再有任何同步问题,这自然让编译器乖乖闭嘴!

在其它情况下,可能 proxy 本身也不是可发送(Sendable )的对象,这时我们还可以使用 局部只读临时变量 来如愿以偿:

likeIdiomCard(idiom)
    .visualEffect { effect, proxy in
        // 假设 proxy 不是可发送的对象
        Task {@MainActor [height = proxy.size.height] in
            if height > maxHeight {
                maxHeight = height
            }
        }
        
        return effect.offset(.zero)
    }

8. 避免递归渲染(Recursive rendering)的一点考虑

现在,经过小伙伴们的不懈努力,上面所有 5 种方法都能圆满的完成任务。

不过,如果“吹毛求疵”的我们希望 TabView 自适应的高度能够与底部有一些空隙,我们可能会这么写:

TabView {    
    //...
}
.tabViewStyle(.page)
.frame(height: maxHeight + 20)

在上面的实现中,我们“贴心”的让 TabView 的高度在 maxHeight 基础上增加 20 以获得一些底部的间隙。

但是,倘若我们胆敢运行上述代码,TabView 自身的高度就会立即进入“突飞猛涨”的节奏,让小伙伴们目瞪口呆:

仔细观察 Xcode 预览中的调试日志就会发现,我们可怜的 TabView 实际在以每次 20 的速率疯狂的长高ing。我们称这种现象称为典型的递归渲染(Recursive rendering 或渲染反噬)。

造成这种情况的原因是:每次好不容易用 maxHeight 设置了 TabView 的高度之后,我们又“贪得无厌”的增加了 TabView 的高度,这样会再次迫使 maxHeight 以新的高度重新求值,从而周而复始没完没了。

解决这种问题的办法有很多,一种就是直接打破“死循环”,让桎梏烟消云散:

TabView {    
    //...
}
.tabViewStyle(.page)
.frame(height: maxHeight)
.padding(.bottom, 20)

如上代码所示,我们放弃了对 maxHeight “指手画脚”的企图,而是转而使用 padding 修改器方法达到了相同的目的。这时,maxHeight 设置的高度和用 paddding 增加的间隙会彼此独立,从而不会有任何渲染死循环,棒棒哒!💯

在下一篇博文中,我们最终将用 Layout 自定义布局来精心打造一款可以自动计算子视图最大高度的容器,敬请期待吧!

总结

在本篇博文中,我们先是搞定了最让编译器头疼的 visualEffect 实现,随后介绍了什么是递归渲染以及如何让其“烟消云散”。

感谢观赏,下一篇再见!8-)

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容