TL;DR:
正確にはこんなコードをやめましょうね:
extension Identifiable where Self: Hashable {
var id: Self { self }
}
まずこのようなコードを見てみましょう:
extension Identifiable where Self: Hashable {
var id: Self { self }
}
struct Post: Hashable, Identifiable {
let title: String
let body: String
var like: Bool
}
struct PostContentView: View {
@Binding var post: Post
var body: some View {
// ...
Button {
post.like.toggle()
} label: {
// ...
}
}
}
struct PostListView: View {
@State private var posts: [Post] = //...
var body: some View {
NavigationView {
List {
ForEach($posts) { $post in
NavigationLink {
PostContentView(post: $post)
} label: {
// ...
}
}
}
}
}
}
ふむふむ、我ながらなかなかいいコード書いたじゃないか。早速動作確認してみよう:
!???いいね!ボタン押すたびに、なぜかリストに戻ってしまうんですが!???
そう、元凶はまさにタイトルに書いた通り、extension Identifiable where Self: Hashable
のところのコードが悪さをしています。
なぜこのようなことが発生しているかというと、SwiftUI は値型駆動のフレームワークです。参照型と大きく違うのは、データの代入するたびに新しいインスタンスが生成されるので、同一オブジェクトの比較をインスタンス比較でできなくなるのです。そのため SwiftUI は Identifiable
に大きく依存しているわけです:同一オブジェクトかどうかは、Identifiable
の適合結果を見て判断するしかないのです!
このポイントを押さえておかないと、「あー Identifiable
の適合って面倒臭い…そうだ!Hashable
があってあれって自動適合じゃん?Identifiable
の ID
もそもそも Hashable
に適合してればいいから、この型自体を Hashable
に適合してしまえば、Identifiable
なんてすぐにできちゃっていちいち自分で var id: Xxx
とか作らなくて済むじゃん!」って発想に至るのもわかります。何せエンジニアはすぐ手を抜きたくなりますもんね。
しかし Identifiable
プロトコルには、ちゃんとした存在意義があるのです。それは前述通り SwiftUI にオブジェクトの同一判定をさせるために必要だからです。なので残念ながら、その同一判定のロジックは、他の誰でもない、仕様を知ってる我々エンジニアが責任を持って書かなければならないのです。だから Identifiable
に自動適合がないのです。
と言うわけで、今回は title
が同じものであれば同じ記事とみなす仕様にしたいので、早速 Post
のロジックを変えましょう:
-struct Post: Hashable, Identifiable {
+struct Post {
// ...
}
+
+extension Post: Identifiable {
+ var id: String { title }
+}
これでちゃんと想定通り、いいねボタンをタップしてもリスト画面に戻らない動きになります:
もちろん id
何を返すかは仕様と相談してください。今回は title
にしていますが、例えば Qiita の記事でしたら、公開後にタイトルを変えることもあるので title
を id
として使えないと思います。その場合によくあるやり方としては最初に作るときに絶対に変わらない let id = UUID()
を作ることです。他にも例えばチェーン店の店舗情報でしたら、管理情報として店舗コードもしくは管理番号などがあると思いますし、自動車の個体管理でしたら、必ず一意で変わらない車台番号1がありますので、それらを id
として使うのもアリです。大事なのは、エンジニアとして責任を持って、これが同じなら同じオブジェクトと見做すものを定めることです。
今回の話についてもっと詳しく知りた方は、ぜひこちらの Apple 公式のセッション動画を見てみてください:
https://developer.apple.com/videos/play/wwdc2021/10022/