MENU

【SwiftUI入門】.task修飾子とは?非同期処理の使い方を初心者向けに徹底解説

SwiftUIでアプリ開発を行う際、API呼び出しやデータ取得などの非同期処理は避けて通れません。この記事では、iOS 15以降で使える.task修飾子について、初心者にもわかりやすく実例を交えて解説します。

目次

.task修飾子とは?

.taskは、SwiftUIのビュー修飾子の一つで、ビューが表示されたときに非同期処理を自動的に実行する機能です。iOS 15以降で利用できます。

基本的な構文

.task {
    // ビューが表示されたときに実行される非同期処理
    await 非同期関数()
}

従来の.onAppearと比べて、非同期処理(async/await)を簡単に扱えるのが最大の特徴です。

なぜ.taskが必要なのか?

SwiftUIでデータを取得する場合、従来は以下のような書き方が必要でした。

従来の方法(.onAppear + Task)

struct OldWayView: View {
    @State private var data = ""
    
    var body: some View {
        Text(data)
            .onAppear {
                Task {
                    data = await fetchData()
                }
            }
    }
    
    func fetchData() async -> String {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return "データ読み込み完了"
    }
}

新しい方法(.task)

struct NewWayView: View {
    @State private var data = ""
    
    var body: some View {
        Text(data)
            .task {
                data = await fetchData()
            }
    }
    
    func fetchData() async -> String {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return "データ読み込み完了"
    }
}

.taskを使うことで、コードがシンプルになり、さらに自動的なタスクキャンセル機能も付いてきます。

.taskの基本的な使い方

例1:シンプルなデータ読み込み

最も基本的な使い方は、ビューが表示されたときにデータを読み込むことです。

import SwiftUI

struct ContentView: View {
    @State private var message = "読み込み中..."
    
    var body: some View {
        VStack {
            Text(message)
                .font(.title)
        }
        .task {
            // ビューが表示されたときに実行
            await loadMessage()
        }
    }
    
    func loadMessage() async {
        // 2秒待つ(ネットワーク処理をシミュレート)
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        message = "データ読み込み完了!"
    }
}

この例では、ビューが表示されると同時にloadMessage()が実行され、2秒後にメッセージが更新されます。

例2:APIからデータを取得する

実際のアプリでは、APIからデータを取得することが多いです。

struct UserProfileView: View {
    @State private var user: User?
    @State private var isLoading = true
    let userId: String
    
    var body: some View {
        VStack {
            if isLoading {
                ProgressView()
                    .scaleEffect(1.5)
            } else if let user = user {
                VStack(spacing: 16) {
                    Text(user.name)
                        .font(.title)
                    Text(user.email)
                        .foregroundColor(.gray)
                }
            } else {
                Text("ユーザーが見つかりません")
                    .foregroundColor(.red)
            }
        }
        .task {
            isLoading = true
            user = await fetchUser(id: userId)
            isLoading = false
        }
    }
    
    func fetchUser(id: String) async -> User? {
        // API呼び出しをシミュレート
        try? await Task.sleep(nanoseconds: 1_500_000_000)
        return User(name: "山田太郎", email: "taro@example.com")
    }
}

struct User {
    let name: String
    let email: String
}

.taskの強力な機能

1. 自動的なライフサイクル管理

.taskの最大の利点は、ビューが消えたときにタスクが自動的にキャンセルされることです。

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("カウント: \(count)")
                .font(.largeTitle)
        }
        .task {
            // 無限ループでカウントアップ
            while !Task.isCancelled {
                try? await Task.sleep(nanoseconds: 1_000_000_000)
                count += 1
            }
            // ビューが消えると自動的にキャンセルされ、ループが終了
        }
    }
}

重要:ビューから別の画面に遷移したり、ビューが非表示になると、実行中のタスクは自動的にキャンセルされます。メモリリークの心配がありません。

2. idパラメータで再実行を制御

idパラメータを指定すると、その値が変わったときだけタスクを再実行できます。

struct SearchView: View {
    @State private var searchText = ""
    @State private var results: [String] = []
    @State private var isSearching = false
    
