🪶

Swift async/await

2024/07/18に公開

how to async/await

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/

公式より引用

Swiftは、構造化された方法で非同期と並列コードを書くための組み込みのサポートを持っています。非同期コードは、プログラムの1つの部分だけが一度に実行されますが、中断して後で再開することができます。プログラムのコードを一時停止したり再開したりすることで、UIの更新のような短期的な処理を進めながら、ネットワーク経由のデータ取得やファイルの解析のような長期的な処理を続けることができる。並列コードとは、複数のコードが同時に実行されることを意味する。たとえば、4コアのプロセッサを搭載したコンピュータでは、4つのコードを同時に実行することができ、それぞれのコアが1つのタスクを実行する。並列コードと非同期コードを使用するプログラムは、一度に複数の処理を実行し、外部システム待ちの処理を中断する。

Swiftは、構造化された方法で非同期と並列コードを書くための組み込みのサポートを持っています。非同期コードは、プログラムの1つの部分だけが一度に実行されますが、中断して後で再開することができます。プログラムのコードを一時停止したり再開したりすることで、UIの更新のような短期的な処理を進めながら、ネットワーク経由のデータ取得やファイルの解析のような長期的な処理を続けることができる。並列コードとは、複数のコードが同時に実行されることを意味する。たとえば、4コアのプロセッサを搭載したコンピュータでは、4つのコードを同時に実行することができ、それぞれのコアが1つのタスクを実行する。並列コードと非同期コードを使用するプログラムは、一度に複数の処理を実行し、外部システム待ちの処理を中断する。

Swiftの言語サポートを使わずに並行コードを書くことは可能ですが、そのコードは読みにくくなる傾向があります。例えば、以下のコードは写真の名前のリストをダウンロードし、そのリストの最初の写真をダウンロードし、ユーザーにその写真を表示します:

listPhotos(inGallery: "Summer Vacation") { photoNames in
    let sortedNames = photoNames.sorted()
    let name = sortedNames[0]
    downloadPhoto(named: name) { photo in
        show(photo)
    }
}

この単純なケースでも、コードは一連の完了ハンドラとして書かなければならないので、結局は入れ子のクロージャを書くことになる。このスタイルでは、深いネストを含む複雑なコードはすぐに扱いにくくなる。

非同期関数の定義と呼び出し

非同期関数や非同期メソッドは、実行の途中で中断できる特殊な関数やメソッドです。これは通常の同期関数やメソッドとは対照的で、完了まで実行するか、エラーを投げるか、あるいは決して戻りません。非同期関数やメソッドは、これら3つのうちの1つを実行することに変わりはありませんが、何かを待っているときに途中で一時停止することもできます。非同期関数やメソッドのボディの内部では、実行を一時停止できる場所をそれぞれマークする。

関数やメソッドが非同期であることを示すには、宣言でパラメータの後にasyncキーワードを記述する。関数やメソッドが値を返す場合は、return矢印(->)の前にasyncと記述します。例えば、ギャラリーの写真の名前を取得する方法を以下に示します:

func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

非同期とthrowsの両方を持つ関数やメソッドでは、throwsの前にasyncと書く。

非同期メソッドを呼び出すと、そのメソッドが戻るまで実行が一時停止する。呼び出しの前にawaitと書くのは、中断される可能性のあるポイントを示すためだ。これは、throw関数を呼び出すときにtryを書くのと同じで、エラーが発生した場合にプログラムの流れが変わる可能性があることを示すためだ。非同期メソッドの内部では、別の非同期メソッドを呼び出したときだけ実行の流れが一時停止される - 一時停止は暗黙的またはプリエンプティブに行われることはない - つまり、一時停止の可能性があるすべてのポイントにawaitで印をつけることになる。コード内のすべての中断ポイントをマークすることで、並行コードを読みやすく理解しやすくすることができます。

例えば、以下のコードはギャラリー内のすべての写真の名前を取得し、最初の写真を表示します:

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

長い解説が続くので、ここまでにして、実際に使ってみよう😅

Githubからデータをfetchしてみよう

async/await を使って、Githubのユーザーの名前とアイコン画像を取得してみましょう。

同じファイルに全て書いているので、冗長になってしまいましたが、こんな感じで書けば、インターネットから、取得したデータを非同期取得できます。

import SwiftUI

// MARK: - Models

struct User: Codable, Identifiable {
    let id = UUID()
    let login: String
    let url: String
    let avatar_url: String
    let html_url: String
}

struct APIResponse: Codable {
    let items: [User]
}

// MARK: - ViewModel

enum FetchState {
    case idle
    case loading
    case loaded([User])
    case error(Error)
}

class UserViewModel: ObservableObject {
    @Published var fetchState: FetchState = .idle
    
    func getUsers() {
        fetchState = .loading
        
        Task {
            let result = await fetchUsers()
            DispatchQueue.main.async {
                switch result {
                case .success(let users):
                    self.fetchState = .loaded(users)
                case .failure(let error):
                    self.fetchState = .error(error)
                }
            }
        }
    }
    
    private func fetchUsers() async -> Result<[User], Error> {
        guard let apiURL = URL(string: "https://api.github.com/search/users?q=greg") else {
            return .failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
        }
        
        do {
            let (data, _) = try await URLSession.shared.data(from: apiURL)
            let apiResponse = try JSONDecoder().decode(APIResponse.self, from: data)
            return .success(apiResponse.items)
        } catch {
            return .failure(error)
        }
    }
}

// MARK: - Views

struct LoadingView: View {
    var body: some View {
        VStack {
            ProgressView().padding()
            Text("Fetching Users...")
        }
    }
}

struct UserListView: View {
    let users: [User]
    
    var body: some View {
        List(users) { user in
            Link(destination: URL(string: user.html_url)!) {
                HStack {
                    AsyncImage(url: URL(string: user.avatar_url)) { phase in
                        switch phase {
                        case .success(let image):
                            image.resizable().frame(width: 50, height: 50)
                        default:
                            Image(systemName: "nosign")
                        }
                    }
                    VStack(alignment: .leading) {
                        Text(user.login)
                        Text(user.url)
                            .font(.system(size: 11))
                            .foregroundColor(Color.gray)
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        NavigationStack {
            Group {
                switch viewModel.fetchState {
                case .idle:
                    Text("Tap to load users")
                case .loading:
                    LoadingView()
                case .loaded(let users):
                    UserListView(users: users)
                case .error(let error):
                    Text("Error: \(error.localizedDescription)")
                }
            }
            .navigationTitle("GitHub Users")
        }
        .onAppear {
            viewModel.getUsers()
        }
    }
}

#Preview {
    ContentView()
}

Lastly

今回はSwiftで、async/awaitを使ってみました。使う場面は、時間かかる処理を実行するとき、同時に処理を実行するが、読み込み中は待って実行したい処理を使う場面で実行します。

Discussion