SwiftUIでリモート画像を非同期に表示したいときに便利なのが AsyncImage です。
AsyncImageは、URLから画像をフェッチして表示するまでの流れ(読み込み中・成功・失敗)を、宣言的に簡潔なコードで表現できます。
この記事では AsyncImage の基本的な意味や使い方、主要な引数の意味、活用シーン、注意点までをわかりやすく丁寧に解説します。
AsyncImage とは?
AsyncImage は、指定した URL から画像を非同期で取得して SwiftUI のビュー階層に表示するためのコンポーネントです。
内部では非同期ダウンロードを行い、その進行状況に応じて表示内容を切り替えます。
表示状態は AsyncImagePhase(.empty/.success(Image)/.failure(Error))で表され、これを使って
- 「読み込み中はローディング表示」
- 「失敗時は代替表示」
- 「成功時は画像表示」
と柔軟に分岐できます。
UIKitで同等のことをするには URLSession と UIImageView、キャッシュや状態管理を自前で組み合わせる必要がありましたが、SwiftUIの AsyncImage なら数行で同じ体験を実現できます。
具体例:最小構成からフェーズ管理、アニメーションまで
まずは最小の例です。
これだけで指定URLの画像を表示します(成功時は自動で画像が表示され、読み込み中や失敗時はシステム既定の挙動)。
1 2 3 4 5 6 7 8 9 10 |
import SwiftUI struct ContentView: View { var body: some View { AsyncImage(url: URL(string: "https://example.com/image.jpg")) .frame(width: 200, height: 200) .clipped() } } |
次に、読み込み中プレースホルダーと失敗時ビューを自分で定義する、フェーズベースの使い方です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import SwiftUI struct ContentView: View { let url = URL(string: "https://example.com/photo.jpg") var body: some View { AsyncImage(url: url) { phase in switch phase { case .empty: ProgressView() // 読み込み中 .frame(width: 200, height: 200) case .success(let image): image .resizable() .scaledToFill() .frame(width: 200, height: 200) .clipped() case .failure: VStack { Image(systemName: "photo") .font(.largeTitle) Text("画像を読み込めませんでした") .font(.caption) } .foregroundColor(.secondary) .frame(width: 200, height: 200) .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) } } } } |
フェードインなどのアニメーションを加える場合は、Transaction を渡します。
フェーズが変わるタイミングにアニメーションが適用されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import SwiftUI struct ContentView: View { let url = URL(string: "https://example.com/banner.png") var body: some View { AsyncImage( url: url, transaction: Transaction(animation: .easeInOut(duration: 0.25)) ) { phase in switch phase { case .empty: Color.gray.opacity(0.1) case .success(let image): image .resizable() .scaledToFit() .transition(.opacity) // フェードイン case .failure: Color.red.opacity(0.1) } } .frame(height: 180) .padding() } } |
主要な引数とその意味
AsyncImage は複数のイニシャライザがありますが、実務ではフェーズを扱えるイニシャライザが最も柔軟です。
主な引数は次の通りです。
引数名(代表的なイニシャライザ) | 型 | 説明 |
---|---|---|
url | URL? | 取得元の画像URL。nil の場合は何もフェッチしない(.empty のまま)。 |
transaction | Transaction | フェーズ遷移時に適用するアニメーション。省略時はアニメーションなし。 |
content(phase:) | (AsyncImagePhase) -> some View | 読み込み状態に応じて表示を切り替えるためのクロージャ。.empty / .success(Image) / .failure(Error) を受け取る。 |
scale(別イニシャライザ) | CGFloat | 画像のスケール係数(@2x/@3x 相当の解像度解釈)。省略可。 |
content(image:)/ placeholder(別イニシャライザ) | (Image) -> Content / () -> Placeholder | 成功時Imageとプレースホルダーを個別に受け取る古典的なスタイル。エラー時の分岐がしづらいため、フェーズ型推奨。 |
使い方
典型的な使い方は「フェーズベースのイニシャライザ」という書き方を使う方法です。
これは「読み込み中」「成功」「失敗」の3つの状態ごとに、見た目を切り替えられる便利な書き方です。
特に大事なのは 表示する大きさを常に同じにしておくこと です。
もし読み込み中は小さくて、成功時の画像は大きい…というようにサイズが違うと、画面がガタガタ動いて不自然になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
AsyncImage(url: URL(string: "https://example.com/avatar.png")) { phase in switch phase { case .empty: ZStack { RoundedRectangle(cornerRadius: 12).fill(.thinMaterial) ProgressView() } case .success(let image): image .resizable() .scaledToFill() .clipShape(RoundedRectangle(cornerRadius: 12)) case .failure: ZStack { RoundedRectangle(cornerRadius: 12).fill(Color.secondary.opacity(0.1)) Image(systemName: "person.crop.circle.badge.exclam") .font(.largeTitle) .foregroundColor(.secondary) } } } .frame(width: 120, height: 120) |
活用シーン
AsyncImage が役立つのは、実際のアプリでリモート画像を使いたいときです。
例えば SNS やニュースアプリのように、たくさんの画像をネット経由で読み込むケースでは欠かせません。
具体的にはこんな場面で使えます:
- ニュースフィードやSNSタイムラインなど、ネットワーク越しに多数のサムネイルを並べるUI
- プロフィール画像やリスト行のアイコンをリモートから取得して表示
- ギャラリー/スライダー/カルーセルの画像を段階的に読み込み、読み込み中は骨組み(シマー)やプレースホルダーを表示
- 詳細画面でのメインビジュアルをフェードイン表示して体感パフォーマンスを向上
- 接続不良や404などの失敗時に、明確な代替表示や再試行ボタンを組み合わせる
注意点
とても便利な AsyncImage ですが、使うときにいくつか気をつけたいポイントがあります。
これを知らないと「思ったように動かない」「画面が崩れる」といったトラブルになりがちです。
チェックしておきたい注意点は次の通りです:
- 要求のカスタマイズは不可:AsyncImage は URL 単体指定です。HTTPヘッダー付与や POST、認証トークンが必要なダウンロードは直接対応しません(URLSession で自前実装が必要)。
- キャッシュ制御は限定的:システムの URL キャッシュに従います。独自のディスクキャッシュや細かな無効化ポリシーが必要ならサードパーティや自前ローダーを検討。
- サイズ指定の必須性:Image はデフォルトでオリジナルサイズです。
.resizable()
と.scaledToFit()
/.scaledToFill()
、.frame(...)
を組み合わせてレイアウトを固定しましょう。 - プレースホルダーのレイアウトを揃える:読み込み中・失敗時にも同じ枠サイズを確保し、レイアウトジャンプ(画面のレイアウト(配置)が急にガタッと動いてしまう現象のこと)を避けると見栄えが安定します。
- 重い画像のコスト:非常に大きい画像はデコード・描画コストが高くスクロール性能に影響します。サーバー側でのリサイズやサムネイル提供を推奨。
- 再試行やキャンセル:標準の AsyncImage には再試行ボタンやキャンセルAPIはありません。必要に応じて URL を変える/明示的にビューを再生成するなどの工夫が必要。
- 対応OS:iOS 15+ / iPadOS 15+ / macOS 12+ 以降が目安。プロジェクトのデプロイターゲットに注意。
まとめると、「基本的な使い方はすごく簡単だけど、細かい制御や重い処理には向かない」ので、その場合は URLSession などと組み合わせるのがおすすめです。
まとめ
今回は SwiftUI の AsyncImage について詳しく紹介しました。
- AsyncImage は、URL からの画像取得を非同期・宣言的に扱えるコンポーネント
- フェーズ(AsyncImagePhase)で読み込み中/成功/失敗を安全に分岐できる
- Transaction と組み合わせてフェードインなどのアニメーションも簡単
- 実運用ではサイズ指定・プレースホルダー整合・キャッシュ戦略を意識する
- ヘッダー付きリクエストや細かなキャッシュ制御が必要な場合は URLSession などで自前実装を検討した方がいい
まずは「フェーズベースの書き方」に慣れるのがおすすめです。
ぜひAsyncImageを使いこなして、より便利なアプリを作ってくださいね!