ContentUnavailableView使ってみる

## `ContentUnavailableView`とは

データがない状態を表す UI を提供してくれる APIです。
iOS17から使用できます。

公式のドキュメントでは以下の用意説明されています。

> An interface, consisting of a label and additional content, that you display when the content of your app is unavailable to users.

日本語訳

> アプリのコンテンツがユーザーに提供できない場合に表示される、ラベルと追加コンテンツで構成されるインターフェース。

例えば、何かしらのデータをリスト形式で表示する UI を想定したときに、データが何もない場合、「データを追加してください」や「データがありません」といったメッセージを表示するケースがあると思います。

例として、Xではポストした動画がない場合は、「ぜひ動画をポストしてみましょう。」といるテキストを表示しています。
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/4f924af9-0562-4500-ac4c-9fcc48d0a283.png"/>

:::note info
Apple の Human Interface Guidline でもデータがない場合には、次に何をすべきかを表示しましょうと言及されています。

> Provide clear next steps on any blank screens. An empty state, like a completed to-do list or bookmarks folder with nothing in it, can provide a good opportunity to make people feel welcome and educate them about your app. Empty states can also showcase your app’s voice, but make sure that the content is useful and fits the context. An empty screen can be daunting if it isn’t obvious what to do next, so guide people on actions they can take, and give them a button or link to do so if possible. Remember that empty states are usually temporary, so don’t show crucial information that could then disappear.

https://developer.apple.com/design/human-interface-guidelines/writing
:::

## `ContentUnavailableView`を使ってみる

`ContentUnavailableView`を使う方法は超単純で、`View`として`ContentUnavailableView`をインスタンス化するだけです。
普段使う、`Text`や`Button`と同じような感覚で使えます。

例として Preview で使ってみると以下のような感じです。

```swift
#Preview {
    ContentUnavailableView(
        "データがありません",
         systemImage: "plus"
    )
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/fbd20904-bbc5-4c48-bc5a-48a462c87216.png"/>

上記の他にもイニシャライザは用意されており、どのイニシャライザで呼び出すかによって表示できる内容もやや変わるので紹介していきます。

## `ContentUnavailableView`のイニシャライザ紹介

### タイトルの表示

```swift
#Preview {
    ContentUnavailableView("データがありません", image: "")
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/04c53f21-387c-4652-a0e4-3a2ef03c83b6.png)

`image`を空文字で設定すれば画像が表示されないので、結果的にタイトルのみ表示する UI になります。
このイニシャライザでは、`font`modifier でフォントサイズのカスタマイズは行えませんでした。

https://developer.apple.com/documentation/swiftui/contentunavailableview/init(_:image:description:)

上記イニシャライザに限らず、画像指定部分のから文字にしてしまえば同じことができます。

`Label`を指定するイニシャライザでは、フォントのサイズをカスタマイズすることも可能でした。

https://developer.apple.com/documentation/swiftui/contentunavailableview/init(label:description:actions:)

### 画像とタイトルの表示

```swift
#Preview {
    ContentUnavailableView("データがありません", image: "sample")
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/9b2cd2f0-b784-4621-b8fd-7db62c9150e8.png"/>

タイトル部分は先ほどと同じだが、`image`部分に Asset に入っている画像の名前を指定すると、画像付きの UI を表示できます。

制限として、試した限りだと、画像とタイトルの位置関係はいじれず、画像の下にタイトルが表示されるレイアウトしか表示できなさそうです。

https://developer.apple.com/documentation/swiftui/contentunavailableview/init(_:image:description:)

::: note warn
大きな画像を`image`に指定すると、なぜか画像が表示されなかったです。
どれぐらい大きければ表示されなくなるかはわからないが、表示されない場合は画像のサイズを小さくすると表示されるかも。
:::

SF Symbols の画像を表示したい場合は、以下イニシャライザを使うこともできます。

```swift
#Preview {
    ContentUnavailableView("データがありません", systemImage: "plus")
}
```
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/81da6a86-1d1c-4779-a77f-c74754aebe09.png"/>

https://developer.apple.com/documentation/swiftui/contentunavailableview/init(_:systemimage:description:)

### 画像とタイトルと Description の表示

```swift
#Preview {
    ContentUnavailableView(
        "データがありません",
        systemImage: "plus",
        description: Text("データを追加しましょう")
    )
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/7010cccc-e9d4-4f7d-9a40-8d98d405a7fa.png"/>

