[SwiftUI]AsyncImageを使用してURLから画像を表示する

## はじめに
iOS15ではAsync/Awaitが大きく注目されました。
その影で非同期処理で画像を読み込んで表示するAsyncImageも導入されました。

https://developer.apple.com/documentation/swiftui/asyncimage

AsyncImageはSwiftUIで使用することができます。
SwiftUIでImageを読み込んで表示させるには、多くの場合、自前でFunctionを作成していると思いますが、今回追加されたAsyncImageを使用すればシンプルにImageを取得できるようになっています。
また、取得時のアニメーションや、ロード中に表示するindicator、取得に失敗した時に表示するImageまで指定する事ができます。
しかし、残念なところもあります。現時点ではAsyncImageではキャッシュされません。起動するたびにImageを再度DownLoadしています。
その辺りも含めて、この記事で触れていきたいと思います。

## 環境
・ macOS Monterey 12.0
・ iOS 15.0
・ Xcode : 13.1

## 従来のImage Downloadの例
```swift
@State var url: String
@State var image: UIImage?
func downloadImageAsync(url: URL, completion: @escaping (UIImage?) -> Void) {
    var request = URLRequest(url: url)
    request.timeoutInterval = 3
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        if let error = error {
            completion(nil)
            print(error)
            return
        }
        guard let response = response as? HTTPURLResponse,
              (200...299).contains(response.statusCode) else {
                  completion(nil)
                  print ("server error")
                  return
              }
        if response.statusCode == 200 {
            image = UIImage(data: data!)
            DispatchQueue.main.async {
                completion(image)
            }
        }
    }
    task.resume()
}
var body: some View {
    VStack {
        Text("従来のImageDL")
        if let image = image {
            Image(uiImage: image)
                .resizable()
                .scaledToFit()
                .scaleEffect(0.9)
        }
    }
    .onAppear {
        url = "https://images.unsplash.com/photo-1635408485959-9f56670da427?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1171&q=80"
        if !url.isEmpty {
            downloadImageAsync(url: URL(string: url)!) { image in
                self.image = image
            }
        }
    }
}
```
|上記のCodeで取得した画面|
|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/052313ec-9282-b2a0-d210-c5cd9fdc0083.jpeg" width=50%>|


## AsyncImageの使用

```swift
let url = URL(string: "https://bit.ly/3nHS735")
var body: some View {
    VStack {
        Text("AsyncImageを使用したImageDL")
        AsyncImage(url: url)
    }
}
```

|PreViewを再生するまではImageは表示されない|
|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/6031a3b2-0975-c5e9-7ce4-9a55ad1eda80.jpeg" width=50%>|

|PreViewを再生して取得した画面|
|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/379545dc-6713-28aa-2e10-240cef0f690b.jpeg" width=50%>|

これだけです。ものすごくCodeがスッキリしました。
また画像の読み込み中に表示するindicatorやSizeの変更、エラー時の表示も指定できます。

```swift
VStack {
    AsyncImage(url: url) { phase in
        if let image = phase.image {
            image
                .resizable()
                .scaledToFit()
        } else if phase.error != nil {
            Text("no image")
        } else {
            ProgressView()
        }
    }
}
.scaleEffect(0.9)
```

上記ではif文で
・読み込んだ画像を表示
・error時は"no image"を表示
・ProgressViewの表示(ここでは読み込んだ画像を表示させるまでに表示させるものを指します)
を上から順に行っています。

またswitch文で書く事もできます。

```swift
AsyncImage(url: url) { phase in
    switch phase {
    case .success(let image):
        image
            .resizable()
            .aspectRatio(contentMode: .fit)
    case .failure(let error):
        Text(error.localizedDescription) 
    case .empty:
        ProgressView()
    @unknown default:
        EmptyView()
    }
}
```
1つ目は、画像の取得に成功したら、取得した画像を表示させています。
caseに関連付けられている値はImageであり、必要なImageのmodifierを使用する事ができます。

