スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

SwiftUI: Explicit Identity の正しい使い方と落とし穴

こんにちは。iOS エンジニアの @_nkmrh です。

SwiftUI が発表されてからはや4年が経ち、昨今のプロダクションコードでも多く活用されているのではないでしょうか。

そこで本稿では SwiftUI を活用する上で欠かすことのできない SwiftUI.View の Explicit Identity についておさらいしていきたいと思います。

Explicit Identity とは

まずはじめに、SwiftUI の Explicit Identity とはなんなのかを説明しておきたいと思います。

Explicit Identity は SwiftUI が個々のView を識別するための識別子です。UIView は class オブジェクトでしたので個々の UIView を識別するためにはオブジェクトのポインタを使っていましたが、SwiftUI の View は struct ですのでポインタはありません。そこで id を付与し個々の View を識別します。

プログラマ.id() モディファイアを付与しなくても SwiftUI は自動的に body の中で宣言されている個々の View に id(implict identity) を付与し識別しています。

これらの id は SwiftUI が View のライフサイクルを管理するために利用されます。

良い id の条件

  • id はユニークでなければいけない(複数の View が1つのidを共有することはできない)
  • id は時間と共に変化してはいけない(コンピューテッドプロパティなど都度計算されるものは用いない)

悪い id 指定の例

ここからは悪い id 指定の例をご紹介します。

下記の swift コードは説明のための簡略化したコードです。 Lesson 構造体からなる lessons 配列があり、ForEach を使って title の値を Text で表示させています。 Text の表示の他に、index の値によって追加する View があるとします。

struct Lesson {
    var title: String
}

// ...省略
@State var lessons: [Lesson] = [
    Lesson(title: "a"),
    Lesson(title: "b"),
    Lesson(title: "c"),
    Lesson(title: "d"),
]

ForEach(lessons.indices, id: \.self) { index in
    Text(lessons[index].title))
     // index を利用した処理
}

このコードは ForEach メソッドの id 引数に lessons 配列のインデックス値を指定しています。 このように書くと Text に id モディファイアでインデックス値を指定しているのと同じような意味として SwiftUI に伝えていることになります。

 Text(lessons[index].title)
   .id(index)

実際、公式ドキュメントには、

「If the id of a data element changes, then the content view generated from that data element will lose any current state and animations.」

と明記されています。これは、データ要素の id が変わると、そのデータ要素から生成されたコンテンツビューは現在の状態やアニメーションを失うことを意味しています。

この書き方が良くない理由として、lessons 配列の先頭に新たな値(Lesson(Lessons(title: "z")) が挿入された場合、配列の値は

 [
    Lesson(title: "z"),
    Lesson(title: "a"),
    Lesson(title: "b"),
    Lesson(title: "c"),
    Lesson(title: "d"),
 ]

となりますが、この時 ForEach が生成するそれぞれの View は Text("z").id(0) Text("a").id(1) のようにインデックス値が1つずれるため、SwiftUI の立場から見ると View の id が変化したため、これまでの Text("a").id(0) を破棄して新しい Text("a").id(1) を保持し描画することになります。

これは先ほどの、「id は時間と共に変化してはいけない」ということや、「id は View のライフサイクルに利用される」ということを示しています。 本来であれば同じ Text("a") を表示するための View なので View を破棄し再生成/再描画する必要は無いのですが、このように id が更新されることにより、ForEach で作成される全ての View を破棄し再生成/再描画することになります。これでは SwiftUI の差分レンダリングによるパフォーマンスの恩恵を受けられなくなってしまいます。

また、それだけではなくアニメーションを指定していた場合、適切なアニメーション効果が View に適用されないバグとなって現れてしまいます。

良い id 指定の例

良い id 指定の例を見てみましょう。

Identifiable に準拠させ、前述した良い id の条件を満たす値を id プロパティから返すようにします。

struct Lesson: Identifiable {
    var databaseId: String
    var title: String
    var id: String {
        databaseId
    }
}

// ...省略

 ForEach(lessons) { lesson in
   Text(lesson.title)
   if let index = lessons.firstIndex { $0.id == lesson.id } {
     // index を利用した処理
   }
 }

このように良い id を指定することで、SwiftUI の意図した設計通りの動作をすることができます。

まとめ

このように Explicit Identity は SwiftUI を使う上で基本的で重要な概念ですが、普段開発をしているとこのような概念をうっかり忘れてしまったり、悪い id の例のように画面を実装しても、要件を満たすことが可能なケースもあるかもしれません。

しかし、SwiftUI は id の取り扱いに特に依存しており、その仕組みの中で動作しています。適切な id を提供することは、ユーザーにとって質の高いアプリ体験を実現するために不可欠です。

今回は以上となります。 それでは良い Swift ライフを!

採用情報

SwiftUI や iOS開発に興味があり、新しい技術や知識を追求したい方は、以下のリンクから採用情報をご確認ください。

https://brand.studysapuri.jp/career/category/engineer/#openPositions