【SwiftUI】非同期で画像を読み込む方法 - AsyncImageを用いて -

SwiftUIで、非同期で画像を読み込む際に、詰まったので記事として記しておく。
初学者記事のため、疑いながら読んでください。

SwiftUIで下画像のような画面を、APIを用いて表示する際に、
![スクリーンショット 2023-05-05 7.09.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799713/76b27937-a2f3-a902-de4a-0a3044bb77bf.png)
<details><summary>推奨しないサンプルコード</summary>

```swift: PokemonImageView.swift
import SwiftUI

struct PokemonImageView: View {
    let imageUrl: URL?
    
    var body: some View {
        if let imageUrl = imageUrl,
           let imageData = try? Data(contentsOf: imageUrl) {
            return Image(uiImage: UIImage(data: imageData)!)
                .resizable()
                .aspectRatio(contentMode: .fit)
        } else {
            return Image(systemName: "questionmark.circle")
                .resizable()
                .aspectRatio(contentMode: .fit)
        }
    }
}
```
</details>

`推奨しないサンプルコード`で、実装した。

シュミレーターで起動した際に、
![スクリーンショット 2023-05-05 7.08.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799713/db4ddec0-44b4-488b-ad0f-1b7b524291b3.png)

紫の警告が表示され、詳しく見てみると、

`Synchronous URL loading of 
https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png should not occur on this application's main thread as it may lead to UI unresponsiveness. Please switch to an asynchronous networking API such as URLSession.`

この警告は、メインスレッドで同期的にURLを読み込んでいることによって、ユーザーインターフェースが応答しなくなる可能性があるという意味です。丁寧に該当するコードの部分で、「非同期で行った方が良いよ」と注意してくれている。

対処法
-
非同期で行うためには、`AsyncImage`を用いることが必要です。

実装例

<details><summary> サンプルコード</summary>

```swift: PokemonImageView
struct PokemonImageView: View {
    let imageUrl: URL?
    
    var body: some View {
        if let imageUrl = imageUrl {
            AsyncImage(url: imageUrl) { phase in
                switch phase {
                case .empty:
                    Image(systemName: "questionmark.circle")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                case .success(let image):
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                case .failure:
                    Image(systemName: "xmark.circle")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                @unknown default:
                    fatalError()
                }
            }
        } else {
            Image(systemName: "questionmark.circle")
                .resizable()
                .aspectRatio(contentMode: .fit)
        }
    }
}
```

</details>


実装例の詳細
-
```
AsyncImage(url: URL(string: "https://example.com/icon.png"))
```
上記のコードを、`body`で実装するだけでも、使用することは可能である。だが、上記のURLで画像取得に失敗した場合に、下画像の赤枠のような表示になり、読み込んでいるのかどうなのかが不明になってしまう。
![スクリーンショット 2023-05-05 7.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799713/e4815145-11cd-2fd3-ebba-97f439f43aaa.png)

そのため、エラーに対して表示内容を変更する必要がある。そこで、`AsyncImagePhase`を用いる。
![スクリーンショット 2023-05-05 7.39.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799713/dd083bdd-569a-54aa-0b56-7943d6a8da3a.png)

`AsyncImagePahse`（列挙型）の詳細を見ていく。
![スクリーンショット 2023-05-05 7.44.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799713/57f05d6f-a87b-6371-2ca9-618deeaa7f14.png)
今回使用したのは、

`empty`　まだ画像を読み込んでいない
`success`　画像の読み込みが成功したとき、取得したImageを使用可能
`failure`　画像の読み込みに失敗したとき、取得したErrorを使用可能

である。

これらを用いて、条件に合ったUIを表示する。

以上です。

他にも良い方法があれば、コメントいただけると大変うれしいです。
良かったと思ったら、いいねやTwitterのフォローよろしくお願いいたします！

https://sites.google.com/view/muranakar
個人でアプリを作成しているので、良かったら覗いてみてください！

参考
-
https://www.yururiwork.net/archives/1821