とらのメモ

IT関係やガジェットについての雑記

Swift5 非同期処理の勉強

Swiftでの非同期処理の勉強

自分がアプリ開発を勉強し始めた2021年4月から色々追加されているようで、 なんとなくで書いていた部分を公式リファレンス等読みながら、コードの書き方を勉強したまとめ。

機能自体は2021年9月に追加されたのであまり最近ではない。

Swift 5.5以前の非同期処理の方法

Swift 5.5以前では、非同期処理を行う時はクロージャを利用することが多い。

たとえば、URLからデータを非同期に取得する場合、URLSessiondataTaskメソッドを使用する。 このメソッドで非同期にデータを取得し、取得したデータはクロージャ内で処理する。

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 { ... } に囲まれた処理は、新しい非同期タスクとしてスケジュールされ、タスクの完了を待たずにすぐに制御が返される。

これは、 PromiseFutureDispatchQueue.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 { ... }

    • ランタイムにチェックが行われるため、コードが実行されるまで メインスレッドで実行する必要がある処理を確認することができない。

参考