[SwiftUI] Listでifを使うと初回ロードが重くなる...?

Listを使って縦スクロール型のビューを組んでいて、初回表示が重くなる問題にぶち当たっています。



実際のコードはもっと複雑ですが、やりたいことはこんな感じです。

List内の各行はItem.typeごとに別々のビューを表示させるようにしています。

struct ContentView: View {
    var body: some View {
        List {
            ForEach(Model.items) {
                ItemView(item: $0)
            }
        }
    }
}

struct ItemView: View {
    let item: Item

    var body: some View {
        if item.type == .type1 {
            Item1View(item: item)
        } else {
            Item2View(item: item)
        }
    }
}


一見問題なさそうに見えます。

表示が崩れたりといった問題もないのですが、このifを使ったビュー構造だと初回ロードが重くなってしまうようなのです。


_printChanges()を使って、ItemViewが呼ばれるタイミングを見てみます。

struct ItemView: View {
    let item: Item

    var body: some View {
        let _ = Self._printChanges()  // 追加

        if item.type == .type1 {
            Item1View(item: item)
        } else {
            Item2View(item: item)
        }
    }
}


すると、画面上は8アイテム分しか表示されていないのにもかかわらず、ForEachに渡しているアイテム数分(今回は100で試しました)、ItemViewの初期化が行われていることがわかりました。

画面はこんな感じ。

f:id:bamboohero:20220322014813p:plain:w400


Xcodeコンソールの表示(100行出力されている)。

f:id:bamboohero:20220322014743p:plain


Memory Graphを見てみると、セルは8アイテム分のみメモリにロードされているようでした。

f:id:bamboohero:20220322014900p:plain


ifを使わずにItem1Viewだけを表示するようにしてみると、7アイテム分のみItemViewの初期化が行われました(Item1Viewの方がItem2Viewより高さが大きいため7アイテム分の表示になる)。

struct ItemView: View {
    let item: Item

    var body: some View {
        let _ = Self._printChanges()

        // if item.type == .type1 {
            Item1View(item: item)
        // } else {
        //     Item2View(item: item)
        // }
    }
}


画面。

f:id:bamboohero:20220322015204p:plain:w400


Xcodeコンソールの表示(7行出力されている)。

f:id:bamboohero:20220322014949p:plain


Memory Graph。

f:id:bamboohero:20220322015248p:plain


List内部の仕組みはよくわかっていませんが、ifの分岐があることで高さ計算が複雑になって全アイテム数分初期化している、みたいなことなのでしょうか...?

これが根本原因なのかはまだわかってないのですが、初回ロードが重くなる現象に関係があると睨んでいます。


なお、以下のようにForEach直下でif文を使った場合も同じで、Model.itemsの全アイテム数分初期化処理が行われました。

debugPrint()についてはこちらをご参照ください。

struct ContentView: View {
    var body: some View {
        List {
            ForEach(Model.items) { item in
                if item.type == .type1 {
                    Item1View(item: item)
                        .debugPrint("\(item.id)")
                } else {
                    Item2View(item: item)
                        .debugPrint("\(item.id)")
                }
            }
        }
    }
}


ちなみに、ScrollView+LazyVStackを使うと、if文を使っても画面に表示されている分しか初期化処理されませんでした。

LazyVStackなので想定通りですね。

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(Model.items) {
                    ItemView(item: $0)
                }
            }
        }
    }
}

struct ItemView: View {
    let item: Item

    var body: some View {
        if item.type == .type1 {
            Item1View(item: item)
        } else {
            Item2View(item: item)
        }
    }
}


ただ、LazyVStackだとアイテム数分のビューをすべてメモリに保持してしまうので、アイテム数が多いと下にスクロールしていくとどんどん重くなっていくという別の問題が出てきます。

このためScrollView+LazyVStackに単純に置き換えればいいというわけでもないので、なかなか苦労しています。。