非同期処理に対応したUIクラスを利用する
async/await構文以外にも、非同期の処理に対応したAsyncImageクラスも新しく追加されました。AsyncImageクラスは、URLから画像を取得して表示する処理を初期化処理とともに非同期で実行します。
URLから画像を取得する
URLから画像を取得して表示する処理は、前節のJSONの処理と同様に幾つものステップを踏むものでした。AsyncImageクラスを利用すると、次のように画像の取得から表示まで1行で記述することができます。
AsyncImage(url: %画像のURLオブジェクト%)
AsyncImageクラスの初期化時に画像のURLを引数として渡すことで、画像の取得から表示までを非同期で行います。画像を取得する特別な処理を記述する必要はありません。
画像の取得の前後で処理を行う
URLから画像を取得する際には、取得前にプレースホルダを表示したり、取得後にサイズを変更したりする処理を行うことが多いです。これらの処理に関しても、AsyncImageクラスの初期化時に行うことができます。
AsyncImage(url: %画像のURLオブジェクト%) { %取得した画像オブジェクト% in
// 取得後の処理
} placeholder: {
// プレースホルダの表示
}
AsyncImageクラスの初期化時に、後ろにクロージャを記述することで、取得前と取得後の処理を定義することができます。非同期で行われる処理は、URLからの画像の取得だけであり、その前後の処理はクロージャで定義します。
次節のサンプルでAsyncImageクラスの具体的な使い方を説明します。
async/await構文とAsyncImageクラスの利用法
Yahoo画像検索を利用した簡単な画像検索アプリを作成して、async/await構文とAsyncImageクラスを実際のアプリの処理として確認してみます。
作成するサンプルの概要
作成するサンプルは、入力されたキーワードからボタン押下時にYahoo画像検索を行い、検索結果から画像を抽出してグリッドに表示するアプリです。
作成するアプリの概要
キーワードの文字数制限はSwiftUIの機能を利用します。グリッド表示にはQGridを利用しますので、使い方などの詳細はGithubで確認しておいてください。
検索結果の画像を表示するクラス
検索結果の画像を、QGridのセルとして表示するためには、ユニークなIDを持つ構造体が必要です。ユニークなIDと画像のURLオブジェクトを持つ構造体を、Identifiableプロトコルを実装して定義します。
struct ImageData: Identifiable {
let id = UUID()
let url: URL
}
次に、QGridのセルとして表示するViewの構造体を作成します。
struct GridCell: View {
var imageData: ImageData
var body: some View {
VStack {
AsyncImage(url: imageData.url) { image in
image.resizable() // リサイズして円で表示
.frame(width: 100, height: 100)
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
} placeholder: {
ProgressView() // プレースホルダ
}
}
}
}
作成したImageData構造体をプロパティとして持たせ、AsyncImageクラスで画像を表示するようにします。AsyncImageクラスでURLから取得した画像は100x100の正方形のサイズにアスペクト比を保ったままリサイズし、clipShapeメソッドで円形に表示します。
プレースホルダにはProgressViewクラスを表示して、ローディング中であることが分かるようにします。
画像検索を制御するクラス
Yahoo画像検索を行って結果を取得する処理をImageLoaderクラスとして定義します。View側から検索結果を参照するので、ObservableObject型のクラスとして定義します。
class ImageLoader: ObservableObject {
@Published var imageList: [ImageData] = []
# 略
ObservableObject型で宣言したクラスの中で、検索結果をImageData構造体の配列として持つプロパティをimageListの名前で定義します。imageListプロパティには、@Publishedをつけて値に変化があった時に、その旨をView側で自動的に検知できるようにしておきます。
次に、Yahoo画像検索を行って検索結果の画像のURLを取得する処理をsearchメソッドの名前で作成します。Yahoo画像検索から画像URLを取得する処理は、次の記事のサンプルを確認してください。
func search(_ keyword: String) async throws {
let urlStr = "https://search.yahoo.co.jp/image/search?ei=UTF-8&p=\(keyword)"
let url = URL(string:urlStr.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!)!
var request = URLRequest(url: url)
request.addValue("xxxxx@xxxx.com", forHTTPHeaderField: "User-Agent")
let (data, response) = try await URLSession.shared.data(for: request, delegate:nil)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw ImageError.serverError }
guard let html = String(data: data, encoding: .utf8) else { throw ImageError.noData }
DispatchQueue.main.async { // メインスレッドで処理
self.imageList = []
// 画像検索結果のHTMLから検索結果の画像URLを正規表現で取得
let pattern = "(https?)://msp.c.yimg.jp/([A-Z0-9a-z._%+-/]{2,1024}).jpg"
let regex = try! NSRegularExpression(pattern: pattern, options: [])
let results = regex.matches(in: html, options: [], range: NSRange(0..<html.count))
// 取得した画像URLをImageDataで返す
self.imageList = results.compactMap { result in
let start = html.index(html.startIndex, offsetBy: result.range(at: 0).location)
let end = html.index(start, offsetBy: result.range(at: 0).length)
let text = String(html[start..<end])
return text
}.reduce([], { $0.contains($1) ? $0 : $0 + [$1] })
.map( { ImageData(url: URL(string: $0)! )})
}
}
searchメソッドは、asyncをつけて処理の結果を待てるように定義しています。Yahoo画像検索の結果から画像URLを正規表現で取得し、ImageDataの配列を作成してimageListプロパティに代入します。
@Publishedをつけて宣言した変数は、更新されるとViewも更新されます。つまり@Publishedをつけて宣言した変数を更新する際には、UIも更新されます。UIの更新処理は、メインスレッドで行わなければなりません。
サンプル内のimageListプロパティの値を更新する処理も、DispatchQueue.main.asyncブロックで囲ってメインスレッドで実行するように定義します。
入力系UIの配置
必要な変数とともに、検索キーワードの入力欄、ボタンを画面に配置します。ObservableObjectプロトコルに準拠したクラスのインスタンスは、値の変更がView側で分かるように@StateObjectをつけて宣言します。サンプルでは、ObservableObject型で宣言したImageLoaderクラスのインスタンスには、@StateObjectをつけて画像検索の結果をView側で受け取れるようにします。
画像検索のキーワード、ボタンの押下可否といったプロパティは、UIとBindingして管理する@Stateをつけて宣言し、プロパティの値とUIが連動して動作できるようにします。
import SwiftUI
import QGrid
struct ContentView: View {
@StateObject var imageLoader = ImageLoader()
@State var text = "" // 検索キーワード
@State var buttonEnabled = false // ボタン押下可否
var body: some View {
NavigationView {
VStack {
TextField("検索キーワード",text: $text, onEditingChanged: { editing in })
.onChange(of: text) {
buttonEnabled = $0.count >= 3 // 3文字以上でボタン押下可能
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(24)
Button(action: {
UIApplication.shared.endEditing() // キーボードを下げる
Task {
do {
// 画像検索
try await imageLoader.search(text)
} catch {
print(error)
}
}
}) {
Text ("Search")
}.disabled(!buttonEnabled)
検索キーワードのプロパティをtext、ボタンの押下可否をbuttonEnabledの名前で@Stateをつけて宣言し、それぞれ入力欄とボタンにバインディングします。
ボタンを押した時に、ImageLoaderクラスのsearchメソッドを、awaitをつけて実行します。
画像検索結果の表示
画像検索結果をグリッド表示する部分を作成します。ImageLoaderクラスのsearchメソッドを実行した後は、画像検索の結果がimageListプロパティに格納されます。
imageListプロパティの値がそのままQGridに表示される処理を記述します。
struct ContentView: View {
# 略
var body: some View {
NavigationView {
VStack {
# 略
Button(action: {
# 略
}) {
Text ("Search")
}.disabled(!buttonEnabled)
Spacer()
// 検索結果をグリッド表示
QGrid(imageLoader.imageList, columns: 3,
vSpacing: 16, hSpacing: 8,
vPadding: 16,hPadding: 16,
isScrollable: true, showScrollIndicators: false
) { item in
GridCell(imageData: item)
}
}.navigationBarTitle(Text("画像検索"), displayMode: .inline)
}
}
}
QGridに配列を渡すと、配列の要素がクロージャに渡されます。この時にグリッドの形状とグリッドのセルを指定します。サンプルでは、最初の項で作成したGridCellに画像検索の結果であるImageDataを渡してAsyncImageで画像を表示しています。
サンプルを実行すると、次の動作が確認できます。
サンプルの実行結果
async/await構文、AsyncImageクラス、SwiftUIを組み合わせた処理を行うことで、次の点でプログラムを簡潔に記述できます。
- 画像検索を行う際にawaitを使って結果を取得するため、クロージャやデリゲートを利用しなくて済む
- 画像検索の結果は、UI側で値の変化を自動的に検知できるため、値を取得する処理を記述する必要はない
- 画像の表示にはAsyncImageクラスを利用するため、URLから画像を取得する特別な処理を記述しなくてよい
非同期処理のサポート以前やUIKItでの処理と比べて、少ないコードで目的の処理を実装できていることを確認してみてください。
まとめ
今回の記事では、 Swift5.5から公式にサポートされた非同期処理について説明しました。非同期処理を利用すると、何かと面倒なデリゲートやクロージャを経由することなく、処理の実行と処理の結果取得を同じ部分に記述することができます。iOS15以降では、既存のクラスのメソッドでもasync対応しているものが多いので是非とも使ってみてください。
