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以降でのみ利用可能です。それ以前のバージョンをサポートする場合は、.onAppear
とTask
を組み合わせて使用する必要があります。
// 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
修飾子について詳しく解説しました。
重要ポイントの復習
- 基本機能
- ビュー表示時に非同期処理を実行
- async/awaitが直接使える
- iOS 15以降で利用可能
- 自動管理機能
- ビューが消えるとタスクが自動キャンセル
- メモリリークの心配がない
- 手動でのキャンセル処理が不要
- idパラメータ
- 値が変わると自動的に再実行
- 検索機能などで便利
- デバウンスと組み合わせて効率化
- .onAppearとの違い
- コードがシンプル
- 自動キャンセル機能付き
- 非同期処理に最適
- 実践的な使い方
- API呼び出し
- リアルタイム更新
- 複数のタスクの並行実行
- エラーハンドリング
ベストプラクティス
- ローディング状態を適切に管理
- エラーハンドリングを忘れずに
- 長時間実行タスクではキャンセルをチェック
- 頻繁に実行されるタスクにはデバウンスを実装
.task
修飾子は、SwiftUIで非同期処理を扱う際の標準的な方法です。API呼び出しやデータ取得など、現代のアプリ開発に欠かせない機能を簡単に実装できます。