2つ目は、画像の取得に失敗した時、errorの説明をlocalizedDescriptionを使用してTextで表示しています。

https://developer.apple.com/documentation/foundation/nserror/1414418-localizeddescription

3つ目は、読み込み中で、Imageを取得するまでに表示するものを指定できます。
先ほど紹介したif文でのProgressViewの部分と同じイメージを持ってもらえると分かりやすいかと思います。
ここではindicatorを表示させる為にProgressViewを使用しています。

最後に@unknown defaultですが、こちらは将来上記以外のcaseが追加される可能性があることを示しています。
今回はEmptyViewを追加しておきましたがここは必要に応じて書き換えてください。

### アニメーションの追加
```swift
AsyncImage(url: url, transaction: Transaction(animation: .easeInOut(duration: 0.6)))
```
urlの後にtransactionを追加する事でImageを表示させる際にアニメーションを追加する事ができます。

|transactionを追加して取得したImageを表示|
|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/8e135aa4-d115-d0f6-567e-a3069bf1f34f.gif" width=50%>|

上記のgifを見ていただくと分かるように、毎回ProgressViewから始まり、Imageを取得している事がわかります。つまりキャッシュはされていません。
また、timeoutIntervalのようなtimeoutできる機能もありません。
今のところキャッシュ機能付きのAsyncImageはないのでGitHubに公開されているライブラリを使用するしかなさそうですね。

https://github.com/lorenzofiamingo/SwiftUI-CachedAsyncImage

## Async/Awaitを使ってimageを取得してみる
ここからはおまけ的な要素ですがAsync/Awaitを使ってimageを取得してみます。

```swift
struct FetchImage: View {
    @State private var image: Image?
    var url: URL
    var body: some View {
        ZStack {
            if let fetchImage = image {
                fetchImage
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                ProgressView()
            }
        }.task(id: url) {
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                if let fetchImage = UIImage(data: data) {
                    self.image = Image(uiImage: fetchImage)
                }
            } catch {
                self.image = Image(systemName: "xmark")
            }
        }
    }
}
```
imageが取得されれば、取得したimageを表示します。それまではProgressViewを表示させます。取得に失敗した場合はxmarkを表示します。
.task(id: url)により、ビューが表示されたとき、または指定された値が変更されたときに実行するタスクを追加するので、urlを変更するたびにimageを変更してくれます。

## まとめ
ここまでCodeがシンプルに書けるのは純粋に感動しました!!
さらにエラーやロード画面が標準で設定できるのもすごくシンプルで書きやすいし、読みやすいです!!
またAsync/Awaitもすごく書きやすくCodeがシンプルに表現できる点が良かったです!!taskとの組み合わせも個人的には好きでした!!
とは言えiOS15〜という事で実際に使用するのはまだ先になりそうですねー。。。

## おまけ
iOS15〜と言わずに、もっと幅広く使用できるようにSPMでimageを取得する簡単なライブラリ作ってみました(^^)
是非試して頂き、良ければ☆頂けると泣いて喜びます★

https://github.com/yuujioka/ImageDownloader

使い方など〜

https://qiita.com/yuujioka/items/651cfa6813e69eee00b5

|公開の様子|
|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517666/7e9d663c-fb35-b604-aca8-47a0705b6ec5.jpeg" width=100%>|


#### 参考記事

https://zenn.dev/yorifuji/articles/swiftui-download-image-async

https://qiita.com/From_F/items/e3eb8bd279f75b864865

https://serialcoder.dev/text-tutorials/swiftui/asyncimage-in-swiftui/

https://betterprogramming.pub/understanding-asyncimage-in-swiftui-55dd2fb6a86e

https://qiita.com/tamappe/items/64fbe4d0991cd222540e

https://qiita.com/Hackenbacker/items/8bfa99afa2d850e44531