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

完全備忘録

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

```swift
extension Identifiable where Self: Hashable {
    var id: Self { self }
}
```
これは、`Identifiable`プロトコルに準拠する型が`Hashable`プロトコルを準拠する事によって適応されます。

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

```swift
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 をやめろ！](https://qiita.com/lovee/items/b669ad3b1717edc97393)

# なぜ使ってはいけないのか
先ほどの[記事](https://qiita.com/lovee/items/b669ad3b1717edc97393)を読んでみましたがまだまだSwiftUIの理解が乏しいため、あまり理解できませんでした...。

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

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

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

```swift
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`で参照型です。

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

```swift
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`は同一のオブジェクトを`インスタンスで識別することが可能`になります。
```swift
class HogeView {
    // 何らかの処理
}

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

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

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

```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/8963c447-4384-e779-9957-d81510dc7905.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/de4ae830-a1fd-4c37-f475-32f5446f6613.gif)

※ ログの見方は以下を参考にしました
[View の更新トリガーを調べるために _printChanges() を利用する。](https://github.com/YusukeHosonuma/Effective-SwiftUI/discussions/24)

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

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

## 不具合の原因
今回そもそも以下のコードが悪さをしていました。
```swift
extension Identifiable where Self: Hashable {
    var id: Self { self }
}
```
例えば、このextensionではViewに対して以下のようなIdが割り振られるんでしたね。
```
Article(title: "kkahaad", body: "adddahdldid", like: false)
```
そして、[記事](https://qiita.com/lovee/items/b669ad3b1717edc97393)では前画面でこのようなidをListの各要素に割り振って遷移先で状態を変更していました。
```swift
// 前画面
@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で遷移した先で値を更新すると、勝手に画面が戻る件](https://qiita.com/Howasuto/items/bdcb964d3610c5e566bc)

[記事](https://qiita.com/Howasuto/items/bdcb964d3610c5e566bc)を参考にすると`前画面が再描画された事により、NavigationLinkでDestination指定している遷移先の画面が破棄されるから`だそうですね。

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