
概覽
在 SwiftUI 的世界里,我們無數(shù)次都夢想著視圖可以自動根據(jù)布局上下文“因勢而變”。大多數(shù)情況下,SwiftUI 會將每個視圖尺寸處理的井井有條,不過在某些時候我們還是得親力親為。

如上圖所示,無論頂部 TabView 容器里子視圖高度如何變化,TabView 本身的高度都能“隨遇而安”。如何用最簡單、最現(xiàn)代化、最有趣且最切中要害的方法讓容器尺寸與子視圖的高度“如影隨形”呢?
在本篇博文中,您將學到如下內(nèi)容:
- 最難滿足編譯器的方法:visualEffect
7.1 第一個問題
7.2 第二個問題 - 避免遞歸渲染(Recursive rendering)的一點考慮
相信學完本課后,小伙伴們必能腦洞大開、格局打開,用“千姿百態(tài)”的方法讓問題的解決一發(fā)入魂、九轉(zhuǎn)功成!
那還等什么呢?Let‘s go!??!;)
7. 最難滿足編譯器的方法:visualEffect
其實,自從一開始在博文開頭拋出這個問題,很多禿頭小伙伴們可能就已經(jīng)想到用 visualEffect 方法了:

實際上,visualEffect 修改器方法的本職工作是在 SwiftUI 視圖上更順暢的應用可視特效(Effects),提供幾何數(shù)據(jù)只是它的“副業(yè)”而已。
利用 visualEffect 方法來獲取 SwiftUI 視圖高度原本很簡單:
likeIdiomCard(idiom)
.visualEffect { content, proxy in
let height = proxy.size.height
if height > maxHeight {
maxHeight = height
}
return content
}
不過,上述代碼會有兩個問題。
7.1 第一個問題
首先,如果我們編譯運行則會發(fā)現(xiàn) visualEffect 方法的閉包并不會得到調(diào)用。這是因為直接照原樣返回 content 貌似并不會觸發(fā)閉包的回調(diào),仔細想想也可以理解:將心比心,如果新視圖的特效和原來如出一轍,為毛還要浪費渲染算力呢?
這個問題很好解決,只需“瞞天過海”讓 SwiftUI 渲染引擎以為我們應用了不同的特效即可:
return effect.offset(.zero)
7.2 第二個問題
第二個問題是如果將編譯器切換到 Swift 6 或啟用 Swift 5 的嚴格并發(fā)模式,那么立馬就會觸發(fā) Compiler 的“牢騷滿腹”:

之前所有的實現(xiàn)都沒有類似的問題,visualEffect 為毛那么“難伺候”呢?雖說代碼也可以達到目的,但患有強迫癥的禿頭碼農(nóng)們又怎能善罷甘休!?
其實,編譯器如此這般抱怨也不完全是“空穴來風”,因為 SwiftUI 視圖的任何狀態(tài)默認都必須隱式在 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 閉包中創(chuàng)建了一個運行在 MainActor 上的任務(Task),這是通過用 @MainActor 修飾任務閉包來實現(xiàn)的。這樣一來,我們對于 maxHeight 狀態(tài)的讀寫操作會以“原子”的方式在主線程上執(zhí)行,不會再有任何同步問題,這自然讓編譯器乖乖閉嘴!
在其它情況下,可能 proxy 本身也不是可發(fā)送(Sendable )的對象,這時我們還可以使用 局部只讀臨時變量 來如愿以償:
likeIdiomCard(idiom)
.visualEffect { effect, proxy in
// 假設 proxy 不是可發(fā)送的對象
Task {@MainActor [height = proxy.size.height] in
if height > maxHeight {
maxHeight = height
}
}
return effect.offset(.zero)
}
8. 避免遞歸渲染(Recursive rendering)的一點考慮
現(xiàn)在,經(jīng)過小伙伴們的不懈努力,上面所有 5 種方法都能圓滿的完成任務。
不過,如果“吹毛求疵”的我們希望 TabView 自適應的高度能夠與底部有一些空隙,我們可能會這么寫:
TabView {
//...
}
.tabViewStyle(.page)
.frame(height: maxHeight + 20)
在上面的實現(xiàn)中,我們“貼心”的讓 TabView 的高度在 maxHeight 基礎上增加 20 以獲得一些底部的間隙。
但是,倘若我們膽敢運行上述代碼,TabView 自身的高度就會立即進入“突飛猛漲”的節(jié)奏,讓小伙伴們目瞪口呆:

仔細觀察 Xcode 預覽中的調(diào)試日志就會發(fā)現(xiàn),我們可憐的 TabView 實際在以每次 20 的速率瘋狂的長高ing。我們稱這種現(xiàn)象稱為典型的遞歸渲染(Recursive rendering 或渲染反噬)。
造成這種情況的原因是:每次好不容易用 maxHeight 設置了 TabView 的高度之后,我們又“貪得無厭”的增加了 TabView 的高度,這樣會再次迫使 maxHeight 以新的高度重新求值,從而周而復始沒完沒了。
解決這種問題的辦法有很多,一種就是直接打破“死循環(huán)”,讓桎梏煙消云散:
TabView {
//...
}
.tabViewStyle(.page)
.frame(height: maxHeight)
.padding(.bottom, 20)
如上代碼所示,我們放棄了對 maxHeight “指手畫腳”的企圖,而是轉(zhuǎn)而使用 padding 修改器方法達到了相同的目的。這時,maxHeight 設置的高度和用 paddding 增加的間隙會彼此獨立,從而不會有任何渲染死循環(huán),棒棒噠!??

在下一篇博文中,我們最終將用 Layout 自定義布局來精心打造一款可以自動計算子視圖最大高度的容器,敬請期待吧!
總結(jié)
在本篇博文中,我們先是搞定了最讓編譯器頭疼的 visualEffect 實現(xiàn),隨后介紹了什么是遞歸渲染以及如何讓其“煙消云散”。
感謝觀賞,下一篇再見!8-)