●BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the output●NATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other builders●PLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screen●COMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to end●PRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that back●DEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify●BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the output●NATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other builders●PLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screen●COMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to end●PRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that back●DEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify
Benchmarking Rork Max SwiftUI Native Generation Across 30 Features — What to Delegate and When to Step In
A systematic evaluation of Rork Max's SwiftUI AI generation capabilities across 30 feature categories, rated S through C. Includes practical prompt patterns and code fixes to elevate C-rated features to production quality.
When I started building a HealthKit-powered fitness tracker with Rork Max, the first prompt produced a working step count graph. I thought, "this is going to be smooth." Then I tried adding sleep data. The generated HKSleepAnalysis handling was subtly off — authorization timing was wrong, and the data parsing didn't account for sleep stage changes. Two revisions in, three prompts deep, I finally had something that worked. And I thought: if HealthKit takes this much effort, ARKit would be a serious challenge.
After years of shipping apps independently, my view is this: trying to delegate everything to Rork Max is risky. But delegating nothing wastes the most powerful development tool available today. What matters is knowing in advance which features you can hand off confidently — and which ones require you to stay involved.
This article presents a systematic evaluation of 30 SwiftUI feature categories based on real testing, rated S through C, along with specific techniques to raise C-rated features to production quality.
Evaluation Methodology
To keep comparisons fair, I used the same baseline requirement for each feature: "Generate code that performs basic read/write/display functionality for this API." I scored each category on five dimensions:
Compile pass rate: Does the first generated code build in Xcode without errors?
Runtime stability: Does the app survive the first minute on a real device without crashing?
Code quality: Is error handling present? Are any deprecated APIs used?
Completeness: Are the main requirements implemented without obvious gaps?
Prompts needed: How many iterations are required to reach usable quality?
Rating thresholds:
S: 1–2 prompts to reach production-ready quality. Copy-paste usable.
A: 2–4 prompts to reach high quality. Minor fixes needed.
B: 5–8 prompts, or substantial manual code changes required.
C: 10+ prompts, or most of the generated code needs to be rewritten.
All testing was done on an iPhone 16 Pro running iOS 18.4 with Xcode 16.3.
Measured Benchmark Data — Aggregated Across 30 Features
Here are the numbers behind the ratings. I ran each feature category five times under identical conditions and recorded the first-prompt compile rate, the average number of prompts needed to reach production quality, and the crash rate when running the freshly generated code on a device for one minute. Treat these as directional indicators rather than precise guarantees.
Rating
Representative features
First-pass compile rate
Avg. prompts
On-device crash rate
S
Basic UI, REST API, CRUD
~90%
1.4
~0%
A
StoreKit 2, MapKit, notifications
~70%
2.8
~5%
B
Dynamic Island, HealthKit, camera
~40%
6.2
~25%
C
ARKit, Core ML, Metal
~15%
11+
~60%
What strikes me about these numbers is that the drop from S to C is not gradual — it deepens sharply between B and C. Average prompt counts stay in single digits through B, but at C that measure starts to lose meaning. Past roughly ten prompts, taking the skeleton and rewriting it is faster than layering on more corrections. That is the basis for the division-of-labor judgment I return to later.
The first-pass compile rate is worth a second look too. For S and A, more than half compile on the first try, whereas for C a clean first compile is the exception. It follows that C-rated features are best treated as fragments of a reference implementation, not as a working draft.
✦
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
✦Measured benchmark data across 30 features: first-pass compile rate, average prompts, on-device crash rate
✦A complete code diff taking a C-rated Core ML feature to A quality in five prompts
✦Measured build time per rating tier and designing revenue around S and A features as a solo developer
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.
Basic UI, REST API, and core CRUD operations consistently earn S ratings. List, Form, NavigationStack, Sheet, Alert, and TabView all compile correctly on the first attempt and reach design-polished quality by the second prompt.
REST API integration is where Rork Max shines most consistently. URLSession-based async/await, Codable JSON decoding, and error-handling fetch methods — this is the category where AI generation is most reliable.
// S-rated: REST API integration generated by Rork Max// All three pillars present: error handling, async/await, Codablestruct Article: Codable, Identifiable { let id: Int let title: String let body: String}@MainActorclass ArticleViewModel: ObservableObject { @Published var articles: [Article] = [] @Published var isLoading = false @Published var errorMessage: String? func fetchArticles() async { isLoading = true errorMessage = nil do { guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { errorMessage = "Invalid URL" isLoading = false return } let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { errorMessage = "Server error" isLoading = false return } articles = try JSONDecoder().decode([Article].self, from: data) } catch { errorMessage = error.localizedDescription } isLoading = false }}
The @MainActor annotation on the class and the parallel management of isLoading and errorMessage are both present from the first generation. This is already designed for clean UI binding without any manual intervention.
Full S-rated category list:
Core UI components (List, Form, Sheet, Alert)
REST API with Codable async fetching
Supabase/Firebase Auth UI basic flows
CoreData basic CRUD (1–3 entities)
UserDefaults/AppStorage
Swift Charts (line, bar, pie)
TabView/NavigationStack basic transitions
A-Rated Features — High Quality After 2–4 Prompts
StoreKit 2, MapKit, Core Location, and Local Notifications land in the A tier. The generated code is largely correct, but specific parts require targeted adjustments.
StoreKit 2 illustrates this well. The basic subscription purchase flow works. But Transaction.currentEntitlements for purchase restoration and expiration handling tend to be oversimplified. You need to explicitly request complete behavior.
// A-rated: StoreKit 2 subscription flow (polished after 3 prompts)// Key: explicit transaction verification and update listener lifecycleclass SubscriptionManager: ObservableObject { @Published var isSubscribed = false private var updateListenerTask: Task<Void, Error>? init() { updateListenerTask = listenForTransactions() Task { await updateSubscriptionStatus() } } deinit { updateListenerTask?.cancel() } func purchase(product: Product) async throws -> StoreKit.Transaction? { let result = try await product.purchase() switch result { case .success(let verification): let transaction = try checkVerified(verification) await updateSubscriptionStatus() await transaction.finish() return transaction case .userCancelled: return nil case .pending: return nil @unknown default: return nil } } private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T { switch result { case .unverified(_, let error): throw error case .verified(let safe): return safe } } @MainActor func updateSubscriptionStatus() async { for await result in StoreKit.Transaction.currentEntitlements { if case .verified(let transaction) = result { if transaction.revocationDate == nil { isSubscribed = true return } } } isSubscribed = false } private func listenForTransactions() -> Task<Void, Error> { return Task.detached { for await result in StoreKit.Transaction.updates { if case .verified(let transaction) = result { await self.updateSubscriptionStatus() await transaction.finish() } } } }}
The listenForTransactions() call in init is frequently omitted in first-pass generation. Without it, any purchase events that happen while the app is backgrounded get lost. Adding the instruction "always start a Transaction.updates listener in init and cancel it in deinit" to your prompt resolves this reliably on the second attempt.
Full A-rated list:
StoreKit 2 purchase flows
MapKit display and pin annotation
Core Location basic geofencing
Local Notifications (including Expo Notifications)
Keychain Services (basic storage/retrieval)
FaceID/TouchID (LocalAuthentication)
SwiftUI animations (basic to intermediate)
AVFoundation audio playback
B-Rated Features — Workable With the Right Approach
Dynamic Island, HealthKit, AVFoundation camera capture, and WidgetKit are B-rated. The code sometimes works, but specific edge cases and state transitions cause problems — and repeating the same prompt often produces the same errors.
For Dynamic Island (ActivityKit), expect the ActivityKit import to be missing from the first generation almost every time. The ActivityAttributes definition and ContentState update timing also tend to get conflated.
The fix is to decompose the request into sequential steps rather than asking for everything at once:
Step 1: "Create a DeliveryAttributes struct conforming to ActivityAttributes.
Its ContentState should have two properties: currentStatus: String
and estimatedTime: Date."
Step 2: "Using the DeliveryAttributes from Step 1, implement a
startDeliveryTracking() method that starts an Activity.
Include error handling and a check for whether ActivityKit
is available on this device."
Step 3: "Implement the Live Activity UI with compact, expanded, and
lock screen presentations using SwiftUI. Use .blue as the
background color and .system(.headline) as the base font."
Breaking it into three steps produces high-quality output at each stage. Asking for everything in one prompt results in something that compiles but behaves unpredictably at runtime.
For HealthKit, adding this instruction significantly improves output quality:
"Request HealthKit authorization when the view appears (equivalent to
viewDidAppear). If access is denied, show a prompt that guides the user
to the Settings app. Also add the required NSHealthShareUsageDescription
and NSHealthUpdateUsageDescription keys to Info.plist."
Full B-rated list:
Dynamic Island / Live Activities (ActivityKit)
HealthKit (read/write)
AVFoundation camera capture
WidgetKit (basic widgets)
Background Task / BGTaskScheduler
Core NFC tag reading
UNNotificationServiceExtension (rich push)
C-Rated Features — Human Intervention Required
ARKit, custom Core ML model integration, Metal, CarPlay, and App Clips earn C ratings. The generated code occasionally compiles, but getting it to production quality effectively requires a rewrite.
ARKit is the clearest example. The choice between ARView and ARSCNView is inconsistent — the same prompt can yield different implementations. ARSessionDelegate error handling is almost always absent, and ARWorldTrackingConfiguration settings are usually incomplete.
// C-rated: The ARKit code Rork Max typically generates// This crashes on a real device within secondsclass BadARViewController: UIViewController { var arView = ARSCNView() // No frame binding, no autolayout override func viewDidLoad() { super.viewDidLoad() view.addSubview(arView) // No constraints arView.session.run(ARWorldTrackingConfiguration()) // No delegate, no config }}// Minimum viable ARKit implementation (written by hand after C-rated output)class GoodARViewController: UIViewController, ARSCNViewDelegate, ARSessionDelegate { private lazy var arView: ARSCNView = { let view = ARSCNView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.delegate = self view.session.delegate = self view.autoenablesDefaultLighting = true return view }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(arView) NSLayoutConstraint.activate([ arView.topAnchor.constraint(equalTo: view.topAnchor), arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let config = ARWorldTrackingConfiguration() config.planeDetection = [.horizontal, .vertical] if let formats = ARWorldTrackingConfiguration.supportedVideoFormats.first { config.videoFormat = formats } arView.session.run(config, options: [.resetTracking, .removeExistingAnchors]) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) arView.session.pause() // Prevent memory leak } func session(_ session: ARSession, didFailWithError error: Error) { if let arError = error as? ARError { switch arError.code { case .cameraUnauthorized: showCameraPermissionAlert() case .worldTrackingFailed: restartSession() default: print("AR error: \(arError.localizedDescription)") } } } private func showCameraPermissionAlert() { let alert = UIAlertController( title: "Camera Access Required", message: "Please enable camera access in Settings.", preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) }) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) present(alert, animated: true) } private func restartSession() { arView.session.run(ARWorldTrackingConfiguration(), options: [.resetTracking, .removeExistingAnchors]) }}
For C-rated features, I recommend having Rork Max handle the non-core parts — UI layout, navigation, and unrelated business logic — while writing the AR/ML processing core yourself. This hybrid approach consistently produces better results than either full AI generation or fully manual implementation.
Full C-rated list:
ARKit / RealityKit
Core ML custom model integration
Metal / custom shaders
CarPlay integration
App Clips
GameKit (multiplayer)
Passkeys (latest Authentication Services)
A Worked Example — Lifting a C-Rated On-Device Core ML Feature to A Quality
Even C-rated features can reach production quality if you split the task and aim your verification carefully. Using on-device image classification as the subject, I recorded how the generated code was repaired. In my case the first prompt did not compile, and it took five exchanges to reach working on-device behavior.
Asking for everything at once — "build a SwiftUI view that classifies images with Core ML" — returned code like this. It loads the model inside the view's body on every call, pressuring memory with each inference.
// Fresh output (typical C-rated) — reloads the model inside body every timestruct BadClassifierView: View { @State private var label = "" var body: some View { VStack { Text(label) Button("Classify") { // ❌ re-initializes the model on every tap let model = try! MyImageClassifier(configuration: .init()) // ❌ leaves a try! crash path in place } } }}
From here I resent the instructions in three stages. Stage one: "Initialize VNCoreMLModel once outside the view and share it as a singleton." Stage two: "Run inference through Vision's VNImageRequestHandler and return the top label and its confidence score." Stage three: "Avoid try!, and express model-loading failure and inference failure as separate enum error cases."
What it finally settled into is below. The model is built once, inference is moved off onto an async path, and failure routes are expressed through types.
// A-quality Core ML inference, reached in five promptsenum ClassifierError: Error { case modelUnavailable case inferenceFailed}final class ImageClassifier { static let shared = try? ImageClassifier() private let model: VNCoreMLModel private init() throws { guard let coreMLModel = try? MyImageClassifier(configuration: MLModelConfiguration()) else { throw ClassifierError.modelUnavailable } self.model = try VNCoreMLModel(for: coreMLModel.model) } func classify(_ image: CGImage) async throws -> (label: String, confidence: Float) { try await withCheckedThrowingContinuation { continuation in let request = VNCoreMLRequest(model: model) { request, _ in guard let results = request.results as? [VNClassificationObservation], let top = results.first else { continuation.resume(throwing: ClassifierError.inferenceFailed) return } continuation.resume(returning: (top.identifier, top.confidence)) } request.imageCropAndScaleOption = .centerCrop let handler = VNImageRequestHandler(cgImage: image, options: [:]) do { try handler.perform([request]) } catch { continuation.resume(throwing: ClassifierError.inferenceFailed) } } }}
The delta from the generated version came down to three things: where the model is built, removing try!, and making inference asynchronous. These are exactly the spots Rork Max tends to miss on the first pass — which means writing those three points into the initial prompt as constraints raises first-attempt quality even for a C-rated feature. The key to reducing rewrite effort is naming the landmines before generation, not patching them afterward.
Common Pitfalls and How to Fix Them
Five issues appear repeatedly when working with B and C-rated features.
① Missing @MainActor
The most frequent issue in generated ObservableObject subclasses is a missing @MainActor annotation. Without it, updating @Published properties from background threads produces runtime warnings and potential data races.
// Pattern that frequently appears without @MainActor (generates warnings)class BadViewModel: ObservableObject { @Published var items: [String] = [] func loadItems() async { items = await fetchFromServer() // Warning: published from non-main thread }}// Correct pattern@MainActorclass GoodViewModel: ObservableObject { @Published var items: [String] = [] func loadItems() async { items = await fetchFromServer() // Safe: MainActor ensures main thread }}
Adding "always annotate ObservableObject subclasses with @MainActor" to your prompt eliminates this issue almost completely.
② @State vs @StateObject confusion
Value types (structs) use @State; reference types (classes) use @StateObject. When a custom ViewModel is declared with @ObservedObject instead of @StateObject, it gets reset every time the parent View re-renders — producing subtle bugs where data appears to reset randomly.
③ Preview Provider crashes
Features requiring real hardware — HealthKit, the camera, NFC — crash in Xcode Previews. Explicitly request that generated Previews use mock/stub data to avoid this.
④ Inter-framework dependency ordering
When combining Core ML with Vision Framework, model initialization timing can cause issues. Specifying "initialize the VNCoreMLModel outside the View, managed as a singleton" stabilizes the behavior significantly.
⑤ Missing Entitlements configuration
Rork Max generates the code for HealthKit, Push Notifications, NFC, and iCloud — but does not configure the .entitlements file or Xcode's Signing & Capabilities section. These must be set manually. Forgetting this step causes cryptic runtime errors that look like code bugs but are actually capability configuration issues.
Measured Development Time by Rating
For solo development, how much time a feature burns matters as much as its quality. Implementing the same small feature from scratch, I recorded the time it took to reach production quality by hand.
Rating
Time to production quality
What to delegate to Rork Max
S
15–30 min
Nearly everything
A
30–60 min
Skeleton plus most logic
B
1.5–3 hours
Skeleton and boilerplate only
C
Half a day to a full day
UI and surrounding parts only
This time gap feeds directly into revenue design. What I have kept doing as an indie developer is the kind of app that recovers its costs in small increments through AdMob ads and subscriptions; most of what I have shipped under the Dolice name fits this same pattern. For that pattern the core features fall within S and A, so the implementation time per app stays low. An app whose selling point is AR or on-device ML, by contrast, will keep slipping its release date unless you budget the C-rated time cost from the start.
Before deciding what to build, estimate which rating band each feature belongs to. That single step, I believe, has an outsized effect on how a solo developer paces the work. Build your footing with S- and A-centered apps that recover quickly, then invest any surplus into the more ambitious C-rated features. Over the long run, on limited time, that ordering feels like the realistic one.
Decision Framework — Matching Features to the Right Approach
Based on the 30-feature benchmark, here's how I approach the strategy decision for each new project.
Fully delegate to Rork Max when:
Your app's core features are drawn from S and A-rated categories. The pattern "fetch data from a REST API, display it in a list, monetize with subscriptions" is almost entirely achievable through AI generation. For apps in this category, 90% or more of the code can be AI-generated with confidence.
Use Rork Max as a starter, then tune manually:
Apps containing B-rated features like Dynamic Island, HealthKit, or WidgetKit. Let Rork Max generate the skeleton, then apply the step-by-step prompt approach described above to polish each feature. Budget roughly 2–3× the development time compared to S-rated-only apps.
Use Rork Max as scaffolding only:
Apps requiring C-rated features like ARKit or Core ML. Have Rork Max handle navigation, UI layout, and non-core business logic, while you implement the AR/ML processing core manually. This division of labor is consistently faster than either pure AI generation or fully manual development.
One thing I've come to accept is that Rork Max's generation quality is probabilistic. The same prompt doesn't always produce the same code, and quality varies across sessions. The ratings in this article describe tendencies, not guarantees. There are better days and harder days, and learning to work with that variability — rather than against it — is part of what makes the tool genuinely useful over time.
The benchmark will need updating as Rork Max releases new versions — generation quality has improved noticeably with each major release, and the C-rated categories are gradually improving. Thank you for reading.
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.