【SwiftUI】AsyncImageのスタイルをViewに応じて綺麗に可変させる

SwiftUIで`一覧画面`と`詳細画面`があり
`一覧`・`詳細`それぞれ外部APIから画像を取得して、表示するアプリはよくありますよね。
そんなときまず検討するのが **`AsyncImage`** だと思います。

https://developer.apple.com/documentation/swiftui/asyncimage
>このビューは、共有URLSessionインスタンスを使用して、指定された URL から画像を読み込み、表示します。

とあるように、urlを渡すと画像取得から表示まで担ってくれる便利なやつ。

この`AsyncImage`を使って画像のViewを作成して、一覧からも詳細からも
そのViewを呼び出して、且つStyleを一覧・詳細それぞれに合わせたものに可変させたいとき
どうすれば実現でき、且つ美しく書けるか考えた時の実装を共有します。

# 今日のゴール

**🐶犬の種類をリストで表示し、詳細画面に遷移できる🐶**

- `AsyncImage`を使って画像表示用のViewを実装する
- 一覧画面、詳細画面どちらからもそのViewを呼び出して、且つStyleを可変させる
- 綺麗に書く

![doglist.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2633746/69b0dc7a-5de1-6265-e816-ae7302d9e1fe.gif)

# コンポーネントの説明
こんな感じで作っていきます。

:::note warn
今回はView周りの説明になるので、ViewModelの実装は省いています
:::
![スクリーンショット 2023-05-02 19.27.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2633746/ba976517-619a-c70f-ee63-fdac80e55328.png)

## DogListView（一覧画面）を作る

`NavigationView`を使って実装する
一覧画面のRowをまとめた`DogRowView`と、遷移先の詳細画面`DogDetailView`に
`ForEach`で取得できる`name`を渡しています。
``` swift
import SwiftUI

struct DogListView: View {
    let dogs = ["Golden Retriever", "Bulldog", "Shiba", "Tosa", "Akita"]
    
    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(dogs, id: \.description) { name in
                        NavigationLink {
                            DogDetailView(name: name)
                        } label: {
                            DogRowView(name: name)
                        }
                    }
                    
                }
            }
            .navigationTitle("Dogs")
        }
    }
}
```

## DogRowView、DogDetailViewを実装する

それぞれ描画する要素を実装します


``` swift
// DogRowView.swift

struct DogRowView: View {
    let name: String
    
    init(name: String) {
        self.name = name
    }
    
    var body: some View {
        HStack {
            ThumbnailView(screen: .overview)
            
            Text("🐶 \(name)")
        }
    }
}
```

``` swift
// DogDetailView.swift

struct DogDetailView: View {
    let name: String
    
    init(name: String) {
        self.name = name
    }
    
    var body: some View {
        VStack {
            ThumbnailView(screen: .detail)
            
            Text("Hi, I'm \(name) !!")
        }
        .navigationTitle(name)
    }
}
```

`ThumbnailView`に渡す引数`screen`は、画面を可変させるために定義するEnumです。
この後`Image`や`View`から参照するので、グローバルに別ファイルで作成しておきます。

``` swift
// Screen.swift

enum Screen {
    case overview
    case detail
}
```

## Imageをextensionする（肝）

ここが肝です。例によってImageをextensionします。

``` swift
// Image+thumbnailStyle.swift

extension Image {
    func thumbnailStyle(to screen: Screen) -> some View {
        VStack {
            switch screen {
                case .overview:
                    self
                        .resizable()
                        .frame(width: 80, height: 80)
                        .padding(8)
                case .detail:
                    self
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(height: 250)
                }
        }
    }
}
```
`AsyncImage`でfetchしたImageに対してstyleを適用する
**`thumbnailStyle()`** を実装しています。

`switch`文で、与えられた引数（screen）に応じて
`overview`or`detail` それぞれの場合のStyleを可変し、
`self`に対してチェーンさせて適用し、返します

## ProgressViewをextensionする

Imageのextension同様に、
`AsyncImage`がFetchを行なっている間に表示する`ProgressView`も
ついでにextensionしちゃいましょう

``` swift
// ProgressView+style.swift

extension ProgressView {
    func style(to screen: Screen) -> some View {
        VStack {
            switch screen {
                case .overview:
                    self
                        .frame(width: 80, height: 80)
                        .padding(8)
                case .detail:
                    self
                        .frame(height: 172)
                }
        }
    }
}
```
これで、Styleを可変させるためのExtensionができました
次にこれを使っていよいよ`AsyncImage`を含むViewを実装しましょう


## ThumbnailViewを実装する


``` swift
// ThumbnailView.swift

struct ThumbnailView: View {
    var screen: Screen = .overview
    let url = URL(string: "https://images.dog.ceo/breeds/havanese/00100trPORTRAIT_00100_BURST20191030212452971_COVER.jpg")

    init(screen: Screen) {
        self.screen = screen
    }
    
    var body: some View {
        AsyncImage(url: url) { phase in
            if let image = phase.image {
                image
                    .thumbnailStyle(to: screen)
            } else if phase.error != nil || url == nil {
                Image("no-image")
                    .noImageStyle(to: screen)
            } else {
                ProgressView()
                    .style(to: screen)
            }
        }
    }
}
```
:::note warn
*今回は説明の都合上、URL決め打ちです*
:::

上で実装してきたextensionを使っています。

`if let image = phase.image`→ fetch成功した場合の処理
`else if phase.error != nil || url == nil`→fetch失敗した場合の処理
`else` → fetch中の処理
エラーハンドリングをしつつ、それぞれの場合に応じて描画するViewを切り替えています。

`image.thumbnailStyle(to: screen)`のように
`Image`に対して先ほどのextensionをチェーンさせて使い、
スクリーンに応じてStyleを可変させることができました。

これで、Imageのコンポーネントを共通化することができ
`DetailThumbnail`とか`OverviewThumbnail`とか
Styleだけが違う似たようなコンポーネントが存在することを防ぐことができます

## 終わりに

今回はextensionを用いて実装しましたが、
`@State`を使って引数なしでこんな感じでできないかとチャレンジしましたが
なんか上手くいきませんでした。

```swift
@State var screen: Screen = .overview

ThumbnailView()
    .overviewStyle()
```

もう少し勉強してみてもっといい方法があったらまた記事投稿します！