SwiftUI 讓視圖自適應高度的 6 種方法(三)

概覽

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

如上圖所示,無論頂部 TabView 容器里子視圖高度如何變化,TabView 本身的高度都能“隨遇而安”。如何用最簡單、最現(xiàn)代化、最有趣且最切中要害的方法讓容器尺寸與子視圖的高度“如影隨形”呢?

在本篇博文中,您將學到如下內(nèi)容:

  1. 最難滿足編譯器的方法:visualEffect
    7.1 第一個問題
    7.2 第二個問題
  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-)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容