見出し画像

SwiftUI + Concurrencyの非同期処理を理解したい【入門編】

こんにちは、Concurrencyでの非同期処理を理解したいマンです。

以前、「SwiftUI + Combineでアプリを作るぞ!」と意気込んでいましたが、最近では「SwiftUI + Concurrency」の組み合わせのほうがアツく、Combineはオワコンとか言われはじめましたね。(勉強した時間返せ。)

そこで今回は、SwiftUIとConcurrencyの基本的な概念と使い方や、Combineとの違い、それぞれのメリットデメリットなど、自分なりの解釈をコード例も交えつつ備忘録に残していきます。

1. そもそも非同期処理って何?

まず、非同期処理についてざっくり理解しましょう。
想像してみてください。あなたはカフェで注文をしています☕️

  1. 同期処理の場合:

    • あなたがコーヒーを注文する

    • バリスタがコーヒーを作り終わるまであなたはカウンターで待つ

    • コーヒーができたら受け取って席に戻る

  2. 非同期処理の場合:

    • あなたがコーヒーを注文する

    • 注文番号をもらって席に戻る

    • バリスタがコーヒーを作っている間、あなたは本を読んだりスマホを見たりできる

    • コーヒーができたら呼ばれて受け取りに行く

つまり、前者の「同期処理」の場合は、注文したあとコーヒーが完成するまで待つ必要がある=他の行動ができない。
後者の「非同期処理」の場合は、待機時に別の行動ができる。

SNSで画面を下に引っ張った際に、新しい投稿が読み込まれるますよね?その時に裏で最新の投稿データをサーバーから取得し、UIを更新しているのが身近なアプリケーションでいう非同期処理ですね。

あとは、洗濯機とか炊飯器もボタンポチっと押して任せている間に自分が他の家事を済ませるというのも身近な非同期処理ですかね。

2. Concurrencyとは

SwiftのConcurrencyは、Swift 5.5で導入された非同期プログラミングを簡潔に書くのためのフレームワークです。直訳すると「並行処理」。主に3つの要素があります:

  1. async/await:非同期の処理を書くための文法

  2. Actor:データの競合を防ぐための特別な型

  3. Task:非同期の作業の単位

カフェの例で説明すると:

  • async/await:注文を出して(async)、コーヒーができるのを待つ(await)こと

  • Actor:混雑時でも順番を間違えずに注文を処理できるスーパーバリスタ

  • Task:「コーヒーを1杯入れる」といった1つの作業のこと

3. SwiftUIとConcurrencyの相性

SwiftUIは画面の見た目を簡単に作れる優れものです。Concurrencyと組み合わせると、次のような利点があります:

  1. コードが読みやすくなる:非同期の処理が分かりやすく書ける

  2. アプリの動きがスムーズになる:重い処理を裏で行えるので、画面がカクカクしにくい

  3. データの管理が楽になる:Actorを使うと、複数の場所から同時にデータをいじっても安全(らしい)

4. 基本的な使い方

では、実際のコードを見てみましょう。カフェで注文するアプリを作ってみます。

import SwiftUI

struct CafeView: View {
    @State private var orderStatus = "準備中..."
    
    var body: some View {
        VStack {
            Text(orderStatus)
            Button("コーヒーを注文") {
                Task {
                    await orderCoffee()
                }
            }
        }
    }
    
    func orderCoffee() async {
        orderStatus = "注文受付中..."
        // コーヒーを作るのに3秒かかると仮定
        try? await Task.sleep(nanoseconds: 3_000_000_000)
        orderStatus = "コーヒーができました!受け取ってください。"
    }
}

このコードを詳しく見ていきましょう:

  • @State:画面に表示する内容(注文状況)を保存します

  • Button:タップするとコーヒーを注文します

  • Task { ... }:非同期の処理を始めるおまじないです

  • await orderCoffee():コーヒーができるまで待ちます

  • func orderCoffee() async:非同期で動く関数です

  • try? await Task.sleep(...):コーヒーを作る時間を再現しています

