Lazy stacks 不是只省記憶體:
它們會用估算與預取換取捲動流暢度
這支 session 的重點不是「用 LazyVStack 取代 VStack」而已,而是理解 SwiftUI 為什麼會估算 off-screen view 的尺寸、何時會提前 evaluate body、以及哪些寫法會讓 programmatic scrolling 和 state 保存變得不穩。
一句話結論
Lazy stack 的核心契約是:SwiftUI 不需要一次建立所有子視圖,但因此必須估算還沒載入的內容尺寸。你可以把它想成一個只看局部資料的 layout engine;它很快,但最怕「view body 裡才決定資料是否存在」和「view 出現後才大幅改變尺寸」。
如果你的捲動定位、內容高度或重要狀態依賴 off-screen view 的實際存在,lazy stack 會把這些假設暴露成 hitch、offset 跳動或狀態遺失。
Layout:尺寸是估算,不是帳本
LazyVStack 只把可見 rect 填滿,離開畫面的子視圖會被移出 lazy stack。為了讓 ScrollView 知道整段內容大約有多長,它會用已放置 view 的平均尺寸與剩餘數量估算完整 content size。當後續載入的 view 尺寸與估算不同,content size 和 offset 也會修正。
實務上要避免用 lazy stack 的 absolute content size 或 content offset 做精準邏輯。這些值會隨著新的 view 被量測而變動,拿它們當穩定座標系,最容易在長列表或跳轉時出現微妙錯位。
Subview loading:不要在 leaf body 做資料過濾
影片最值得記下的是:你寫的一個 view struct 不一定對應 lazy stack 看到的一個 subview。若 StepView 的 body 在頂層回傳兩個 view,lazy stack 可能分別載入它們;若 body 依環境值決定回傳一個或零個 view,lazy stack 為了維持 index 可能保留更早的 view。
這代表在 ForEach 的 leaf view 裡用條件式內容來「過濾資料」不是好主意。更好的做法是在資料層先過濾,例如 SwiftData 用 Predicate,讓 lazy stack 一開始就知道有幾個子視圖,不必先建 view 才知道 count 與 index。
Prefetch:onAppear 不是初始化生命週期
lazy stack 會在捲動方向快到新內容時提前做部分工作,例如 evaluate body 或 layout,讓真正進入畫面時不用在單一 frame 內完成所有 expensive work。這是 smooth scrolling 的重要來源。
但若你把每個 cell 的 setup 放在 onAppear,而且出現後才改變尺寸或內容,等於讓 prefetch 做好的 work 報廢並重做。可提前準備的資料與合理初始狀態應放在 initializer、model object 或 cache loader;onAppear 比較適合 infinite scrolling 這類「真的進入畫面才觸發」的行為。
Programmatic scrolling:ID 可跳,但布局要穩
SwiftUI 可以 scroll 到還沒在畫面上的 ID,但 lazy stack 仍得估算那個 view 的位置。最順的情況是 ForEach 的每筆資料固定解析成單一 subview,lazy stack 可以快速查 ID 與 count,不用建立大量 view。
相反地,若 leaf view 可能解析成零個或多個 subviews,或透過 onGeometryChange 改 state 再觸發第二次 layout,跳轉位置就更容易不穩。需要複雜尺寸關係時,優先考慮 SwiftUI layout primitives 或 custom layout,而不是讓 view 出現後再回頭改高度。
我會立刻採用的四條規則
- →Lazy stack 用估算換效率;off-screen 尺寸和 offset 會隨載入修正。
- →在 leaf view 裡用條件式內容過濾資料,會讓 index 與生命週期變複雜。
- →prefetch 可能先 evaluate body;不要把 setup 全押在 onAppear。
- →programmatic scrolling 最吃「每筆資料解析成單一穩定 subview」這個條件。