    var body: some View {
        VStack {
            TextField("検索キーワードを入力", text: $searchText)
                .textFieldStyle(.roundedBorder)
                .padding()
            
            if isSearching {
                ProgressView()
            } else {
                List(results, id: \.self) { result in
                    Text(result)
                }
            }
        }
        .task(id: searchText) {
            // searchTextが変わるたびに実行される
            guard !searchText.isEmpty else {
                results = []
                return
            }
            
            isSearching = true
            
            // デバウンス:少し待ってから検索
            try? await Task.sleep(nanoseconds: 500_000_000)
            
            // 最新の検索テキストで検索
            results = await performSearch(query: searchText)
            
            isSearching = false
        }
    }
    
    func performSearch(query: String) async -> [String] {
        // 検索処理をシミュレート
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return [
            "\(query)に関する結果1",
            "\(query)に関する結果2",
            "\(query)に関する結果3"
        ]
    }
}

この例では、ユーザーが入力するたびに検索が実行されますが、0.5秒のデバウンスを設けることで、タイピング中は無駄な検索を防いでいます。

.taskと.onAppearの違い

両者の違いを理解することは重要です。

特徴.task.onAppear
非同期処理async/awaitが直接使えるTaskでラップする必要がある
自動キャンセルビューが消えると自動キャンセル手動でキャンセル処理が必要
コードの簡潔さシンプルやや冗長
iOS要件iOS 15以降すべてのバージョン
使いどころ非同期処理全般同期的な初期化処理

コード比較

struct ComparisonView: View {
    @State private var data1 = ""
    @State private var data2 = ""
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Task: \(data1)")
            Text("OnAppear: \(data2)")
        }
        // .taskを使う場合(推奨)
        .task {
            data1 = await fetchData(label: "Task")
        }
        
        // .onAppearを使う場合(古い方法)
        .onAppear {
            Task {
                data2 = await fetchData(label: "OnAppear")
            }
        }
    }
    
    func fetchData(label: String) async -> String {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return "\(label)のデータ"
    }
}

結論:非同期処理を行う場合は、.taskを使うのがベストプラクティスです。

実践例:APIからデータを取得する

実際のアプリケーションでよくあるパターンを見てみましょう。

例1:記事リストの表示

struct ArticleListView: View {
    @State private var articles: [Article] = []
    @State private var isLoading = true
    @State private var errorMessage: String?
    
    var body: some View {
        NavigationView {
            Group {
                if isLoading {
                    ProgressView("読み込み中...")
                } else if let error = errorMessage {
                    VStack(spacing: 16) {
                        Image(systemName: "exclamationmark.triangle")
                            .font(.system(size: 50))
                            .foregroundColor(.orange)
                        Text(error)
                            .foregroundColor(.red)
                    }
                } else {
                    List(articles) { article in
                        VStack(alignment: .leading, spacing: 8) {
                            Text(article.title)
                                .font(.headline)
                            Text(article.summary)
                                .font(.subheadline)
                                .foregroundColor(.gray)
                        }
                        .padding(.vertical, 4)
                    }
                }
            }
            .navigationTitle("記事一覧")
        }
        .task {
            await loadArticles()
        }
    }
    
    func loadArticles() async {
        isLoading = true
        errorMessage = nil
        
        do {
            // 実際のAPI呼び出し
            guard let url = URL(string: "https://api.example.com/articles") else {
                throw URLError(.badURL)
            }
            
            let (data, _) = try await URLSession.shared.data(from: url)
            articles = try JSONDecoder().decode([Article].self, from: data)
        } catch {
            errorMessage = "データの読み込みに失敗しました: \(error.localizedDescription)"
        }
        
        isLoading = false
    }
}

struct Article: Identifiable, Codable {
    let id: Int
    let title: String
    let summary: String
}

例2:リアルタイム更新

リアルタイムでデータを更新する場合も.taskが便利です。

struct LiveStockPriceView: View {
    @State private var price: Double = 0.0
    @State private var lastUpdated = Date()
    let stockSymbol: String
    
    var body: some View {
        VStack(spacing: 16) {
            Text(stockSymbol)
                .font(.title)
                .bold()
            
            Text("¥\(price, specifier: "%.2f")")
                .font(.system(size: 48, weight: .bold))
                .foregroundColor(price > 0 ? .green : .primary)
            
            Text("最終更新: \(lastUpdated, style: .time)")
                .font(.caption)
                .foregroundColor(.gray)
        }
        .padding()
        .task {
            // 5秒ごとに価格を更新
            while !Task.isCancelled {
                price = await fetchStockPrice(symbol: stockSymbol)
                lastUpdated = Date()
                
                try? await Task.sleep(nanoseconds: 5_000_000_000)
            }
        }
    }
    
