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ではポストした動画がない場合は、「ぜひ動画をポストしてみましょう。」といるテキストを表示しています。
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 で使ってみると以下のような感じです。
#Preview {
ContentUnavailableView(
"データがありません",
systemImage: "plus"
)
}

上記の他にもイニシャライザは用意されており、どのイニシャライザで呼び出すかによって表示できる内容もやや変わるので紹介していきます。
ContentUnavailableView
のイニシャライザ紹介
タイトルの表示
#Preview {
ContentUnavailableView("データがありません", image: "")
}
image
を空文字で設定すれば画像が表示されないので、結果的にタイトルのみ表示する UI になります。
このイニシャライザでは、font
modifier でフォントサイズのカスタマイズは行えませんでした。
上記イニシャライザに限らず、画像指定部分のから文字にしてしまえば同じことができます。
Label
を指定するイニシャライザでは、フォントのサイズをカスタマイズすることも可能でした。
画像とタイトルの表示
#Preview {
ContentUnavailableView("データがありません", image: "sample")
}

タイトル部分は先ほどと同じだが、image
部分に Asset に入っている画像の名前を指定すると、画像付きの UI を表示できます。
制限として、試した限りだと、画像とタイトルの位置関係はいじれず、画像の下にタイトルが表示されるレイアウトしか表示できなさそうです。
大きな画像をimage
に指定すると、なぜか画像が表示されなかったです。
どれぐらい大きければ表示されなくなるかはわからないが、表示されない場合は画像のサイズを小さくすると表示されるかも。
SF Symbols の画像を表示したい場合は、以下イニシャライザを使うこともできます。
#Preview {
ContentUnavailableView("データがありません", systemImage: "plus")
}

画像とタイトルと Description の表示
#Preview {
ContentUnavailableView(
"データがありません",
systemImage: "plus",
description: Text("データを追加しましょう")
)
}

これまで紹介してきたイニシャライザには、description
の引数があり、これにText
で文言を指定することで、Description の表示も行えます。
もし、Description のサイズを変更したい場合は、以下イニシャライザを使用するとできます。
ボタン付き
#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))
}
}
}

action
の引数にButton
を指定することができます。
ここに指定するButton
は普段通りいろんな modifier を使えるので好きなボタンを実装できました。
このイニシャライザではaction
だけでなく、label
とdescription
でもButton
を使えるので、やろうと思えば、label
をボタンにして他はデフォルトにしてしまえば、以下のような UI も実装できます。
#Preview {
ContentUnavailableView {
Button {
print("tapped title")
} label: {
Label("データがありません", systemImage: "plus")
}
}
}

検索結果が見つからなかったケース専用のUI
#Preview {
ContentUnavailableView.search
}

検索結果が見つからなかった場合専用のUIは、静的に定義されていました。
一応、多言語対応もしていそうで、以下のようにlocaleをenvironmennt
で注入すると日本語にも変更できました。
#Preview {
ContentUnavailableView.search
.environment(\.locale, .init(identifier: "ja_JP"))
}

また、検索した文言を付与することも可能でした。
#Preview {
ContentUnavailableView.search(text: "sample")
}

ContentUnavailableView
のアクセシビリティ
Apple提供のUIコンポーネントなので、おそらくアクセシビリティ機能は標準で大体実装されているだろう思ったので、VoiceOverとDynamicTypeに対応しているかみてみました。
VoiceOverの対応状況
想定通り、VoiceOverで読み上げられました。
ただこれは自分の知識不足で、日本語の文言が読み上げられなくて少しハマりました。
以下記事にある通り、String Catalogsで日本語として文言を管理することで、正しく読み上げられるようになりました。
DynamicType
こちらも想定通り、DynamicFontに対応していました。
ただ、以下イニシャライザだと、FontをsystemFont
で設定できるので、systemFont
で固定のフォントによる設定を行うと、DynaimcTypeには対応できませんでした。
おわり
ContentUnavailableView
を使うことで、統一感のあるEmpty StateのUIを素早く実装できそうです。
また、Appleに提供されているUIコンポーネントを使うことのメリットとして、アクセシビリティの対応を開発者が意識せずとも実装されていたりするので、
業務でもデザイナーさんと相談しつつ積極的に取り入れるのはアリかなと思いました。