⭐️ はじめに
Swiftのasync/awaitはこれまで何度か使ったことがありましたが、正直に仕組みや原理をしっかり理解しないまま使っていました。
今回、改めてその基本や動作の仕組みをきちんと整理し、自分の理解を深めるためにこの記事を書くことにしました。
これから同じように非同期処理について学びたい方の参考になれば幸いです。
詳細に入る前に、同期処理と非同期処理について簡単に説明います。
🔄 同期処理(Synchronous)とは?
同期処理とは、処理が上から順番に一つずつ実行される方式です。
ある処理が終わるまで次の処理は始まりません。つまり、処理が順番に“列に並んで”待っているようなイメージです。
たとえば、ブタ丼を作るとしましょう。
- 炊飯器でご飯を炊く(約30分)
- ご飯が炊き終わるまで何もせず待つ
- ご飯が終わると、野菜を切る
- その後、豚肉を焼く
- 最後に盛り付けて完成!
このように、すべての工程が「終わるのを待ってから次へ」進むため、全体に時間がかかってしまいます。
開発現場に置き換えると?
APIを使ったデータ取得の場面で考えてみましょう。
もし同期処理でAPIを呼び出すと、レスポンスが返ってくるまでUIは何もできず、ずっと待たされます。
ユーザーからすると、
- 画面が固まっている
- ボタンを押しても反応しない
- アプリが遅い・重い
という感じで、ストレスの多い体験になります。
🔁 非同期処理(Asynchronous)とは?
非同期処理では、時間がかかる作業を「終わるまで待たずに」他の作業を進められます。
非同期処理で豚丼を作ってみましょう。
- ご飯を炊飯器にセットしてスイッチオン(あとは自動で炊ける)
- ご飯が炊けるのを待っている間に、野菜を切る
- そのあと、豚肉を焼く
- ご飯が炊き上がったら、すぐに盛り付けて完成!
炊飯という時間のかかる作業を「待たずに、他の準備を進める」ことで、全体の時間を短縮でき、効率的に作業できます。
🚀 開発現場でのメリット
非同期でAPIを呼び出すと、その間に他の処理(UI表示、アニメーション、次のAPI呼び出しなど)を同時に進められます。
その結果
- アプリの性能が良くなる
- ユーザーは「待たされている」感覚を感じにくい
- 全体の処理効率が上がる
上記の同期と非同期の性能を比較するため、簡単なコードを実装してみました。
同期の実行画面を見る
import SwiftUI
struct ContentView: View {
@State private var counter = 0
@State private var image: UIImage? = nil
var body: some View {
VStack(spacing: 20) {
Text("Count: \(counter)")
.font(.largeTitle)
Button("+") {
counter += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Circle())
if let uiImage = image {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.frame(height: 200)
} else {
Text("No image")
.foregroundColor(.gray)
}
Button("画像を同期で読み込む") {
loadImageSynchronously()
}
.padding()
.background(Color.red)
.foregroundColor(.white)
}
.padding()
.onAppear {
loadImageSynchronously()
}
}
func loadImageSynchronously() {
let url = URL(string: "https://picsum.photos/4000/4000")!
if let data = try? Data(contentsOf: url),
let uiImage = UIImage(data: data) {
self.image = uiImage
}
}
}
画像の読み込み処理が同期で行われているため、画像を読み込んでいる間に「+」ボタンを押しても、UIが反応せず、カウントがすぐ更新されないことが確認できます。
非同期の実行画面を見る
import SwiftUI
struct ContentView: View {
@State private var counter = 0
@State private var image: UIImage? = nil
var body: some View {
VStack(spacing: 20) {
Text("Count: \(counter)")
.font(.largeTitle)
Button("+") {
counter += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Circle())
if let uiImage = image {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.frame(height: 200)
} else {
Text("No image")
.foregroundColor(.gray)
}
Button("画像を同期で読み込む") {
Task {
try await loadImageAsynchronously()
}
}
.padding()
.background(Color.red)
.foregroundColor(.white)
}
.padding()
.onAppear {
Task {
try await loadImageAsynchronously()
}
}
}
func loadImageAsynchronously() async throws {
let url = URL(string: "https://picsum.photos/4000/4000")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let uiImage = UIImage(data: data) {
self.image = uiImage
}
} catch {
print(error)
}
}
}
画像の読み込み処理が非同期で行われているため、画像を読み込んでいる間に「+」ボタンを押すと、カウントがすぐ更新されることが確認できます。
👨💻 詳細
Swift Concurrency(async/await)が登場した背景
Swiftでは、非同期処理に主に以下の方法が使われてきました。
- DispatchQueue.global().async などGCD(Grand Central Dispatch)
- クロージャー(completionHandler)
しかし、これらの方法にはいくつかの課題がありました。
- コールバック地獄
非同期処理がネストされ、コードが読みにくくなります。
getUser { user in
getPosts(user) { posts in
getComments(posts) { comments in
// ...
}
}
}
-
エラーハンドリングが複雑
do-catch が使えず、if let や Result を多用する必要があります。 -
処理の流れが追いにくい
処理の順序が直感的でなく、可読性と保守性が下がります。
Swift Concurrency(async/await)の登場
Swift5.5で導入された「async/await」により同期処理のように非同期処理が書けます。
- エラーハンドリングが簡単(try / catch)
- コードが読みやすく、保守しやすい
- Task, Actor でスレッド安全性も向上
上記のコード(コメントを取得する)を「async/await」を使ったコードに置き換えます。
do {
let user = try await getUser()
let posts = try await getPosts(for: user)
let comments = try await getComments(for: posts)
} catch {
print("エラーが発生しました: \(error)")
}
ネストがなくなり、コードが直感的で読みやすくなっているのがわかります。
🌟 Swift Concurrencyの使い方の簡単な説明やコード
次のキーワードを中心に、簡単なコードで使い方を説明していきます。
- async / await
- Task
- throws / try
async / await
まず、「async」キーワードは「非同期関数であること」を示します。
「await」はその非同期関数の「完了を待つ」ために使います。
以下のコードは、Firebaseのデータベースからカフェのレビューを取得するコードです。
// ViewModel.swift
func fetchPlaceReview(id placeID: String) async {
let db = Firestore.firestore()
do {
let snapshot = try await db
.collection("CafeReviews")
.document(placeID)
.collection("reviews")
.order(by: "createdAt", descending: true)
.getDocuments()
let reviews = try snapshot.documents.compactMap {
try $0.data(as: CafeReview.self)
}
self.cafeReviews = reviews
} catch {
print(error)
}
}
// View.swift
Task {
await viewModel.fetchPlaceReview(id: place.id)
showPlaceModal = true
}
上記のコードのshowPlaceModalはfetchPlaceReviewが完了した後に実行されます。
つまり、「await」を使うことでfetchPlaceReview関数が終了するまで待機するという意味になります。
Task
でわ上記のコードでTaskとは?
「Task」は、Swiftの非同期処理を始めるための「箱」のようなものです。
非同期の関数は、すぐに実行されるわけではなく、「await」を使って結果が返ってくるまで待つ必要があります。
しかし、普通のボタンの処理など、同期的なコードの中では「await」を直接使えません。
上記のコードはエラーが発生します。
上のコードのように 「Task」で囲むことで、非同期コードを実行できます。
簡単にこのコードの流れを説明すると(現在、ユーザはレビュー作成画面)
- ユーザーがレビューを書いて、「作成する」ボタンを押す
- saveReview()関数が実行される
- ユーザーが入力したレビューがデータベースに保存されるまで待機
- 保存が完了すると、fetchPlaceReview() 関数が呼ばれ、最新のレビューを取得する(「3」で保存したレビューも含む)
- レビュー取得が終わると、作成画面を閉じる(dismiss)
- ユーザはカフェのレビュー詳細画面で更新されたレビューを見ることができる
throws / try
throwsとは?
関数が「エラーを投げる可能性がある」ことを示します。
throwsが付いた関数を使うときは、エラーが発生するかもしれないので注意が必要です。
tryとは?
throwsを持つ関数を呼び出すときに使います。
「この関数はエラーを出すかもしれないから気をつけて使うよ」という意味です。
下記のコードは上で使用したレビューを取得する関数です。
この関数は「ViewModel」内に実装されており、エラー処理を「ViewModel」内で行う場合はこのまま使用しても問題ないです。
// ViewModel.swift
func fetchPlaceReview(id placeID: String) async {
let db = Firestore.firestore()
do {
let snapshot = try await db
.collection("CafeReviews")
.document(placeID)
.collection("reviews")
.order(by: "createdAt", descending: true)
.getDocuments()
let reviews = try snapshot.documents.compactMap {
try $0.data(as: CafeReview.self)
}
self.cafeReviews = reviews
} catch {
print(error)
}
}
// View.swift
Task {
await viewModel.fetchPlaceReview(id: place.id)
showPlaceModal = true
}
しかし、上記の関数のエラーを「View」で処理したいなら下記のコードように変更できます。
// ViewModel.swift
func fetchPlaceReview(id placeID: String) async throws {
let db = Firestore.firestore()
do {
let snapshot = try await db
.collection("CafeReviews")
.document(placeID)
.collection("reviews")
.order(by: "createdAt", descending: true)
.getDocuments()
let reviews = try snapshot.documents.compactMap {
try $0.data(as: CafeReview.self)
}
self.cafeReviews = reviews
}
}
// View.swift
Task {
do {
try await viewModel.fetchPlaceReview(id: place.id)
showPlaceModal = true
} catch {
errorMessage = "DBエラーが発生しました"
}
}
エラーをどう処理するかによって、コードの作成の方法が変わることがあります。
💬 まとめ
該当の記事では、同期・非同期処理や Swift Concurrency について簡単に紹介しました。
もっと難しくて奥深い内容もたくさんありますが、また機会があれば勉強して記事にしてみようと思います。
どなたかの参考になれば幸いです!