iOS15+の新機能として登場したSwiftUIの「AsyncImage」を使うことで、非同期での画像データ取得が簡易的に実装できるようになりました。
本記事では、AsyncImageの機能と使い方、具体的な実装コードについて掘り下げていきます。
[ 本記事はこんな人におすすめ ]
・外部からの画像データを非同期で取得表示する方法を知りたい
・AsyncImageの使い方を知りたい
・SwiftUIでアプリ開発をしている
AsyncImageはiOS15+から使用できるSwiftUIの画像を表示するビュー機能です。
クラウドサーバーやAPIなど、外部から取得した画像URLを渡すことで、画像を表示します。
名前に「async(非同期)」とあるように、非同期的にデータを取得できるというのが大きな特徴となります。
AsyncImage(url: URL) { image in
// 画像取得完了後のビュー
} placeholder: {
// 画像取得中のプレースホルダー表示
}
URLを元にした画像データ取得は非同期的に実行されるため、データ取得の完了を待たずに他の処理を走らせることができ、容量の大きい処理であってもスムーズなユーザー操作を実現できます。
下記のサンプル実装については、本記事の後述【多機能と組み合わせた実装コード例】にてご紹介しています。
AsyncImageとの比較例として、SwiftUIの「Image」があります。
Imageは同期的にデータを取得し、画像を表示します。
例えば容量の大きい画像データや、たくさんの画像枚数をImageで表示しようとすると、ブロッキング(※1)によって画面の一時的なフリーズが発生することがあります。
これは、同期的なデータ取得によって、データの取得が完了するまで他の処理を待たせてしまっているのが原因です。
【同期的なデータ取得】
対して、AsyncImageのような非同期的なデータ取得であれば、画像データ取得の完了を待たずに他の処理が並行で実行されます。
これにより、ブロッキング(※1)によるフリーズを発生させることなく画像を取得表示することができます。
負荷がかかりやすい画像の取得処理を、他の作業場所に逃してあげているようなイメージですね。
【非同期的なデータ取得】
※1)ブロッキングとは
プログラムの実行において、ある操作が完了するまで他の操作や処理を待たせることを指します。
同期的なデータ取得では、リクエストが送信された後、データの取得が完了するまで次の処理が待機されます。この間、プログラムの実行は一時停止され、ユーザーインターフェースの反応性も低下します。
例えば、ユーザーがボタンをクリックした際に、同期的なデータ取得が行われている場合、ボタンのクリックに対する応答が遅くなり、アプリケーションが一時的にフリーズしたように感じられることがあります。
AsyncImageのシンプルな実装コード例です。
カスタムビューとして用意しておき、呼び出し時に画像URLを渡します。
・取得完了した画像はクロージャ引数image
に格納される
・クロージャ引数に格納された取得データは
.resizable()
などの加工メソッドを使用可能
/// AsyncImageを使った基本的なカスタムビュー
struct BasicAsyncImage: View {
let urlString: String
var body: some View {
// URLデータに変換後、AsyncImageにURLを渡して画像をロード
if let url = URL(string: urlString) {
AsyncImage(url: url) { image in
// 取得した画像ビュー
image.resizable()
} placeholder: {
// プレースホルダーのビュー
ProgressView()
}
}
} // body
} // View
上記のようにカスタムビューとして作っておけば、呼び出し側で画像の加工も可能です。
画像データのロード中に表示されるプレースホルダーは、任意でカスタマイズができます。
AsyncImageによる読み込みの状態は、「AsyncImagePhase」型として返ってくるデータを用いることで判定できます。
AsyncImagePhaseの値を判定に用いることで、ロード成功・失敗・途中のフェーズに合わせてビューの表示を出し分けることが可能です。
AsyncImagePhaseにはImage?型の「image」とError?型の「error」二つのオプショナルプロパティが定義されており、値の有無によって読み込みのフェーズを判定します。
/// AsyncImageの読み込みフェーズの取得
struct AsyncImagePhaseView: View {
var body: some View {
// クロージャ引数「phase」のプロパティ「image」「error」の有無判定によるフェーズの取得
AsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
if let image = phase.image {
// ロード成功時...
image.resizable()
} else if phase.error != nil {
// ロード失敗時...
Text("画像取得エラー")
} else {
// ロード中...
Text("画像取得中...")
}
}
} // body
} // View
また、以下のようなスイッチ記法による判定も可能です。
// スイッチ判定による読み込みフェーズの取得
AsyncImage(url: URL(string: urlString)) { phase in
switch phase {
case .success(let image):
image.resizable()
case .empty:
Image(systemName: "Placeholder Image")
case .failure(_):
Image(systemName: "Error Image")
@unknown default:
Image(systemName: "Placeholder Image")
}
}
AsyncImageのインスタンス変数「transition:」にトランジションを渡すことで、ビューの表示時にアニメーションを付与することができます。
この場合も、扱うデータは「AsyncImagePhase」型のデータとなります。
/// AsyncImageの引数「transition:」にトランジションを渡すことで
/// ビューの出し分けにアニメーションを付与できる
/// この場合、扱うデータは「AsyncImagePhase」型となる
AsyncImage(url: URL(string: urlString),
transaction: .init(animation: .easeIn) // ⬅︎
) { phase in
if let image = phase.image {
// ロード成功時...
} else if phase.error != nil {
// ロード失敗時...
} else {
// ロード中...
}
}
画像表示時にフェードアニメーションが付与されていますね。
最後に、ここまで挙げたAsyncImageの各機能と、他のSwiftUI機能を組み合わせた実装の一例をご紹介します。
「ScrollView」「LazyHStack」を活用した横スクロール画像プレビューです。
AsyncImageによる非同期のデータ取得と、LazyHStackの遅延ロード(※2)によって、数の多い画像描画であってもメモリに過度な負荷をかけず、ユーザー操作の快適性を損なわない実装が実現できます。
※2)遅延ロードとは
画面領域内に存在する画像のみをメモリ生成し、画面外の画像は生成しないようにすることで、画面表示の高速化・メモリの効率化を図る仕組みのこと。
本記事のサンプルコードでは、画像URLをベタ書きして使用していますが、実際の使用時にはサーバーやAPIなど、外部から取得した画像URLを用いて実装していくことになると思います。
【実装コード全体】
import SwiftUI
struct ScrollAsyncImagesView: View {
/// 複数の画像URLStringが格納された配列
let imageUrlStrings = [ "htpps://...", "htpps://...", "htpps://..." ]
var body: some View {
VStack(spacing: 40) {
ForEach(0...2, id: \.self) { _ in
ScrollView(.horizontal) {
LazyHStack {
ForEach(imageUrlStrings, id: \.self) { urlString in
AsyncImageRow(urlString: urlString)
.scaledToFill()
.frame(width: 300, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
.padding(.horizontal, 20)
}
}
}
}
}
struct AsyncImageRow: View {
let urlString: String
var body: some View {
AsyncImage(url: URL(string: urlString),
transaction: .init(animation: .easeIn)
) { phase in
if let image = phase.image {
// ロード成功時...
image.resizable()
} else if phase.error != nil {
// ロード失敗時...
LoadPhaseView(title: "画像取得エラー")
} else {
// ロード中...
LoadPhaseView(title: "画像を取得中です...")
}
}
} // body
@ViewBuilder
func LoadPhaseView(title: String) -> some View {
Rectangle()
.fill(.gray)
.overlay {
VStack(spacing: 20) {
Image(systemName: "photo.fill")
.font(.title)
Text(title)
.font(.footnote)
.tracking(5)
}
}
}
} // View
以上、AsyncImageの機能と使い方についてでした!
ポイントを改めてまとめておきます。
・画像URLをもとにした非同期的なデータ読み込みができる
・読み込みのフェーズ(状態)を取得することができる(AsyncImagePhase)
・フェーズ(成功・失敗・途中)ごとにビューの表示切り替えができる
本記事が参考になりましたら幸いです。
本記事のソースコード
\ SHARE /
アプリ開発が学べる勉強会を開催中!
CodeCandyではアプリ開発を学ぶための勉強会を定期開催しています。
学習する習慣を身につけたい、他の参加者と作業したい、アプリ開発の基本をマスターしたい、という方のために無料で学べる勉強会です。
グループにメンバー登録して頂くと、イベント開催時にメールで通知されます。
徹底した基礎学習からマスターするiPhoneアプリ開発集中オンライン講座開講!
本書「iPhoneアプリ開発集中講座」を執筆している現役エンジニア講師陣が直接に指導!
基礎、課題実習で実践力を鍛えて、オリジナルアプリ公開までチャレンジ!
充実した転職支援もあるので、エンジニアへ転職したい人にもおすすめです!
まずは、現役エンジニアに相談できる無料相談をご利用ください。
2022年2月よりSwift学習を始め、4月からiOSアプリ開発オンラインスクール「CodeCandy」にてアプリ開発を学ぶ。 2023年10月に個人開発アプリ「unico」をリリース。現在はアプリの機能アップデートをしながら、スクール運営の技術ブログの執筆や、出版書籍の入稿チェック・デバッグにも携わる。