BambooHero

iOSアプリ開発と株式投資をメインに色々書きます

【SwiftUI】Listの使い方と一意性についての注意

f:id:bamboohero:20210529003414p:plain

SwiftUIでデータをリスト表示するときに使用するListの使い方と、Listを使うときに注意すべきデータの一意性についてまとめました。



Listの使い方

人物名をリスト表示する例を取り上げます。

f:id:bamboohero:20210528170218p:plain:w500


実装はこんな感じです。

// [2]
private struct Person: Identifiable {
    let id = UUID()
    let name: String
}

private let persons: [Person] = [
    Person(name: "Alice"),
    Person(name: "Bob"),
    Person(name: "Carol"),
    Person(name: "Dave")
]

struct ListTestView: View {
    var body: some View {
        List {
            // [1]
            ForEach(persons) { person in
                Text(person.name)
            }
        }
    }
}

[1]
personsの数だけForEachがイテレートされ、List内にTextが並びます。

[2]
ForEachにはIdentifiableに準拠した型の配列を渡す方法と、後述するKeyPathを使う方法があります。


KeyPathを使う場合はこんな感じの実装になります。

// [1]
private struct Person {
    let id = UUID()
    let name: String
}

private let persons: [Person] = [
    Person(name: "Alice"),
    Person(name: "Bob"),
    Person(name: "Carol"),
    Person(name: "Dave")
]

struct ListTestView: View {
    var body: some View {
        List {
            // [2]
            ForEach(persons, id: \.id) { person in
                Text(person.name)
            }
        }
    }
}

struct ListTestView_Previews: PreviewProvider {
    static var previews: some View {
        ListTestView()
    }
}

[1]
先ほどとの違いはIdentifiableが無いところだけです。

[2]
引数idにKeyPathを指定します。ここではPersonのidプロパティを指定していますが、Hashableに準拠している型のプロパティであれば何でも指定できます。例えば、PersonのnameプロパティはStringで、StringはHashableに準拠しているので、以下のように指定することができます。

ForEach(persons, id: \.name) { person in
    Text(person.name)
}


データの一意性を担保する

ForEachに渡すコレクションのデータは一意性が担保されている必要があります。

SwiftUIはデータが一意に識別可能であることで、コレクションデータの追加・削除に伴うアニメーションを自動で行ってくれるとドキュメントに記載があります。UITableViewやUICollectionViewでは自分でこれらを実装する必要がありました。

Members of a list must be uniquely identifiable from one another. Unique identifiers allow SwiftUI to automatically generate animations for changes in the underlying data, like inserts, deletions, and moves.

https://developer.apple.com/documentation/swiftui/displaying-data-in-lists


さて、一意性を担保するのは実装者の責任です。注意しないとIdentifiableに準拠していたとしても一意性を担保できていないことがあり、データが正しく表示されません。

以下は一意性が担保されていない悪い例です。

// [1]
private struct Person: Identifiable {
    let id: Int
    let name: String
}

private let persons: [Person] = [
    Person(id: 1, name: "Alice"),
    Person(id: 2, name: "Bob"),
    Person(id: 1, name: "Carol"),  // [2]
    Person(id: 3, name: "Dave")
]

struct ListTestView: View {
    var body: some View {
        List {
            ForEach(persons) { person in
                Text(person.name)
            }
        }
    }
}

struct ListTestView_Previews: PreviewProvider {
    static var previews: some View {
        ListTestView()
    }
}

[1]
PersonはIdentifiableに準拠しています。一意なインスタンスであることを示すidプロパティはInt型で定義されています。

[2]
idプロパティの値がname: "Alice"name: "Carol"で同じ1が指定されていて、重複してしまっています。

この実装だと以下のような表示になります。Carolが表示されてほしいところがAliceとなっています。

f:id:bamboohero:20210529002158p:plain:w500


推測ですが、ForEachがidの値を元にデータをコレクションから抽出するため、3つ目のデータを抽出しようとしたときにid=1なのでname: "Alice"のデータが抽出されてしまっているのではないかと思います。

Identifiableに準拠しているからといって一意性が担保されているわけではないので、idプロパティの型および値の設計には注意が必要です。


まとめ

Listの使い方について簡単にご紹介しました。Listにコレクションデータを渡すときは、Identifiableに準拠した型を渡す方法と、KeyPathを使う方法があることを説明しました。

また、データの一意性に注意を払う必要があることについて、具体的な悪い例を取り上げて説明しました。

UIKit時代と比べてリスト表示の実装が格段にシンプルになりましたが、Identifiableなど新しい知識も必要となっていますね。