🔄

SwiftUIのonAppearとtaskの違いを理解する

に公開

SwiftUIのonAppearとtaskの違いを理解する

この記事は?

SwiftUIで画面の表示に合わせて処理を実行したい場合、主に2つのモディファイアを使用します:

  • .onAppear()
  • .task()

この2つは一見似ていますが、実際には大きな違いがあります。本記事では、これらのモディファイアの違いと使い分け方を公式ドキュメントを引用しながら解説します。

onAppear

onAppearは、ビューが画面に表示されたタイミングで同期的な処理を実行するためのモディファイアです。

公式ドキュメント

公式ドキュメントでは以下のように説明されています:

Adds an action to perform when this view appears.

(このビューが表示されたときに実行するアクションを追加します。)

func onAppear(perform action: (() -> Void)? = nil) -> some View

公式ドキュメントによれば、このモディファイアは「ビューが表示階層に追加された直後」に実行されます。

基本的な使い方

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .onAppear {
                print("ビューが表示されました")
                // 同期的な処理を実行
                loadInitialData()
            }
    }
    
    func loadInitialData() {
        // データ読み込み処理
    }
}

onAppearの特徴

  1. 同期処理向け: onAppearは非同期関数(async)を直接受け付けません
  2. 引数なし: 外部からの値を直接受け取れません
  3. キャンセル不可: 一度実行されたアクションはキャンセルできません
  4. 表示イベントのみ: ビューが表示された時のみ発火し、他のライフサイクルイベントは監視しません

task

taskは、iOS 15以降で導入された、ビューの表示に合わせて非同期処理を実行するためのモディファイアです。

公式ドキュメント

公式ドキュメントでは以下のように説明されています:

Adds an asynchronous task to perform when this view appears.

(このビューが表示されたときに実行する非同期タスクを追加します。)

func task(
    priority: TaskPriority = .userInitiated,
    @_implicitSelfCapture _ action: @escaping @Sendable () async -> Void
) -> some View

さらに、重要な追加情報として:

The system cancels the task if the view disappears before the task completes.

(タスクが完了する前にビューが非表示になると、システムはタスクをキャンセルします。)

基本的な使い方

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .task {
                print("非同期タスクを開始します")
                // 非同期処理を実行
                await fetchData()
            }
    }
    
    func fetchData() async {
        // 時間のかかるネットワークリクエストなど
        try? await Task.sleep(nanoseconds: 2_000_000_000) // 2秒待機
        print("データの取得が完了しました")
    }
}

taskの特徴

  1. 非同期処理向け: async/awaitを使用した非同期処理に対応
  2. 自動キャンセル: ビューが非表示になると実行中のタスクが自動的にキャンセルされる
  3. 優先度指定: タスクの実行優先度を指定できる
  4. @Sendable要件: クロージャーは@Sendableである必要がある

主な違いと使い分け

onAppear task
導入バージョン SwiftUI 1.0 (iOS 13+) SwiftUI 3.0 (iOS 15+)
処理タイプ 同期処理 非同期処理
使用方法 .onAppear { ... } .task { await ... }
キャンセル機能 なし あり(自動)
再実行タイミング ビューのライフサイクルに依存 ビューのライフサイクルとID変更時
主な用途 シンプルな初期化処理 時間のかかるデータ取得処理

具体的なユースケース

onAppearの適切な使用例

  1. UIの更新: アニメーションの開始やUIの更新
.onAppear {
    withAnimation {
        isExpanded = true
    }
}
  1. ナビゲーション状態の更新: 画面遷移の記録など
.onAppear {
    analyticsService.logScreenView("DetailView")
}
  1. 軽量なローカルデータの読み込み: UserDefaultsからの読み込みなど
.onAppear {
    username = UserDefaults.standard.string(forKey: "username") ?? ""
}

taskの適切な使用例

  1. APIからのデータ取得:
.task {
    items = await apiService.fetchItems()
}
  1. 大量のデータの非同期処理:
.task {
    processedImages = await processImages(images)
}
  1. 定期的な更新が必要なデータ:
.task(id: refreshID) {
    while !Task.isCancelled {
        latestData = await fetchLatestData()
        try? await Task.sleep(nanoseconds: 60_000_000_000) // 1分ごとに更新
    }
}

taskとIDを使った再実行

taskモディファイアは、第一引数にidを指定することで、その値が変化したときにタスクを再実行できます。

.task(id: userID) {
    userData = await fetchUserData(userID)
}

この場合、userIDが変更されるたびに新しいタスクが開始され、以前のタスクはキャンセルされます。公式ドキュメントでは:

The task cancels when the view disappears, or when id changes, upon which SwiftUI creates a new task using the action closure.

(タスクはビューが非表示になったとき、またはIDが変更されたときにキャンセルされ、その後SwiftUIはアクションクロージャを使用して新しいタスクを作成します。)

実際のコード例:データ取得

onAppearでの実装(iOS 13+対応)

struct ProductListView: View {
    @StateObject private var viewModel = ProductViewModel()
    
    var body: some View {
        List(viewModel.products) { product in
            Text(product.name)
        }
        .onAppear {
            // 非同期処理をDispatchQueueで処理
            DispatchQueue.global().async {
                let products = viewModel.fetchProductsSync()
                DispatchQueue.main.async {
                    viewModel.products = products
                }
            }
        }
    }
}

taskでの実装(iOS 15+)

struct ProductListView: View {
    @StateObject private var viewModel = ProductViewModel()
    
    var body: some View {
        List(viewModel.products) { product in
            Text(product.name)
        }
        .task {
            // 非同期処理をスムーズに記述
            viewModel.products = await viewModel.fetchProducts()
        }
    }
}

タスクの明示的なキャンセル

taskで開始されたタスクは自動的にキャンセルされますが、Task.checkCancellation()Task.isCancelledを使って自分でキャンセルを確認することもできます:

.task {
    do {
        for i in 1...100 {
            // 定期的にキャンセルをチェック
            try Task.checkCancellation()
            await processItem(i)
        }
    } catch {
        // キャンセルされたときの処理
        print("Task was cancelled")
    }
}

iOS 15未満での非同期処理

iOS 15未満で非同期処理を扱う場合の代替手段:

struct LegacyDataView: View {
    @State private var data: [Item] = []
    @State private var cancellable: AnyCancellable?
    
    var body: some View {
        List(data) { item in
            Text(item.title)
        }
        .onAppear {
            cancellable = fetchData()
                .receive(on: DispatchQueue.main)
                .sink(receiveCompletion: { _ in },
                      receiveValue: { self.data = $0 })
        }
        .onDisappear {
            cancellable?.cancel()
        }
    }
    
    func fetchData() -> AnyPublisher<[Item], Error> {
        // Combine を使った非同期処理
    }
}

まとめ

  • onAppear: iOS 13以上で使用可能な、同期処理向けのシンプルなモディファイア
  • task: iOS 15以上で使用可能な、非同期処理に最適化されたモディファイア

それぞれの特性を理解して適切に使い分けることで、SwiftUIアプリの効率とユーザー体験を向上させることができます。特に新規アプリ開発ではiOS 15以上をターゲットにする場合、taskモディファイアを活用することで、より簡潔で保守性の高いコードを書くことができます。

参考リンク

GitHubで編集を提案

Discussion