Swiftでの非同期処理の勉強
自分がアプリ開発を勉強し始めた2021年4月から色々追加されているようで、 なんとなくで書いていた部分を公式リファレンス等読みながら、コードの書き方を勉強したまとめ。
機能自体は2021年9月に追加されたのであまり最近ではない。
Swift 5.5以前の非同期処理の方法
Swift 5.5以前では、非同期処理を行う時はクロージャを利用することが多い。
たとえば、URLからデータを非同期に取得する場合、URLSession
のdataTask
メソッドを使用する。
このメソッドで非同期にデータを取得し、取得したデータはクロージャ内で処理する。
let url = URL(string: "https://httpbin.org/ip")! URLSession.shared.dataTask(with: url) { (data, response, error) in if let error = error { print("Error: \(error)") } else if let data = data { let str = String(data: data, encoding: .utf8) print("Received data:\n\(str ?? "")") } }.resume()
RxSwiftで書いた場合
import RxSwift import RxCocoa let url = URL(string: "https://httpbin.org/ip")! let request = URLRequest(url: url) let disposeBag = DisposeBag() URLSession.shared.rx.response(request: request) .subscribe(onNext: { (response, data) in let str = String(data: data, encoding: .utf8) print("Received data:\n\(str ?? "")") }, onError: { error in print("Error: \(error)") }) .disposed(by: disposeBag)
Swift 5.5以降の非同期処理の方法
Swift 5.5以降では、非同期処理はasync
/await
を使って表現することが推奨される。
参考:swift-evolution/0296-async-await.md at main · apple/swift-evolution · GitHub
以下はサンプルコード。
import Foundation // 非同期関数の定義 func fetchData(from url: URL) async throws -> String { let (data, _) = try await URLSession.shared.data(from: url) guard let str = String(data: data, encoding: .utf8) else { throw URLError(.badServerResponse) } return str } // 非同期タスクの開始 let url = URL(string: "https://httpbin.org/ip")! Task { do { let result = try await fetchData(from: url) return result } catch { print("Error: \(error)") return "Error" } }
Taskについて
async
/await
と同じく、Swift 5.5で追加された。
Pythonを使っていると非同期処理の書き方として馴染み深いかもしれない。
先ほど使用した非同期処理のasyncメソッドだが、そのままだと同期メソッドから非同期メソッドを呼び出すことになり、エラーが発生する。
func fetchData(from url: URL) async throws -> String { let (data, _) = try await URLSession.shared.data(from: url) guard let str = String(data: data, encoding: .utf8) else { throw URLError(.badServerResponse) } return str }
Task.init
を使うとエラーが解消される。
Task { ... }
に囲まれた処理は、新しい非同期タスクとしてスケジュールされ、タスクの完了を待たずにすぐに制御が返される。
これは、 Promise
や Future
、 DispatchQueue.async
と似ている振る舞いをするが、
より強力で柔軟性がある。
Task { do { let result = try await fetchData(from: url) return result } catch { print("Error: \(error)") return "Error" } }
メインスレッドでの実行
Taskを使ったメインスレッドでの実行。
下記のコードはDispatchQueue.main.async { ... }
と同じような動作をする。
Task { @MainActor in // UI更新処理などメインスレッドで行う処理 }
具体的な処理の違い
Task { @MainActor in ... }
- コンパイル時にチェックが行われ、メインスレッドで実行する必要がある処理を明示的にマークすることができる。これにより、メインスレッド以外で実行されてはならない処理が間違ってメインスレッド以外で実行されるというミスを防ぐことができる。
- Swift5.5以降では使用が推奨される。
DispatchQueue.main.async { ... }
- ランタイムにチェックが行われるため、コードが実行されるまで メインスレッドで実行する必要がある処理を確認することができない。