◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
Articles/Dev Tools
⬡ Dev Tools/2026-07-09Advanced

Getting Rork Max's Swift Through Swift 6's Strict Concurrency Checking

A field record of taking a Rork Max-generated SwiftUI app through Swift 6 complete concurrency checking: 217 warnings cleared target by target, where @MainActor actually belongs, and measured before/after numbers.

Rork Max235Swift 6ConcurrencySwiftUI66Refactoring5

✦ Premium Article

The moment I flipped Strict Concurrency Checking to complete in Xcode, 217 warnings appeared. The same code had produced exactly zero the day before.

Rork Max writes clean, readable SwiftUI. That readability holds under the Swift 5 language mode. Raise the project to Swift 6 and the same clarity turns into a long list of "nobody has declared which thread touches this."

Running six apps on the App Store as an indie developer, 217 is not a scary number by itself. What worries me is that the wrong fix silently changes runtime behavior. Add a single @MainActor and the image decoding that used to run on a background thread gets pulled onto the UI thread. The warning disappears. Your scroll drops to 12fps.

This is the record of clearing those 217 warnings over three weeks. I've spent more of it on the places where I nearly made the wrong call than on the steps themselves.

The all-at-once attempt that failed

For the first two days I raised the whole project to Swift 6 and worked down the error list from the top.

That was the wrong shape of work, for two reasons.

Errors cascade. Conform one type to Sendable and a new error surfaces in the type that holds it. When the entire project is red, you cannot tell whether the diagnostic in front of you is a root cause or a ripple from something three files away.

And a build that stays broken for two days means you cannot check behavior along the way. Concurrency fixes are not "it compiles, therefore it's correct." Adding @MainActor is a genuine change to runtime semantics. Every change I made during those two red days had to be re-verified afterward, one at a time.

On day three I started over, raising language mode per target rather than per project.

// Package.swift — advance one target at a time
.target(
    name: "CoreModels",
    swiftSettings: [
        .swiftLanguageMode(.v6)          // leaves of the dependency graph first
    ]
),
.target(
    name: "ImagePipeline",
    dependencies: ["CoreModels"],
    swiftSettings: [
        .swiftLanguageMode(.v5),         // still v5
        .enableUpcomingFeature("StrictConcurrency")  // observe as warnings only
    ]
),

Start at the leaves — the modules that depend on nothing. Once a target is on v6, errors never reappear there. Targets still on v5 enable StrictConcurrency as an upcoming feature, so the warning count stays visible without blocking the build.

After that change, only one target was ever red at a time.

WeekTargets on v6Warnings leftNotes
0 (start)0 / 5217Abandoned the big-bang attempt
12 / 5134CoreModels, Persistence
24 / 541ImagePipeline, Networking
35 / 50App target

@MainActor belongs on the state layer, not the View

The generated code put @State and async work together inside the View. A very common shape.

// As generated: the View owns both state and networking
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()   // no isolation declared
        isLoading = false
    }
}

Under complete, the compiler flags the point where [Wallpaper] crosses into the View's isolation domain.

The easiest fix is to annotate WallpaperAPI with @MainActor. The warning goes away — and the JSON decoding inside fetchAll() now runs on the main thread.

I made that mistake once and caught it as a frame drop on first load. Decoding 500 thumbnail records took 180ms, and all 180ms sat on the main thread.

The right place for the annotation is neither the View nor the API. It's the state layer between them.

// After: one isolation boundary, on the store
@MainActor
@Observable
final class WallpaperStore {
    private(set) var items: [Wallpaper] = []
    private(set) var isLoading = false
 
    private let api: WallpaperFetching   // a nonisolated protocol
 
    init(api: WallpaperFetching) { self.api = api }
 
    func load() async {
        isLoading = true
        defer { isLoading = false }
        do {
            // main actor is released across the await
            items = try await api.fetchAll()
        } catch {
            items = []
        }
    }
}
 
// The API declares no isolation. It runs wherever it's called from.
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)  // off the main thread
    }
}

WallpaperStore carries @MainActor. WallpaperFetching carries none. The main actor is released across try await api.fetchAll(), so decoding happens on a background thread, and the assignment to items hops back.

This forces Wallpaper to be Sendable, which is the correct demand — the value really does cross an actor boundary.

The rule I settled on fits in one line. Isolation annotations belong where mutable state is owned, not where the value is eventually read. The View only reads. The store owns.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦The target-by-target migration path that took 217 warnings to zero in three weeks
✦Why @MainActor belongs on the state layer rather than the View, with the before/after diff
✦Three conditions for allowing nonisolated(unsafe), plus the CI baseline that keeps the remaining 12 from growing
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-07-18
The Generated Screen That Quietly Jams at AX5 and in German — Putting Layout Resilience Checks on Rork Max SwiftUI
A record of running every Rork Max generated SwiftUI screen through pseudolocalization and the largest text size to find exactly where it jams. Covers when to reach for ViewThatFits, ScaledMetric and layoutPriority, plus the snapshot checks that catch regressions every time you regenerate.
⬡ Dev Tools2026-05-17
Testing Rork Max SwiftUI Features on a Real Wallpaper App — What Worked, What Needed Fixes
I benchmarked Rork Max's SwiftUI generation against the production code of a wallpaper app I actually run. Here is what shipped as-is, what needed fixing, and what I ended up writing by hand — with working code and on-device measurements.
⬡ Dev Tools2026-05-12
Building a Shared UI Component Library with Swift Package Manager in Rork Max
A practical guide to building a shared SwiftUI component library with Swift Package Manager for indie developers running multiple apps — eliminate duplicate code across wallpaper, healing, and similar app families.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links