LoginSignup
4
4

More than 1 year has passed since last update.

SwiftUIではidをちゃんと指定しないといけない事が分かった

Last updated at Posted at 2023-05-03

完全備忘録

最近、以下のextensionに出会いました。

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

これは、Identifiableプロトコルに準拠する型がHashableプロトコルを準拠する事によって適応されます。

そして、idは型がSelf(準拠する型)となって、以下のように返します。

struct Article: Identifiable, Hashable {
    var title: String
}

let article = Article(title: "aaa")
print(article.id) // Article(title: "aaa")

このextensionによってIdentifiableプロトコル、Hashableプロトコルどちらの要求も満たす事ができます。

しかも、idを自分で指定しなくて良いから最初は便利だな〜と思って使おうとしていましたが以下の記事では使うことを推奨していませんでした。
今すぐ extension Identifiable where Self: Hashable をやめろ!

なぜ使ってはいけないのか

先ほどの記事を読んでみましたがまだまだSwiftUIの理解が乏しいため、あまり理解できませんでした...。

ただ、実際にコードを書きながら都度、調べながら何となく理解できました(認識が間違っているかもしれないので指摘して頂けるとありがたいです)。

前提

まず、前提としてSwiftUIのViewオブジェクトはStructで値型です。

なのでインスタンスを生成時には毎回、新しく生成されます。

struct Hoge {
    var name: String
}

// hoge1, hoge2変数に対して、
//インスタンスのバイト列が直接、コピーされている

// つまり、変数に代入するたびに新しく生成される
var hoge1 = Hoge(name: "aaa")
var hoge2 = hoge1

hoge2.name = "bbbb"

print(hoge1.name) // aaa
print(hoge2.name) // bbb

そして、SwiftUIが登場する以前のUIKitではViewオブジェクトはclassで参照型です。

参照型ではインスタンスを生成した際の挙動は値型と違い、インスタンス生成時はそのインスタンスを格納しているメモリアドレスが格納されます。

class Hoge {
    var name: String = "aaa"
}

// hoge1, hoge2変数に対して、インスタンスが格納されたメモリアドレスが代入される
var hoge1 = Hoge()
var hoge2 = hoge1

hoge2.name = "bbbb"

print(hoge1.name) // bbb
print(hoge2.name) // bbb

そして、この割り振られたメモリアドレスは一意なので重複しません。

したがって、classは同一のオブジェクトをインスタンスで識別することが可能になります。

class HogeView {
    // 何らかの処理
}

// view1, view2も同じメモリアドレスを参照しているのでHogeViewかどうか識別が可能
let view1 = HogeView()
let view2 = view1

ではSwiftUIではどうでしょうか?

SwiftUIは冒頭でも書きましたが、Viewオブジェクトはstructで値型です。

値型は参照型と違って、インスタンス単位でそのViewオブジェクトが同一かどうかが分かりません、それはインスタンスを生成するたびに、毎回新しく生成されるからです。

struct HogeView {
    // 何らかの処理
}

// view1, view2も同じHogeViewだけど、コピーなので別物扱い
let view1 = HogeView()
let view2 = view1

これが記事内でも書いてた「データの代入するたびに新しいインスタンスが生成されるので、同一オブジェクトの比較をインスタンス比較でできなくなる」ですかね。

なので、SwiftUIではViewオブジェクトを識別するためには明示的にidを割り振る必要がありそうです。

検証

まずは、上記の記事のように2つの画面とArticleを用意しました(ソースコードはほぼ同じなので割愛)。

そして、Viewの再描画を監視するために_printChanges()を仕込んで問題箇所の修正前・修正後のアプリを実際に動かして比較していきたいと思います。

以下が実際の挙動と出力されたログです。

  • 修正前
ArticleListView: @self, @identity, _articles changed.
ArticleDetailView: @self, @identity, _article changed.
ArticleListView: _articles changed.

Simulator Screen Recording - iPhone 14 Pro - 2023-05-02 at 15.36.54.gif

  • 修正後
ArticleListView: @self, @identity, _articles changed.
ArticleDetailView: @self, @identity, _article changed.
ArticleListView: _articles changed.
ArticleDetailView: @self, _article changed.

Simulator Screen Recording - iPhone 14 Pro - 2023-05-03 at 12.19.42.gif

※ ログの見方は以下を参考にしました
View の更新トリガーを調べるために _printChanges() を利用する。

このログから分かるように、修正前では前画面のArticleListViewしか再描画されていません

修正後では、遷移先も再描画されている事が分かりますね。

不具合の原因

今回そもそも以下のコードが悪さをしていました。

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

例えば、このextensionではViewに対して以下のようなIdが割り振られるんでしたね。

Article(title: "kkahaad", body: "adddahdldid", like: false)

そして、記事では前画面でこのようなidをListの各要素に割り振って遷移先で状態を変更していました。

// 前画面
@State private var articles: [Article] = // 何らかの値

// 何らかの処理

NavigationView(content: {
    List {
        ForEach($articles) { article in
            NavigationLink {
                ArticleDetailView(article: article)
            } label: {
                //...
            }
        }
    }
})

// 遷移先
@Binding var article: Article

// 何らかの処理

Button {
    article.like.toggle()
} label: {
    Image(systemName: article.like ? "heart.fill" : "heart")
}

したがって遷移先で状態を変更した事により、以下のようにidが変わってしまってSwiftUIがどのViewを再描画して良いかが分からなくなってしまい、前画面のViewだけが再描画され、遷移先のViewが再描画されなかったという感じだと思われます。

Article(title: "kkahaad", body: "adddahdldid", like: false)
↓
Article(title: "kkahaad", body: "adddahdldid", like: true) // likeが変わった

画面遷移が起こった理由としては、調べてみると以下の記事が見つかりました。

【SwiftUI】Navigationlinkで遷移した先で値を更新すると、勝手に画面が戻る件

記事を参考にすると前画面が再描画された事により、NavigationLinkでDestination指定している遷移先の画面が破棄されるからだそうですね。

まとめ

今考えると、状態は常に変化するのでView自身をidとして指定するのはかなり危ないですね。

4
4
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
4
4