これまで紹介してきたイニシャライザには、`description`の引数があり、これに`Text`で文言を指定することで、Description の表示も行えます。

もし、Description のサイズを変更したい場合は、以下イニシャライザを使用するとできます。

https://developer.apple.com/documentation/swiftui/contentunavailableview/init(label:description:actions:)

### ボタン付き

```swift
#Preview {
    ContentUnavailableView {
        Label("データがありません", systemImage: "plus")
    } description: {
        Text("データを追加しましょう")
    } actions: {
        Button {
            print("tapped button")
        } label: {
            HStack(spacing: 4) {
                Image(systemName: "plus")
                Text("action")
                    .font(.title3)
            }
            .padding(8)
            .background(.cyan)
            .foregroundStyle(.white)
            .clipShape(RoundedRectangle(cornerRadius: 8))
        }
    }
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/bec43042-d7d8-4dd8-9278-8bbcd3a89e35.png"/>

`action`の引数に`Button`を指定することができます。
ここに指定する`Button`は普段通りいろんな modifier を使えるので好きなボタンを実装できました。

https://developer.apple.com/documentation/swiftui/contentunavailableview/init(label:description:actions:)

このイニシャライザでは`action`だけでなく、`label`と`description`でも`Button`を使えるので、やろうと思えば、`label`をボタンにして他はデフォルトにしてしまえば、以下のような UI も実装できます。

```swift
#Preview {
    ContentUnavailableView {
        Button {
            print("tapped title")
        } label: {
            Label("データがありません", systemImage: "plus")
        }
    }
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/679b13eb-0d9e-426e-805b-34ea7d376970.png"/>

### 検索結果が見つからなかったケース専用のUI

```swift
#Preview {
    ContentUnavailableView.search
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/8ce898d8-dd13-473f-8743-578f97553e49.png"/>

https://developer.apple.com/documentation/swiftui/contentunavailableview/search

検索結果が見つからなかった場合専用のUIは、静的に定義されていました。

一応、多言語対応もしていそうで、以下のようにlocaleを`environmennt`で注入すると日本語にも変更できました。

```swift
#Preview {
    ContentUnavailableView.search
        .environment(\.locale, .init(identifier: "ja_JP"))
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/e41cb0cc-97cb-45ed-ad19-e6b68e642041.png"/>

また、検索した文言を付与することも可能でした。

```swift
#Preview {
    ContentUnavailableView.search(text: "sample")
}
```

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/8b68022a-3095-4e20-bf43-60dff0200ab2.png"/>

https://developer.apple.com/documentation/swiftui/contentunavailableview/search(text:)

## `ContentUnavailableView`のアクセシビリティ

Apple提供のUIコンポーネントなので、おそらくアクセシビリティ機能は標準で大体実装されているだろう思ったので、VoiceOverとDynamicTypeに対応しているかみてみました。

### VoiceOverの対応状況

想定通り、VoiceOverで読み上げられました。
ただこれは自分の知識不足で、日本語の文言が読み上げられなくて少しハマりました。

以下記事にある通り、String Catalogsで日本語として文言を管理することで、正しく読み上げられるようになりました。

https://zenn.dev/kokoaola/articles/7caaa557bf69de

### DynamicType

こちらも想定通り、DynamicFontに対応していました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/9b636be6-956b-4ce4-bf2a-a4f9bb9baead.png)

ただ、以下イニシャライザだと、Fontを`systemFont`で設定できるので、`systemFont`で固定のフォントによる設定を行うと、DynaimcTypeには対応できませんでした。

## おわり

`ContentUnavailableView`を使うことで、統一感のあるEmpty StateのUIを素早く実装できそうです。
また、Appleに提供されているUIコンポーネントを使うことのメリットとして、アクセシビリティの対応を開発者が意識せずとも実装されていたりするので、
業務でもデザイナーさんと相談しつつ積極的に取り入れるのはアリかなと思いました。