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

在这里插入图片描述

概览

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

在这里插入图片描述

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

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

那还等什么呢?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 增加的间隙会彼此独立,从而不会有任何渲染死循环,棒棒哒!💯

在这里插入图片描述


想要进一步系统地学习 Swift 开发的小伙伴们,可以来我的《Swift 语言开发精讲》专栏逛一逛哦:

在这里插入图片描述


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

总结

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

感谢观赏,下一篇再见!😎

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大熊猫侯佩

赏点钱让我买杯可乐好吗 ;)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值