取り組みの背景 — Swift Concurrency が変えたiOS開発の世界
iOS開発における非同期処理は、長らく「難しい」というイメージがつきまとってきました。コールバック地獄(Callback Hell)、DispatchQueue の複雑なスレッド管理、completion handler の入れ子構造——これらは多くのiOS開発者を悩ませてきた問題です。
Swift 5.5でAppleが導入したSwift Concurrency は、この状況を根本から変えました。async/await による直感的な非同期コード、Actor によるデータ競合の防止、Structured Concurrency によるライフサイクル管理——これらが組み合わさることで、安全で読みやすい非同期処理が実現します。
しかし、Swift Concurrencyには学習コストが伴います。@MainActor や @Sendable、Task グループ、AsyncSequence——概念が多く、どこから手をつけるべきか迷う方も少なくありません。
そこで活躍するのがAntigravity です。AntigravityのAIエージェントは、Swift Concurrencyのボイラープレートを瞬時に生成し、設計の相談に乗り、バグの原因を特定してくれます。ここではAntigravityを使いながらSwift Concurrencyを体系的にマスターする方法を、実践的なコード例と、私自身が本番アプリで踏んだ失敗を交えて整理します。
Swift Concurrency 全体像 — 3つの柱
Swift Concurrencyを理解するには、3つの中核概念を把握する点が肝心です。
① async/await : 非同期処理を同期的なコードスタイルで書けるようにするシンタックス。コールバック地獄を解消し、可読性を劇的に向上させます。
② Actor : 内部状態へのアクセスをシリアライズすることでデータ競合を防ぐ参照型。class に似ていますが、コンパイラが並行アクセスを自動的に防いでくれます。
③ Structured Concurrency : Task とその子タスクのライフサイクルを木構造で管理する仕組み。タスクのキャンセル・エラー伝播が自動化されます。
Antigravityでこの全体像を素早く把握するには、AIエージェントに以下のように質問するのが効果的です。
# Antigravity への依頼例
「Swift Concurrencyのasync/await、Actor、Structured Concurrencyの
関係性を説明しながら、iOSアプリにおける典型的なユースケースをコード例付きで示してください」
async/await 徹底解説 — コールバックからの移行
従来のコールバック vs async/await
まず、従来のコールバックスタイルとasync/awaitの違いを見てみましょう。
// ❌ 旧来のコールバックスタイル(読みにくく、エラー処理も複雑)
func fetchUser ( id : String , completion : @escaping (Result<User, Error >) -> Void ) {
URLSession.shared. dataTask ( with : userURL ( id : id)) { data, response, error in
if let error = error {
completion (. failure (error))
return
}
guard let data = data else {
completion (. failure (NetworkError.noData))
return
}
do {
let user = try JSONDecoder (). decode (User. self , from : data)
completion (. success (user))
} catch {
completion (. failure (error))
}
}. resume ()
}
// ✅ async/await スタイル(直感的で読みやすい)
func fetchUser ( id : String ) async throws -> User {
let (data, _ ) = try await URLSession.shared. data ( from : userURL ( id : id))
return try JSONDecoder (). decode (User. self , from : data)
}
async/awaitスタイルは、エラー処理を throws に統一でき、コードの流れが上から下へ自然に読めます。
複数の非同期処理を組み合わせる
複数のAPIを呼ぶケースでasync/awaitの真価が発揮されます。
// ユーザー情報と投稿一覧を取得して画面に表示
func loadUserProfile ( userId : String ) async throws -> UserProfile {
// 順次実行(依存関係がある場合)
let user = try await fetchUser ( id : userId)
let posts = try await fetchPosts ( authorId : user.id)
let followers = try await fetchFollowers ( userId : user.id)
return UserProfile ( user : user, posts : posts, followers : followers)
}
この処理は順次実行なので、3つのAPIコールが直列に行われます。もし並列実行できるなら、async let を使いましょう(詳しくは後述)。
Antigravity でのコールバック→async/await 移行
既存コードをasync/awaitに移行する際、Antigravityが大きな力を発揮します。
# Antigravity への依頼例
「以下のコールバックベースのコードをasync/awaitに移行してください。
エラー処理とキャンセル対応も含めて」
[コードを貼り付ける]
Antigravityは withCheckedThrowingContinuation や withTaskCancellationHandler を適切に使ったブリッジコードを自動生成してくれます。
// コールバックAPIをasync/awaitでブリッジ
func fetchLegacyData () async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
legacyAPI. fetchData { result in
continuation. resume ( with : result)
}
}
}
Actor モデル — データ競合をコンパイル時に防ぐ
なぜ Actor が必要か
複数のスレッドから同じ可変状態にアクセスすると、データ競合(Data Race)が発生します。これはクラッシュや予期しない動作の原因になりますが、再現が難しくデバッグが困難です。
Swift Concurrencyの actor は、内部の可変状態へのアクセスを自動的にシリアライズし、データ競合をコンパイル時に防ぎます。
// ✅ Actorを使ったキャッシュ実装(データ競合が発生しない)
actor ImageCache {
private var cache: [URL: UIImage] = [ : ]
func image ( for url: URL) -> UIImage ? {
return cache[url]
}
func store ( _ image: UIImage, for url: URL) {
cache[url] = image
}
func removeAll () {
cache. removeAll ()
}
}
// 使用側
let cache = ImageCache ()
// Actor のメソッドはawaitが必要(外部からは非同期アクセス)
func loadImage ( from url: URL) async -> UIImage {
// キャッシュをチェック
if let cached = await cache. image ( for : url) {
return cached
}
// ネットワークから取得
let (data, _ ) = try! await URLSession.shared. data ( from : url)
let image = UIImage ( data : data) !
// キャッシュに保存
await cache. store (image, for : url)
return image
}
nonisolated と isolated
Actor内のメソッドは、デフォルトで Actor の isolation(隔離)に属します。しかし、Actor の状態に依存しない純粋な計算は nonisolated にすることで、caller のコンテキストで直接実行できます。
actor UserRepository {
private var users: [ String : User] = [ : ]
// Actor isolation: awaitが必要
func findUser ( id : String ) -> User ? {
return users[id]
}
// nonisolated: awaitなしで呼べる(Actorの状態に依存しない)
nonisolated func validateEmail ( _ email: String ) -> Bool {
let emailRegex = #"^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"#
return email. range ( of : emailRegex, options : .regularExpression) != nil
}
}
@MainActor — UIスレッドの保証
@MainActor はメインスレッド(UIスレッド)での実行を保証する特別なグローバルActorです。SwiftUIやUIKitの更新は必ずメインスレッドで行う必要があるため、View Modelに@MainActorを付与することが一般的なパターンです。
@MainActor
class ArticleViewModel : ObservableObject {
@Published var articles: [Article] = []
@Published var isLoading = false
@Published var errorMessage: String ?
private let repository: ArticleRepository
init ( repository : ArticleRepository) {
self .repository = repository
}
func loadArticles () async {
isLoading = true
errorMessage = nil
do {
// repository.fetchAll() は非Actorコンテキストで実行される可能性がある
let fetchedArticles = try await repository. fetchAll ()
// @MainActorのため、このプロパティ更新はメインスレッドで行われる
articles = fetchedArticles
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
Antigravityに「SwiftUI用のViewModel設計」を依頼すると、@MainActor の適切な使い方を含む定型パターンを自動生成してくれます。
Structured Concurrency — タスクの生成と管理
Task の基本
Task は Swift Concurrencyの並行処理の基本単位です。新しい非同期コンテキストを生成します。
// Task の基本的な使い方
struct ContentView : View {
@State private var imageData: Data ?
var body: some View {
Group {
if let data = imageData {
Image ( uiImage : UIImage ( data : data) ! )
} else {
ProgressView ()
}
}
. task {
// .task modifier: Viewが表示されたら開始、非表示で自動キャンセル
do {
let (data, _ ) = try await URLSession.shared. data ( from : imageURL)
imageData = data
} catch {
print ( "Error: \( error ) " )
}
}
}
}
SwiftUIの .task モディファイアは、Viewのライフサイクルと紐づいた Task を生成するため、画面遷移時に自動的にキャンセルされます。
async let — 並列実行の基本
依存関係のない複数の非同期処理を並列実行するには async let を使います。
func loadDashboard () async throws -> Dashboard {
// 3つのAPIを並列実行(順次実行の約3倍速)
async let userProfile = fetchUserProfile ()
async let recentArticles = fetchRecentArticles ()
async let notifications = fetchNotifications ()
// 全て完了するまで待機
return Dashboard (
user : try await userProfile,
articles : try await recentArticles,
notifications : try await notifications
)
}
async let で宣言した後は、実際に await するまで処理が開始されません。await した瞬間に並列実行が始まります。
TaskGroup — 動的な並列処理
async let はコンパイル時に件数が決まっている場合のみ使えます。配列の各要素を並列処理するような動的なケースでは TaskGroup を使います。
func fetchAllAvatars ( userIds : [ String ]) async throws -> [ String : UIImage] {
try await withThrowingTaskGroup ( of : ( String , UIImage). self ) { group in
// 各ユーザーのアバター取得タスクを追加
for userId in userIds {
group. addTask {
let image = try await fetchAvatar ( userId : userId)
return (userId, image)
}
}
// 全タスクの結果を収集
var avatars: [ String : UIImage] = [ : ]
for try await (userId, image) in group {
avatars[userId] = image
}
return avatars
}
}
Antigravityに「100件のAPIを並列処理したい」と伝えると、適切なコンカレンシー制限付きの TaskGroup パターンも生成してくれます。
// 並列数を制限したTaskGroup(同時に最大5件)
func fetchWithConcurrencyLimit < T >(
items : [T],
maxConcurrency : Int = 5 ,
operation : @Sendable @escaping (T) async throws -> Void
) async throws {
try await withThrowingTaskGroup ( of : Void . self ) { group in
var activeTasks = 0
var iterator = items. makeIterator ()
// 最初のバッチを開始
while activeTasks < maxConcurrency, let item = iterator. next () {
group. addTask { try await operation (item) }
activeTasks += 1
}
// 完了ごとに次のタスクを追加
for try await _ in group {
activeTasks -= 1
if let item = iterator. next () {
group. addTask { try await operation (item) }
activeTasks += 1
}
}
}
}
AsyncStream と AsyncSequence — リアルタイムデータ処理
AsyncStream の活用
AsyncStream は、Combineの Publisher に似た概念ですが、Swift Concurrencyネイティブです。WebSocketの受信データ、位置情報の更新、センサーデータなど、時系列で流れてくるデータの処理に適しています。
// WebSocketからのメッセージをAsyncStreamで処理
class WebSocketManager {
private var webSocket: URLSessionWebSocketTask ?
func messageStream ( url : URL) -> AsyncStream< String > {
AsyncStream { continuation in
let session = URLSession ( configuration : .default)
let webSocket = session. webSocketTask ( with : url)
self .webSocket = webSocket
// メッセージ受信ループ
Task {
webSocket. resume ()
while ! Task.isCancelled {
do {
let message = try await webSocket. receive ()
switch message {
case . string ( let text) :
continuation. yield (text)
case . data ( let data) :
if let text = String ( data : data, encoding : . utf8 ) {
continuation. yield (text)
}
@unknown default:
break
}
} catch {
continuation. finish ()
break
}
}
}
// キャンセル時のクリーンアップ
continuation.onTermination = { _ in
webSocket. cancel ()
}
}
}
}
// 使用側
let manager = WebSocketManager ()
let stream = manager. messageStream ( url : wsURL)
for await message in stream {
print ( "受信: \( message ) " )
}
CLLocationManager を AsyncStream に変換
既存のデリゲートベースのAPIをAsyncStreamにブリッジするパターンは非常に一般的です。
// 位置情報をAsyncStreamとして取得
extension CLLocationManager {
var locationStream: AsyncStream<CLLocation> {
AsyncStream { continuation in
let delegate = LocationDelegate ( continuation : continuation)
self .delegate = delegate
self . startUpdatingLocation ()
continuation.onTermination = { [ weak self ] _ in
self ? . stopUpdatingLocation ()
}
}
}
}
private class LocationDelegate : NSObject , CLLocationManagerDelegate {
let continuation: AsyncStream<CLLocation>.Continuation
init ( continuation : AsyncStream<CLLocation>.Continuation) {
self .continuation = continuation
}
func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for location in locations {
continuation. yield (location)
}
}
func locationManager ( _ manager: CLLocationManager, didFailWithError error: Error ) {
continuation. finish ()
}
}
// 使用例
let locationManager = CLLocationManager ()
for await location in locationManager.locationStream {
print ( "現在地: \( location. coordinate ) " )
}
Antigravityに「CLLocationManagerをSwift Concurrencyに対応させたい」と伝えるだけで、上記のようなブリッジパターンを自動生成してくれます。
@Sendable と データ競合防止
@Sendable の意味
@Sendable は、クロージャやクラスが複数のコンカレンシードメイン間を安全に移動できることをコンパイラに伝えるマーカーです。
// @Sendable クロージャ(スレッド安全でなければならない)
func processInBackground < T : Sendable >(
value : T,
transform : @Sendable @escaping (T) async throws -> T
) async throws -> T {
try await Task. detached ( priority : .background) {
try await transform (value)
}. value
}
// Sendable に準拠したデータ構造(値型は自動的にSendable)
struct ArticleMetadata : Sendable {
let id: String
let title: String
let publishedAt: Date
}
// クラスでSendableを実現(全プロパティが定数かつSendableである必要がある)
final class ImmutableConfig : Sendable {
let apiEndpoint: URL
let apiKey: String
init ( apiEndpoint : URL, apiKey : String ) {
self .apiEndpoint = apiEndpoint
self .apiKey = apiKey
}
}
完全なSwift 6対応(Strict Concurrency Checking)
Swift 6では、データ競合がコンパイルエラーになります。段階的に移行するために、Xcode の SWIFT_STRICT_CONCURRENCY ビルド設定を使います。
// Package.swift での設定
. target (
name : "MyApp" ,
swiftSettings : [
// 段階的移行: minimal → targeted → complete
. swiftLanguageMode (.v6)
]
)
Antigravityに「このコードをSwift 6対応にしたい」と伝えると、@Sendable の追加、nonisolated の適切な使用、@unchecked Sendable の暫定的な使用など、移行作業を自動化してくれます。
テストとデバッグ戦略
非同期コードのXCTest
Swift Concurrencyのコードをテストするには、async テストメソッドを使います。
import XCTest
@testable import MyApp
final class ArticleServiceTests : XCTestCase {
var sut: ArticleService !
var mockRepository: MockArticleRepository !
@MainActor
override func setUp () async throws {
try await super . setUp ()
mockRepository = MockArticleRepository ()
sut = ArticleService ( repository : mockRepository)
}
func testFetchArticlesSuccess () async throws {
// Given
let expectedArticles = [
Article ( id : "1" , title : "テスト記事1" ),
Article ( id : "2" , title : "テスト記事2" )
]
mockRepository.articlesToReturn = expectedArticles
// When
let articles = try await sut. fetchArticles ()
// Then
XCTAssertEqual (articles. count , 2 )
XCTAssertEqual (articles[ 0 ].title, "テスト記事1" )
}
func testFetchArticlesWithTimeout () async throws {
// タイムアウトのテスト
mockRepository.simulateDelay = true
do {
_ = try await withTimeout ( seconds : 1.0 ) {
try await self .sut. fetchArticles ()
}
XCTFail ( "タイムアウトエラーが発生するべき" )
} catch is TimeoutError {
// 期待通り
}
}
}
// タイムアウトヘルパー関数
func withTimeout < T : Sendable >(
seconds : TimeInterval,
operation : @escaping @Sendable () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup ( of : T. self ) { group in
group. addTask { try await operation () }
group. addTask {
try await Task. sleep ( nanoseconds : UInt64 (seconds * 1_000_000_000 ))
throw TimeoutError ()
}
guard let result = try await group. next () else {
throw TimeoutError ()
}
group. cancelAll ()
return result
}
}
struct TimeoutError : Error {}
Actor でのテスト
Actorのテストは、await を使って通常通りに書けます。
final class ImageCacheTests : XCTestCase {
func testCacheStoreAndRetrieve () async {
let cache = ImageCache ()
let testURL = URL ( string : "https://example.com/image.png" ) !
let testImage = UIImage ( systemName : "star.fill" ) !
// 保存
await cache. store (testImage, for : testURL)
// 取得
let retrieved = await cache. image ( for : testURL)
XCTAssertNotNil (retrieved)
}
}
Instruments でのデバッグ
Xcode InstrumentsのSwift Concurrencyインストゥルメントを使うと、タスクの生成・完了・キャンセルを視覚的に確認できます。
Antigravityに「非同期処理のパフォーマンスボトルネックを調査したい」と伝えると、プロファイリングのポイントや改善策を提案してくれます。
よくあるエラーと対処法
エラー① "Expression is 'async' but is not marked with 'await'"
async 関数を await なしで呼ぼうとしたときに発生します。
// ❌ エラー
let user = fetchUser ( id : "123" ) // 'await' が必要
// ✅ 修正
let user = await fetchUser ( id : "123" ) // async コンテキスト内で
// または
let user = try await fetchUser ( id : "123" ) // throws も必要な場合
エラー② "Sending 'x' risks causing data races"
Swift 6のStrict Concurrency Checkingで発生するエラーです。Sendable でない値をアクター境界を越えて送ろうとしています。
// ❌ エラー(NSMutableArray は Sendable ではない)
actor DataStore {
var items: NSMutableArray = []
}
// ✅ 修正(Sendable な値型を使う)
actor DataStore {
var items: [ String ] = [] // Array<String> は Sendable
}
エラー③ タスクリーク(Task が解放されない)
長時間実行されるタスクを適切に管理しないと、メモリリークや予期しない動作が発生します。
// ❌ タスクがキャンセルされない
class ViewModel {
func startPolling () {
Task { // このTaskはViewModelが解放されても続く
while true {
await refresh ()
try await Task. sleep ( nanoseconds : 5_000_000_000 )
}
}
}
}
// ✅ タスクを保持してキャンセル可能にする
class ViewModel {
private var pollingTask: Task< Void , Never > ?
func startPolling () {
pollingTask = Task {
while ! Task.isCancelled {
await refresh ()
try? await Task. sleep ( nanoseconds : 5_000_000_000 )
}
}
}
func stopPolling () {
pollingTask ? . cancel ()
pollingTask = nil
}
deinit {
pollingTask ? . cancel ()
}
}
本番アプリへの実践的な適用パターン
Repository パターン × Swift Concurrency
// プロトコル定義(テスト容易性のため)
protocol ArticleRepositoryProtocol : Sendable {
func fetchAll () async throws -> [Article]
func fetchById ( _ id: String ) async throws -> Article
func save ( _ article: Article) async throws
}
// 本番実装
actor ArticleRepository : ArticleRepositoryProtocol {
private let apiClient: APIClient
private let localStorage: LocalStorageProtocol
private var cache: [ String : Article] = [ : ]
init ( apiClient : APIClient, localStorage : LocalStorageProtocol) {
self .apiClient = apiClient
self .localStorage = localStorage
}
func fetchAll () async throws -> [Article] {
// まずローカルストレージから
let localArticles = try await localStorage. fetchArticles ()
if ! localArticles. isEmpty {
return localArticles
}
// なければAPIから取得
let remoteArticles = try await apiClient. get ( "/articles" , responseType : [Article]. self )
// ローカルに保存
try await localStorage. saveArticles (remoteArticles)
return remoteArticles
}
func fetchById ( _ id: String ) async throws -> Article {
// キャッシュ確認
if let cached = cache[id] {
return cached
}
let article = try await apiClient. get ( "/articles/ \( id ) " , responseType : Article. self )
cache[id] = article
return article
}
func save ( _ article: Article) async throws {
try await apiClient. post ( "/articles" , body : article)
cache[article.id] = article
try await localStorage. saveArticle (article)
}
}
このパターンをAntigravityに「Repository パターンで非同期データアクセス層を実装して」と伝えると、プロジェクトの既存構造に合わせた実装が自動生成されます。
エラー伝播と回復戦略
enum AppError : Error , LocalizedError {
case networkUnavailable
case serverError (statusCode: Int , message: String )
case decodingError (type: String )
case unauthorized
case notFound (id: String )
var errorDescription: String ? {
switch self {
case .networkUnavailable :
return "ネットワーク接続を確認してください"
case . serverError ( let code, let message) :
return "サーバーエラー( \( code ) ): \( message ) "
case . decodingError ( let type) :
return " \( type ) のデコードに失敗しました"
case .unauthorized :
return "ログインが必要です"
case . notFound ( let id) :
return "データが見つかりません: \( id ) "
}
}
}
// 再試行機能付きの非同期実行
func withRetry < T >(
maxAttempts : Int = 3 ,
delay : Duration = . seconds ( 1 ),
operation : @escaping () async throws -> T
) async throws -> T {
var lastError: Error ?
for attempt in 0 ..< maxAttempts {
do {
return try await operation ()
} catch {
lastError = error
// 再試行不可能なエラーは即座に投げる
if let appError = error as? AppError, case .unauthorized = appError {
throw error
}
// 最後の試行以外は待機
if attempt < maxAttempts - 1 {
try await Task. sleep ( for : delay * Double (attempt + 1 ))
}
}
}
throw lastError !
}
公式ドキュメントには書かれていない Actor 再入の落とし穴
ここからは、私が実際に運用しているアプリで Swift Concurrency を本番投入したときに踏んだ、ドキュメントだけ読んでいては気づきにくい問題を共有します。2014年から個人でアプリを作り続け、現在も壁紙・癒し系を中心に運用しているのですが、その更新作業で actor を使い始めた頃に厄介なバグに悩まされました。
actor は「データ競合を防いでくれる」と説明されます。これは正確なのですが、await を挟んだ瞬間にそのアクターは他のタスクの呼び出しを受け付ける という挙動(再入・reentrancy)が抜け落ちがちです。次のコードは、一見すると安全に見えて状態が壊れます。
// ❌ 再入で重複ダウンロードが走るキャッシュ
actor ThumbnailCache {
private var cache: [URL: UIImage] = [ : ]
func image ( for url: URL) async throws -> UIImage {
if let cached = cache[url] {
return cached
}
// ここで await すると、同じ url の別呼び出しがこの間に入り込む
let (data, _ ) = try await URLSession.shared. data ( from : url)
let image = UIImage ( data : data) !
cache[url] = image
return image
}
}
同じ URL に対して短時間に複数回 image(for:) を呼ぶと、最初の await が完了する前に2件目・3件目が cache[url] の nil チェックを通過してしまい、同じ画像を何度もダウンロードします。壁紙アプリの一覧画面はサムネイルが一気に並ぶため、この問題が目に見える形で帯域とメモリを食っていました。
対処は「進行中のタスク自体をキャッシュする」ことです。画像ではなく Task を保存します。
// ✅ 進行中タスクを共有して重複を防ぐ
actor ThumbnailCache {
private enum Entry {
case inProgress (Task<UIImage, Error >)
case ready (UIImage)
}
private var entries: [URL: Entry] = [ : ]
func image ( for url: URL) async throws -> UIImage {
if let entry = entries[url] {
switch entry {
case . ready ( let image) :
return image
case . inProgress ( let task) :
return try await task. value // 既存のダウンロードに相乗りする
}
}
let task = Task < UIImage, Error > {
let (data, _ ) = try await URLSession.shared. data ( from : url)
guard let image = UIImage ( data : data) else { throw URLError (.cannotDecodeContentData) }
return image
}
entries[url] = . inProgress (task)
do {
let image = try await task. value
entries[url] = . ready (image)
return image
} catch {
entries[url] = nil // 失敗したら次回リトライできるよう消す
throw error
}
}
}
ポイントは、Task を entries に格納する処理が await を挟まずに同期的に完了することです。アクターのメソッド冒頭から entries[url] = .inProgress(task) までの間には中断点がないため、ここに再入は割り込めません。Antigravity に「この actor キャッシュは再入で重複取得が起きないか」と尋ねると、この相乗りパターンを具体例で提示してくれるので、設計レビューの相手として有効でした。
Swift 6 移行で計測した実際の効果
「Swift Concurrency に移行すると速くなる・安全になる」という話は抽象的に語られがちなので、自分のアプリで取った数字を残しておきます。題材は、設定画面の起動時に3系統の API(リモート設定・購読状態・お知らせ)を叩く処理です。
移行前は completion handler を直列で繋いでいて、実機(iPhone 12・4G 回線)で平均 1,180ms かかっていました。これを async let の並列実行に置き換えたところ、平均 420ms まで縮みました。約 2.8 倍の短縮 です。3本の API が互いに独立していたため、最も遅い1本の時間にほぼ収束した格好です。
// 直列(1,180ms平均) → 並列(420ms平均)に置き換えた起動処理
func bootstrap () async throws -> AppBootstrap {
async let config = remoteConfigClient. fetch ()
async let purchase = purchaseClient. currentStatus ()
async let notices = noticeClient. latest ( limit : 5 )
return AppBootstrap (
config : try await config,
purchase : try await purchase,
notices : try await notices
)
}
もう一つ効果が大きかったのがクラッシュ率です。移行前は Crashlytics に、複数スレッドから同じ配列を触ることに起因すると思われる EXC_BAD_ACCESS が断続的に上がっていました。発生頻度は低いものの再現手順が掴めず、半年以上放置していた類のクラッシュです。可変状態を actor に閉じ込め、SWIFT_STRICT_CONCURRENCY を complete まで上げてコンパイラに穴を潰させたところ、この系統のクラッシュは次のリリース以降ほぼゼロになりました。データ競合は「落ちないけれど壊れている」状態を生むので、コンパイラに検出させられる価値は数字以上に大きいと感じています。
移行は一気にやらず、次の順番で段階的に進めるのが私の推奨です。
SWIFT_STRICT_CONCURRENCY を minimal のままにして、まず completion handler を async/await に機械的に置き換える
可変状態を持つキャッシュ・ストア層だけを actor 化する
ビルド設定を targeted に上げ、警告が出た箇所を @Sendable や nonisolated で個別対応する
最後に complete(または Swift 6 言語モード)へ上げ、残った警告を潰す
いきなり complete にすると数百件の警告が一度に出て心が折れます。私は最初それで挫折したので、targeted を経由する遠回りのほうが結果的に速いと考えています。
まとめ
Swift Concurrencyは、iOSアプリ開発における非同期処理のベストプラクティスです。async/await でコードの可読性が向上し、actor でデータ競合がコンパイル時に防止され、Structured Concurrencyでタスクライフサイクルが安全に管理されます。
AntigravityのAIエージェントは、このSwift Concurrencyの習得と実装を大幅に加速します。ボイラープレートの自動生成から、既存コードの移行支援、デバッグ支援まで、日々の開発フローに組み込むことで生産性が飛躍的に向上します。
ぜひ今日から、既存の completion handler を1つ async/await に書き換えることから始めてみてください。Antigravityがその最初の一歩をサポートしてくれるはずです。
iOSアプリ開発の基礎から始めたい方は、まずAntigravityでiOSアプリを作る入門ガイドをご覧ください。また、SwiftUIの実装パターンについてはAntigravity SwiftUI スキルで iOS アプリ開発を加速する完全ガイド も参考になります。データ永続化にSwiftDataを使う場合はAntigravity × SwiftData 実践ガイド と組み合わせると、モダンなiOSアーキテクチャが完成します。
Swift Concurrencyをさらに体系的に