    func fetchStockPrice(symbol: String) async -> Double {
        // API呼び出しをシミュレート
        try? await Task.sleep(nanoseconds: 500_000_000)
        return Double.random(in: 1000...2000)
    }
}

例3:複数のタスクを並行実行

1つのビューで複数のデータソースから情報を取得する場合です。

struct DashboardView: View {
    @State private var userData: User?
    @State private var notifications: [Notification] = []
    @State private var stats: DashboardStats?
    @State private var isLoading = true
    
    var body: some View {
        ScrollView {
            VStack(spacing: 24) {
                if isLoading {
                    ProgressView("ダッシュボードを読み込み中...")
                        .scaleEffect(1.5)
                } else {
                    // ユーザー情報
                    if let user = userData {
                        VStack(alignment: .leading, spacing: 8) {
                            Text("ようこそ、\(user.name)さん")
                                .font(.title)
                            Text(user.email)
                                .foregroundColor(.gray)
                        }
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding()
                        .background(Color.blue.opacity(0.1))
                        .cornerRadius(12)
                    }
                    
                    // 通知
                    HStack {
                        Image(systemName: "bell.fill")
                        Text("通知: \(notifications.count)件")
                            .font(.headline)
                    }
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.orange.opacity(0.1))
                    .cornerRadius(12)
                    
                    // 統計情報
                    if let stats = stats {
                        VStack(spacing: 12) {
                            Text("今月の統計")
                                .font(.headline)
                            HStack(spacing: 32) {
                                StatItem(title: "投稿", value: "\(stats.posts)")
                                StatItem(title: "いいね", value: "\(stats.likes)")
                                StatItem(title: "フォロワー", value: "\(stats.followers)")
                            }
                        }
                        .padding()
                        .background(Color.green.opacity(0.1))
                        .cornerRadius(12)
                    }
                }
            }
            .padding()
        }
        // 複数のタスクを並行実行
        .task {
            await loadDashboardData()
        }
    }
    
    func loadDashboardData() async {
        isLoading = true
        
        // 並行して3つのAPIを呼び出す
        async let user = fetchUser()
        async let notifs = fetchNotifications()
        async let statistics = fetchStats()
        
        // すべての結果を待つ
        userData = await user
        notifications = await notifs
        stats = await statistics
        
        isLoading = false
    }
    
    func fetchUser() async -> User {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return User(name: "山田太郎", email: "taro@example.com")
    }
    
    func fetchNotifications() async -> [Notification] {
        try? await Task.sleep(nanoseconds: 800_000_000)
        return [
            Notification(message: "新しいコメント"),
            Notification(message: "フォローされました"),
            Notification(message: "投稿がシェアされました")
        ]
    }
    
    func fetchStats() async -> DashboardStats {
        try? await Task.sleep(nanoseconds: 1_200_000_000)
        return DashboardStats(posts: 42, likes: 328, followers: 156)
    }
}

struct StatItem: View {
    let title: String
    let value: String
    
    var body: some View {
        VStack(spacing: 4) {
            Text(value)
                .font(.title2)
                .bold()
            Text(title)
                .font(.caption)
                .foregroundColor(.gray)
        }
    }
}

struct Notification: Identifiable {
    let id = UUID()
    let message: String
}

struct DashboardStats {
    let posts: Int
    let likes: Int
    let followers: Int
}

エラーハンドリング

非同期処理では、エラー処理が重要です。.task内でエラーを適切に処理しましょう。

struct DataView: View {
    @State private var data: String?
    @State private var errorMessage: String?
    @State private var isLoading = true
    
    var body: some View {
        VStack(spacing: 20) {
            if isLoading {
                ProgressView()
                    .scaleEffect(1.5)
            } else if let error = errorMessage {
                VStack(spacing: 12) {
                    Image(systemName: "xmark.circle.fill")
                        .font(.system(size: 50))
                        .foregroundColor(.red)
                    Text("エラーが発生しました")
                        .font(.headline)
                    Text(error)
                        .font(.caption)
                        .foregroundColor(.gray)
                        .multilineTextAlignment(.center)
                    
                    Button("再試行") {
                        Task {
                            await loadData()
                        }
                    }
                    .buttonStyle(.borderedProminent)
                }
                .padding()
            } else if let data = data {
                Text(data)
                    .font(.title2)
                    .padding()
            }
        }
        .task {
            await loadData()
        }
    }
    
