◉RORK LABEN
●EXPO — EAS Observe がネイティブクラッシュも記録(10/07)。SDK 57 は 57.0.21 以降で確認できます●SDK 58 — SDK 58 Beta は 9/15 公開。stable の日付はまだ未確認です●RN 0.88 — React Native 0.88.x の正式リリース予定は 10/12、残り3日●Q&A — expo-widgets が本番ビルドだけ真っ黒になる、という問いが出ています●RORK — 09-29 に GPT-6.1 Sol を追加。Pro・Max プランで利用できます●NEW — クライアントのアプリを作る前に決める、公開アカウントの持ち主●EXPO — EAS Observe がネイティブクラッシュも記録(10/07)。SDK 57 は 57.0.21 以降で確認できます●SDK 58 — SDK 58 Beta は 9/15 公開。stable の日付はまだ未確認です●RN 0.88 — React Native 0.88.x の正式リリース予定は 10/12、残り3日●Q&A — expo-widgets が本番ビルドだけ真っ黒になる、という問いが出ています●RORK — 09-29 に GPT-6.1 Sol を追加。Pro・Max プランで利用できます●NEW — クライアントのアプリを作る前に決める、公開アカウントの持ち主
記事一覧/開発ツール
⬡ 開発ツール/2026-07-09上級

Rork Max が書いた Swift を、Swift 6 の厳格な並行性検査に通す

Rork Max が生成した SwiftUI アプリを Swift 6 の complete concurrency checking に通した記録です。警告217件をターゲット単位で削り、@MainActor の置き場所と Sendable の壊し方を実測とともに整理しました。

Rork Max235Swift 6並行性SwiftUI66リファクタリング5

✦ プレミアム記事

Xcode のビルド設定で Strict Concurrency Checking を complete に切り替えた瞬間、警告が 217 件出ました。生成直後は 1 件も出ていなかったコードです。

Rork Max が書いた SwiftUI アプリは、素直で読みやすいコードでした。ただそれは Swift 5 言語モードでの話でした。Swift 6 の言語モードに上げると、その素直さがそのまま「どのスレッドから触られるか誰も宣言していない」という指摘に変わります。

個人開発でアプリを 6 本並行運用している立場からすると、217 件はそれ自体が怖い数字ではありません。怖いのは、直し方を誤ると実行時の挙動が静かに変わることです。@MainActor をひとつ足すだけで、これまで背景スレッドで走っていた画像デコードが UI スレッドに引き戻される。警告は消えて、スクロールが 12fps になる。

この記事は、その 217 件を 3 週間かけて 0 にした過程の記録です。手順よりも「どこで判断を間違えかけたか」に紙幅を割きました。

まず一括で通そうとして失敗した

最初の 2 日間、私はプロジェクト全体の Swift 言語モードを 6 に上げて、出てきたエラーを上から順に潰そうとしました。

これは筋の悪いやり方でした。理由は 2 つあります。

ひとつは、エラーが連鎖するためです。ある型を Sendable に適合させると、その型を持つ別の型で新しいエラーが出ます。全体を一度に赤くすると、いま自分が直しているのが根本原因なのか、それとも波及した結果なのかが判別できなくなります。

もうひとつは、ビルドが通らない状態が長引くと、途中で挙動を確かめられないためです。並行性の修正は「コンパイルが通った=正しい」ではありません。@MainActor の付与は正真正銘の実行時セマンティクスの変更です。ビルドが 2 日間赤いまま進めた変更は、後から一件ずつ検証し直すはめになりました。

3 日目にやり直しました。プロジェクト全体ではなく、ターゲット単位で complete にする方針です。

// Package.swift — SPM ターゲットごとに段階を刻む
.target(
    name: "CoreModels",
    swiftSettings: [
        .swiftLanguageMode(.v6)          // 依存の葉から先に v6 へ
    ]
),
.target(
    name: "ImagePipeline",
    dependencies: ["CoreModels"],
    swiftSettings: [
        .swiftLanguageMode(.v5),         // まだ v5
        .enableUpcomingFeature("StrictConcurrency")  // 警告としてだけ観測
    ]
),

依存グラフの葉(他に依存しない層)から順に v6 へ上げます。上げ切ったターゲットは以降エラーが再発しません。まだ上げていないターゲットでは StrictConcurrency を upcoming feature として有効化し、警告としてだけ数を観測します。

このやり方に変えてから、赤い状態が続くのは 1 ターゲットぶんだけになりました。

週v6 済みターゲット残警告備考
0(開始時)0 / 5217全体一括を断念
12 / 5134CoreModels / Persistence
24 / 541ImagePipeline / Networking
35 / 50App ターゲット

@MainActor は View ではなく状態層に置く

Rork Max が生成したコードは、SwiftUI の View に @State と非同期処理が同居する構造でした。よくある形です。

// 生成直後: View が状態と通信の両方を抱えている
struct WallpaperGridView: View {
    @State private var items: [Wallpaper] = []
    @State private var isLoading = false
 
    var body: some View {
        ScrollView { /* ... */ }
            .task { await load() }
    }
 
    private func load() async {
        isLoading = true
        items = try! await WallpaperAPI.fetchAll()   // 隔離が宣言されていない
        isLoading = false
    }
}

complete にすると、WallpaperAPI.fetchAll() が返す [Wallpaper] を View の隔離コンテキストへ渡す箇所で警告が出ます。

ここで最も手軽な対処は、WallpaperAPI に @MainActor を付けることです。警告は消えます。そして fetchAll() の内部で走っていた JSON デコードが、メインスレッドに乗ります。

私はこれを一度やってしまい、初回ロード時のフレームドロップで気づきました。500 件のサムネイル情報をデコードする 180ms ぶんが、そのままメインスレッドを塞いでいました。

正しい置き場所は、View でも API でもなく、その間の状態層です。

// 移行後: 隔離の境界を Store に一本化する
@MainActor
@Observable
final class WallpaperStore {
    private(set) var items: [Wallpaper] = []
    private(set) var isLoading = false
 
    private let api: WallpaperFetching   // nonisolated なプロトコル
 
    init(api: WallpaperFetching) { self.api = api }
 
    func load() async {
        isLoading = true
        defer { isLoading = false }
        do {
            // await の向こう側はメインスレッドから離れる
            items = try await api.fetchAll()
        } catch {
            items = []
        }
    }
}
 
// API 側は隔離を持たない。呼ばれたスレッドで走る
protocol WallpaperFetching: Sendable {
    func fetchAll() async throws -> [Wallpaper]
}
 
struct WallpaperAPI: WallpaperFetching {
    func fetchAll() async throws -> [Wallpaper] {
        let (data, _) = try await URLSession.shared.data(from: Self.endpoint)
        return try JSONDecoder().decode([Wallpaper].self, from: data)  // 背景で走る
    }
}

WallpaperStore に @MainActor を付け、WallpaperFetching は隔離を持たないままにします。try await api.fetchAll() の呼び出しでメインアクターは一度手放されるため、デコードは背景スレッドで実行されます。戻り値を items に代入する時点でメインアクターに戻ります。

この形にすると、Wallpaper が Sendable である必要が生じます。それは正しい要求です。値がアクター境界を越えるからです。

判断基準として私が置いたのは次の 1 行でした。隔離注釈は「その値を最終的に誰が読むか」ではなく「その値の可変状態を誰が所有するか」に付ける。 View は読むだけです。所有しているのは Store です。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦警告217件を3週間で0にした、ターゲット単位の段階移行手順とビルド設定
✦@MainActor を View ではなく状態層に置いた判断基準と、その前後の差分
✦nonisolated(unsafe) を許容する3条件と、残り12箇所を追跡し続ける仕組み
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-07-18
文字サイズ最大とドイツ語で、生成された画面は静かに詰まる — Rork Max の SwiftUI にレイアウト耐性の検証を通した記録
Rork Max が生成した SwiftUI 画面を疑似ローカライズと最大文字サイズに通し、どこから詰まるかを機械的に洗い出した記録です。ViewThatFits・ScaledMetric・layoutPriority の使いどころと、再生成のたびに回帰を捕まえるスナップショット検証を、実際の数値とともにまとめました。
⬡ 開発ツール2026-05-17
Rork Max の SwiftUI 機能を壁紙アプリ開発で検証した結果 — 実際に動いた機能と手を入れた機能
運用中の壁紙アプリの本番コードを基準に、Rork Max の SwiftUI ネイティブ生成を検証しました。そのまま使えた機能、手を入れた機能、生成を諦めた機能を、動くコードと実機での計測値とともにお伝えします。
⬡ 開発ツール2026-05-12
Rork Max で複数アプリ共通UIライブラリを Swift Package Manager で構築する実践ガイド
複数アプリを運営する個人開発者向けに、Swift Package Managerで共通UIライブラリを構築してRork Max生成コードの重複を排除する手順です。パッケージ構成の設計、テーマシステムとカードコンポーネント、アニメーションプリセットの共通化、テストまで詳解します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます