SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Swift 5.5で追加された非同期処理を使いこなす

Swift 5.5でサポートされた非同期処理の使い方――async/await構文と新UIクラス

  • X ポスト
  • このエントリーをはてなブックマークに追加

非同期処理に対応したUIクラスを利用する

 async/await構文以外にも、非同期の処理に対応したAsyncImageクラスも新しく追加されました。AsyncImageクラスは、URLから画像を取得して表示する処理を初期化処理とともに非同期で実行します。

URLから画像を取得する

 URLから画像を取得して表示する処理は、前節のJSONの処理と同様に幾つものステップを踏むものでした。AsyncImageクラスを利用すると、次のように画像の取得から表示まで1行で記述することができます。

[リスト4]AsyncImageクラスの初期化
AsyncImage(url: %画像のURLオブジェクト%)

 AsyncImageクラスの初期化時に画像のURLを引数として渡すことで、画像の取得から表示までを非同期で行います。画像を取得する特別な処理を記述する必要はありません。

画像の取得の前後で処理を行う

 URLから画像を取得する際には、取得前にプレースホルダを表示したり、取得後にサイズを変更したりする処理を行うことが多いです。これらの処理に関しても、AsyncImageクラスの初期化時に行うことができます。

[リスト5]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プロトコルを実装して定義します。

[リスト6]画像検索結果を格納する構造体(ImageSearchSample/ImageData.swift抜粋)
struct ImageData: Identifiable {
    let id = UUID()
    let url: URL
}

 次に、QGridのセルとして表示するViewの構造体を作成します。

[リスト7]グリッドのセルを表示するクラス(ImageSearchSample/GridCell.swift抜粋)
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型のクラスとして定義します。

[リスト8]画像検索を制御するクラス(ImageSearchSample/ImageLoader.swift抜粋)
class ImageLoader: ObservableObject  {
    
    @Published var imageList: [ImageData] = []
    
# 略

 ObservableObject型で宣言したクラスの中で、検索結果をImageData構造体の配列として持つプロパティをimageListの名前で定義します。imageListプロパティには、@Publishedをつけて値に変化があった時に、その旨をView側で自動的に検知できるようにしておきます。

 次に、Yahoo画像検索を行って検索結果の画像のURLを取得する処理をsearchメソッドの名前で作成します。Yahoo画像検索から画像URLを取得する処理は、次の記事のサンプルを確認してください。

[リスト9]画像検索を制御するクラス(ImageSearchSample/ImageLoader.swift抜粋)
    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が連動して動作できるようにします。

[リスト10]入力系UIの配置(ImageSearchSample/ContentView.swift抜粋)
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に表示される処理を記述します。

[リスト11]画像検索結果の表示(ImageSearchSample/ContentView.swift抜粋)
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対応しているものが多いので是非とも使ってみてください。

参考資料

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
この記事の著者

WINGSプロジェクト 片渕 彼富(カタフチ カノトミ)

WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS Twitter: @yyamada(公式)、@yyamada/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/15457 2022/01/28 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング