RORK LABJP
MAX — Rork Max builds native Swift apps instead of React Native for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — Rork Max unlocks native features like AR/LiDAR scanning, Metal 3D games, widgets, and Dynamic IslandAPPLE — It also reaches advanced Apple capabilities such as HealthKit, HomeKit, NFC, App Clips, and on-device Core MLRN — Standard Rork builds iOS and Android from a single prompt using React Native (Expo)PRICE — Rork is free to start, with paid plans beginning at $25 per monthGROWTH — Rork now sees 743,000+ monthly visits, 85% growth, and recently raised $15MMAX — Rork Max builds native Swift apps instead of React Native for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — Rork Max unlocks native features like AR/LiDAR scanning, Metal 3D games, widgets, and Dynamic IslandAPPLE — It also reaches advanced Apple capabilities such as HealthKit, HomeKit, NFC, App Clips, and on-device Core MLRN — Standard Rork builds iOS and Android from a single prompt using React Native (Expo)PRICE — Rork is free to start, with paid plans beginning at $25 per monthGROWTH — Rork now sees 743,000+ monthly visits, 85% growth, and recently raised $15M
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 Max218Swift 6ConcurrencySwiftUI62Refactoring4

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 $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-07-06
Migrating Rork Max SwiftUI to @Observable: Narrowing the Re-renders ObservableObject Was Spreading
Rork Max tends to generate SwiftUI apps built on ObservableObject and @Published, where a single state change re-evaluates every subscribing view. Moving to the Observation framework's @Observable narrows invalidation to the property level. Here is the migration path, plus the view-body execution counts I measured in Instruments before and after.
Dev Tools2026-05-02
10 Refactoring Patterns to Take Rork Max's Generated SwiftUI Code from Demo to Production
Rork Max generates SwiftUI in seconds, but the output won't always pass App Store review as-is. Here are ten refactoring patterns I run on every generation, with before/after code, to ship without rejections.
Dev Tools2026-06-30
Building StandBy-Optimized Widgets in Rork Max
A hands-on walkthrough for tuning WidgetKit widgets for StandBy mode, the landscape charging display in iOS 17+. Covers always-on dimming, night mode, and container backgrounds, including which parts of Rork Max's generated code you still have to finish by hand.
📚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
See all →