5. Actorを使ってみよう

次は、Actorを使ってカフェの従業員を表現してみましょう。

actor CafeStaff {
    private var ordersInProgress = 0
    
    func makeCoffee() async -> String {
        ordersInProgress += 1
        defer { ordersInProgress -= 1 }
        
        // コーヒーを作る(3秒かかると仮定)
        try? await Task.sleep(nanoseconds: 3_000_000_000)
        
        return "淹れたてのコーヒー"
    }
    
    func currentOrders() -> Int {
        ordersInProgress
    }
}

struct CafeView: View {
    @State private var orderStatus = "準備中..."
    @State private var orderCount = 0
    let staff = CafeStaff()
    
    var body: some View {
        VStack {
            Text(orderStatus)
            Text("現在の注文数: \(orderCount)")
            Button("コーヒーを注文") {
                Task {
                    orderStatus = await staff.makeCoffee()
                    orderCount = await staff.currentOrders()
                }
            }
        }
    }
}

ここでのポイントは:

  • actor CafeStaff:複数の注文を安全に処理できるスーパー従業員です

  • ordersInProgress:現在処理中の注文数を管理します

  • makeCoffee():非同期でコーヒーを作ります

  • await staff.makeCoffee():従業員にコーヒーを作ってもらいます

Actorを使うと、複数の人が同時に注文しても混乱せずに対応できます。

6. CombineとConcurrencyの違い

最後に、以前よく使われていたCombineという方法と、新しいConcurrencyを比較してみましょう。

まず、共通で使う部分を準備します:

struct Coffee: Decodable {
    let name: String
    let price: Int
}

class CafeService {
    func orderCoffee(name: String) async throws -> Coffee {
        // 実際はここでネットワーク通信をします
        try await Task.sleep(nanoseconds: 2_000_000_000)
        return Coffee(name: name, price: 500)
    }
}

Combineバージョン

import Combine
import SwiftUI

class CafeViewModelCombine: ObservableObject {
    @Published var coffee: Coffee?
    @Published var errorMessage: String?
    
    private let cafeService = CafeService()
    private var cancellables = Set<AnyCancellable>()
    
    func orderCoffee(name: String) {
        Future<Coffee, Error> { promise in
            Task {
                do {
                    let coffee = try await self.cafeService.orderCoffee(name: name)
                    promise(.success(coffee))
                } catch {
                    promise(.failure(error))
                }
            }
        }
        .receive(on: DispatchQueue.main)
        .sink { completion in
            if case .failure(let error) = completion {
                self.errorMessage = error.localizedDescription
            }
        } receiveValue: { coffee in
            self.coffee = coffee
        }
        .store(in: &cancellables)
    }
}

Concurrencyバージョン

import SwiftUI

@MainActor
class CafeViewModelConcurrency: ObservableObject {
    @Published var coffee: Coffee?
    @Published var errorMessage: String?
    
    private let cafeService = CafeService()
    
    func orderCoffee(name: String) {
        Task {
            do {
                self.coffee = try await cafeService.orderCoffee(name: name)
            } catch {
                self.errorMessage = error.localizedDescription
            }
        }
    }
}

主な違いは:

  1. コードの量:Concurrencyの方がシンプルで短いです

  2. エラー処理:Concurrencyは普通のtry-catchが使えます

  3. キャンセル処理:Concurrencyは自動でキャンセルしてくれます

  4. メインスレッドの扱い:Concurrencyは@MainActorで簡単に指定できます

まとめ

SwiftUIとConcurrencyを使うと、非同期処理を含むアプリがとても作りやすい気がします。

  • 非同期処理:待ち時間の間に他の作業ができる効率的な方法

  • Concurrency:Swiftの新しい非同期処理の書き方

  • SwiftUI + Concurrency:画面の動きとデータの処理を簡単に書ける



この記事が気に入ったらサポートをしてみませんか?