LoginSignup
33
12

More than 1 year has passed since last update.

今すぐ extension Identifiable where Self: Hashable をやめろ!

Last updated at Posted at 2022-09-06

TL;DR:

正確にはこんなコードをやめましょうね:

extension Identifiable where Self: Hashable {
    var id: Self { self }
}

まずこのようなコードを見てみましょう:

Identifiable
extension Identifiable where Self: Hashable {
    var id: Self { self }
}
Post.swift
struct Post: Hashable, Identifiable {
    let title: String
    let body: String
    var like: Bool
}
View.swift
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: {
                        // ...
                    }
                }
            }
        }
    }
}

ふむふむ、我ながらなかなかいいコード書いたじゃないか。早速動作確認してみよう:

output.gif

!???いいね!ボタン押すたびに、なぜかリストに戻ってしまうんですが!???

そう、元凶はまさにタイトルに書いた通り、extension Identifiable where Self: Hashable のところのコードが悪さをしています。

なぜこのようなことが発生しているかというと、SwiftUI は値型駆動のフレームワークです。参照型と大きく違うのは、データの代入するたびに新しいインスタンスが生成されるので、同一オブジェクトの比較をインスタンス比較でできなくなるのです。そのため SwiftUI は Identifiable に大きく依存しているわけです:同一オブジェクトかどうかは、Identifiable の適合結果を見て判断するしかないのです!

このポイントを押さえておかないと、「あー Identifiable の適合って面倒臭い…そうだ!Hashable があってあれって自動適合じゃん?IdentifiableID もそもそも Hashable に適合してればいいから、この型自体を Hashable に適合してしまえば、Identifiable なんてすぐにできちゃっていちいち自分で var id: Xxx とか作らなくて済むじゃん!」って発想に至るのもわかります。何せエンジニアはすぐ手を抜きたくなりますもんね。

しかし Identifiable プロトコルには、ちゃんとした存在意義があるのです。それは前述通り SwiftUI にオブジェクトの同一判定をさせるために必要だからです。なので残念ながら、その同一判定のロジックは、他の誰でもない、仕様を知ってる我々エンジニアが責任を持って書かなければならないのです。だから Identifiable に自動適合がないのです。

と言うわけで、今回は title が同じものであれば同じ記事とみなす仕様にしたいので、早速 Post のロジックを変えましょう:

Post.swift
-struct Post: Hashable, Identifiable {
+struct Post {
    // ...
}
+
+extension Post: Identifiable {
+    var id: String { title }
+}

これでちゃんと想定通り、いいねボタンをタップしてもリスト画面に戻らない動きになります:

output.gif

もちろん id 何を返すかは仕様と相談してください。今回は title にしていますが、例えば Qiita の記事でしたら、公開後にタイトルを変えることもあるので titleid として使えないと思います。その場合によくあるやり方としては最初に作るときに絶対に変わらない let id = UUID() を作ることです。他にも例えばチェーン店の店舗情報でしたら、管理情報として店舗コードもしくは管理番号などがあると思いますし、自動車の個体管理でしたら、必ず一意で変わらない車台番号1がありますので、それらを id として使うのもアリです。大事なのは、エンジニアとして責任を持って、これが同じなら同じオブジェクトと見做すものを定めることです。

今回の話についてもっと詳しく知りた方は、ぜひこちらの Apple 公式のセッション動画を見てみてください:
https://developer.apple.com/videos/play/wwdc2021/10022/

  1. 気をつけてほしいのは、車台番号は車のナンバープレートの、いわゆる登録番号ではありません。車台番号は車が製造される段階で打刻される一意の番号で、(オーナーが違法改造などしない限り)廃車されるまで変わることはありません;逆にナンバープレートの登録番号は車が陸運局で正式登録されるまで交付されませんし、オーナーの引っ越しなどの都合で変わることもあります。また豆知識ですが、車台番号はあくまで日本国内での流通で使われるものであり、海外では VIN コードの使用が主流です。

33
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
12