    func loadData() async {
        isLoading = true
        errorMessage = nil
        data = nil
        
        do {
            data = try await fetchDataWithError()
        } catch let error as URLError {
            switch error.code {
            case .notConnectedToInternet:
                errorMessage = "インターネット接続がありません"
            case .timedOut:
                errorMessage = "接続がタイムアウトしました"
            default:
                errorMessage = "ネットワークエラーが発生しました"
            }
        } catch {
            errorMessage = "予期しないエラー: \(error.localizedDescription)"
        }
        
        isLoading = false
    }
    
    func fetchDataWithError() async throws -> String {
        try await Task.sleep(nanoseconds: 2_000_000_000)
        
        // エラーをランダムにシミュレート
        if Bool.random() {
            throw URLError(.notConnectedToInternet)
        }
        
        return "データ読み込み成功!"
    }
}

.taskを使う際のベストプラクティス

1. ローディング状態を管理する

ユーザーに処理中であることを明示しましょう。

struct BestPracticeView: View {
    @State private var data: String?
    @State private var isLoading = false
    
    var body: some View {
        VStack {
            if isLoading {
                ProgressView("読み込み中...")
            } else if let data = data {
                Text(data)
            }
        }
        .task {
            isLoading = true
            data = await fetchData()
            isLoading = false
        }
    }
    
    func fetchData() async -> String {
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        return "完了"
    }
}

2. キャンセルをチェックする

長時間実行されるタスクでは、キャンセルをチェックしましょう。

.task {
    for i in 1...100 {
        // キャンセルされたら処理を中断
        if Task.isCancelled {
            break
        }
        
        // 処理を実行
        await processItem(i)
    }
}

3. 適切なエラーハンドリング

エラーは必ず処理し、ユーザーにフィードバックを提供しましょう。

.task {
    do {
        data = try await fetchData()
    } catch {
        errorMessage = error.localizedDescription
        // ログに記録
        print("Error: \(error)")
    }
}

4. デバウンスを実装する

頻繁に実行されるタスクには、デバウンスを設けましょう。

.task(id: searchText) {
    guard !searchText.isEmpty else { return }
    
    // 入力が落ち着くまで待つ
    try? await Task.sleep(nanoseconds: 500_000_000)
    
    results = await search(query: searchText)
}

.taskの制限と注意点

1. iOS 15以降が必要

.taskはiOS 15以降でのみ利用可能です。それ以前のバージョンをサポートする場合は、.onAppearTaskを組み合わせて使用する必要があります。

// iOS 15未満もサポートする場合
.onAppear {
    Task {
        await loadData()
    }
}

2. ビューの再描画に注意

ビューが再描画されるたびに.taskが実行されるわけではありません。idパラメータを指定しない限り、ビューが最初に表示されたときだけ実行されます。

3. メインスレッドでの実行

.task内のコードは、UI更新を含む場合、自動的にメインスレッドで実行されます。重い処理は別のスレッドで実行しましょう。

.task {
    // バックグラウンドで重い処理を実行
    let result = await Task.detached {
        // 重い計算処理
        return heavyComputation()
    }.value
    
    // UI更新はメインスレッドで
    data = result
}

まとめ:.taskをマスターしよう

この記事では、SwiftUIの.task修飾子について詳しく解説しました。

重要ポイントの復習

  1. 基本機能
    • ビュー表示時に非同期処理を実行
    • async/awaitが直接使える
    • iOS 15以降で利用可能
  2. 自動管理機能
    • ビューが消えるとタスクが自動キャンセル
    • メモリリークの心配がない
    • 手動でのキャンセル処理が不要
  3. idパラメータ
    • 値が変わると自動的に再実行
    • 検索機能などで便利
    • デバウンスと組み合わせて効率化
  4. .onAppearとの違い
    • コードがシンプル
    • 自動キャンセル機能付き
    • 非同期処理に最適
  5. 実践的な使い方
    • API呼び出し
    • リアルタイム更新
    • 複数のタスクの並行実行
    • エラーハンドリング

ベストプラクティス

  • ローディング状態を適切に管理
  • エラーハンドリングを忘れずに
  • 長時間実行タスクではキャンセルをチェック
  • 頻繁に実行されるタスクにはデバウンスを実装

.task修飾子は、SwiftUIで非同期処理を扱う際の標準的な方法です。API呼び出しやデータ取得など、現代のアプリ開発に欠かせない機能を簡単に実装できます。

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次