🔥

Swift ConcurrencyでGitHub APIを使う

2021/09/27に公開

この記事では、iOS15から使える(現時点)Concurrencyを使って、GitHub APIのリポジトリ検索機能を実装します。
async/awaitなどの用語についての説明は、最低限に留めます。

APIクライアント

APIの処理を行うクラスを実装します。

class ApiClient {
    
    private let session = URLSession.shared
    private let baseUrl: String = "https://api.github.com"
    
    .....

    func searchRepositories(query: String) async throws -> SearchRepositoryResponse {
        let queryString = query.replacingOccurrences(of: " ", with: "+")
        let url = URL(string: baseUrl + "/search/repositories?q=\(queryString)")!
        let request = URLRequest(url: url)
        async let (data, _) = session.data(for: request)
        return try await decoder.decode(SearchRepositoryResponse.self, from: data) //need try await
    }
    
}

GitHub検索API

コールバックを使っていた時よりスッキリ書けました。
searchRepositoriesの最後でawaitを使っていますが、これはasync letで定義した変数(ここではdata)を使うには、awaitが必要になるためです。
もう一点、classではなくactorを使うことを検討していましたが、このクラスでは特に平行処理で考えるべき変数がなかったためclassにしました。

ViewModel/ObservableObject

ViewModelの実装です。


//③UIスレッドで行うため
@MainActor
class ViewModel: ObservableObject {
    
    @Published var query: String = ""
    @Published var items: [SearchRepositoryItem] = []
    @Published var loading: Bool = false
    @Published var hasError: Bool = false
    @Published var errorMessage: String = ""
    
    private var cancellations = Set<AnyCancellable>()
    private let apiClient = ApiClient()
    
    init() {

        //①キーボードを打ち終わってから1秒後に処理を行います
        $query
            .debounce(for: 1.0, scheduler: DispatchQueue.main)
            .sink(receiveValue: { [weak self] in self?.doSearch(text: $0) })
            .store(in: &cancellations)
        
    }
    
}

private extension ViewModel {
    
    func doSearch(text: String) {
        
        if text.isEmpty {
            return
        }
        
        debugPrint("doSearch: \(text)")
        
        //②Taskで非同期処理を行います。
        Task {
            self.loading = true
            do {
                let response =  try await apiClient.searchRepositories(query: text)
                self.items = response.items
                self.hasError = false
            } catch let error {
                debugPrint(error)
                self.hasError = true
                self.errorMessage = error.localizedDescription
            }
            self.loading = false
        }
        
    }
    
}

このクラスの機能として、

  • ①キーボードを打ち終わってから1秒後に検索を開始します
  • ②Taskで非同期処理を行います
  • ③UIスレッドで結果を受け取るために@MainActorを使います。

@MainActorが付けられた処理は必ずメインスレッドで実行されます。変数にもつけることができますが、今回はclassにつけています。注意点として全ての処理がメインスレッドで行われるので、複雑なロジックは別スレッドで行うようにします。

View

Viewの実装です。一部省略しています。

struct ContentView: View {
    
    @ObservedObject var viewModel: ViewModel = .init()
    
    var body: some View {

        VStack {
            
            TextField.init("search respository", text: $viewModel.query)

            Group {

                if !viewModel.hasError {
                    
                    VStack {
                        if viewModel.loading {
                            ProgressView()
                        }
                        
                        List {
                            ForEach.init(viewModel.items) { item in
                                RepositoryRow(item: item)
                            }
                        }.frame(maxHeight: .infinity)
                        
                    }                    
                    
                } else {
                    //show error
                    
                    VStack {
                        Image(systemName: "xmark.octagon.fill")
                        Text("Error").font(.title)
                        Text(self.viewModel.errorMessage).font(.body)
                    }
                    
                }
            }.frame(maxHeight: .infinity)
            
        }
    }
}

検索結果に従い、Viewを出し分けたりしています。

ObservableObjectにactorを使わなかった理由

今回はObservableObjectclassの代わりにactorは使いませんでした。
以下の理由です。

  • @Publishedの代わりに@MainActor @Publishedにもできますが、個数が多くなってしまう
  • このクラス内での処理がそこまで複雑ではない
  • View側で.taskを使うと、エラーハンドリングなどのロジックがView側に入ってしまう

ソースコード

今回のソースコードはこちらです。
https://github.com/usk-sample/GithubConcurrencySample/

参考

https://zenn.dev/akkyie/articles/swift-concurrency#mainactor
https://dev.classmethod.jp/articles/try-async-await-actor-in-swift/

Discussion