🔥
Swift ConcurrencyでGitHub APIを使う
この記事では、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
}
}
コールバックを使っていた時よりスッキリ書けました。
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を使わなかった理由
今回はObservableObject
にclass
の代わりにactor
は使いませんでした。
以下の理由です。
-
@Published
の代わりに@MainActor @Published
にもできますが、個数が多くなってしまう - このクラス内での処理がそこまで複雑ではない
-
View
側で.task
を使うと、エラーハンドリングなどのロジックがView
側に入ってしまう
ソースコード
今回のソースコードはこちらです。
参考
Discussion