There was a day I opened a freshly generated Rork Max project, ran it in the simulator, and archived it as-is. It passed review. Then I installed the build on a real device, scrolled a list, and watched it visibly stutter — a symptom the simulator had never once reproduced.
Speed of generation and shippable quality live on different axes. Now that Rork Max owns the first one, the second one takes up proportionally more of the day. That's the honest read after six months of running it on solo projects.
What follows is only that second half, in the order the work actually happens — with notes on where the time went.
Reading a Rork Max Project Structure
Before touching anything, understand what Rork generated.
MyApp/
├── MyApp.xcodeproj/
├── Sources/
│ ├── App/ ← application entry point
│ ├── Views/ ← SwiftUI views
│ ├── Models/ ← data models, Codable types
│ ├── Services/ ← API clients, persistence
│ └── Utils/ ← extensions, helpers
├── Resources/
│ ├── Assets.xcassets/
│ ├── Localizations/
│ └── Fonts/
└── .rork/ ← Rork's internal metadata (don't touch)
The cardinal rule: never edit .rork/. It's how Rork Max tracks what it generated so it can regenerate cleanly later. Hand-editing it breaks regeneration.
Here's the boundary in more detail:
| Location | Safe to edit? | Why |
|---|---|---|
.rork/ | No | The baseline for regeneration diffs. Editing it corrupts the next output |
Sources/Views/ | Yes, but expect conflicts | The area Rork rewrites most. Keep your diffs minimal |
Sources/Services/ | Yes | Meant to be replaced post-generation. Swap real API implementations here |
Sources/Extensions/ (yours) | Preferred | A directory Rork never generates, so conflicts can't occur |
*.xcodeproj | Yes, carefully | Build Settings survive; target restructuring gets silently reverted |
That last row matters most in practice. Build Settings flags persist across regenerations, but split a target or rework a scheme and the next generation may quietly undo it. Restructure the project file only once you're ready to stop regenerating entirely.
Three Instruments Profiles to Run
Generated code works. At the starting line, though, it stops at "functional." The evidence for pushing past that only comes from measuring on hardware.
The stutter I opened with never appeared in the simulator. Running on a Mac CPU, the expensive computation simply never surfaced as a cost.
1. Time Profiler — CPU Hotspots
First thing I check when something feels slow. The classic generated pattern that shows up as a hotspot:
// Slow: recomputes every frame during scrolling
var body: some View {
List(items) { item in
Row(item: item, score: computeScore(for: item))
}
}If computeScore is non-trivial, it recalculates on every frame while scrolling. The fix is a cached version:
@State private var cachedScores: [UUID: Double] = [:]
var body: some View {
List(items) { item in
Row(item: item, score: cachedScores[item.id] ?? 0)
}
.task {
await computeAllScores()
}
}Profile a Release build on a device, always. An unoptimized Debug build reshapes the profile enough that functions which aren't bottlenecks float to the top. I once spent a full day optimizing a function that turned out to be irrelevant.
2. Allocations — Memory Usage
Image handling in generated code is often the biggest memory waste. Look at Persistent allocations (not Transient). Lists that use AsyncImage or UIImage(data:) without downsampling are a usual suspect.
A helper worth pasting in:
func downsample(image data: Data, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else {
return nil
}
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
return UIImage(cgImage: downsampledImage)
}The kCGImageSourceShouldCache: false on the first line is load-bearing. Drop it and CGImageSource retains the decoded bitmap internally, roughly cancelling out the savings you get downstream. Every time someone tells me this helper "doesn't do anything," that line is missing.
3. SwiftUI Performance — Tracing a Redraw to Its Cause
SwiftUI's declarative model makes redraws non-obvious, and this is the area where Xcode 26 changed the tooling, so it's worth spending some time on.
Instruments 26 ships a dedicated SwiftUI template. Its Update Groups lane lays out, on a timeline, when SwiftUI is doing update work. Where Time Profiler answers "which function is expensive," this answers "which update held the main thread, and for how long."
The more useful half is the Cause & Effect Graph. Select an update, choose "Show Cause & Effect Graph," and you get a node graph showing how a state mutation propagated into body re-evaluation. The nodes to the left of a view are the events that triggered its update.
This lands directly on the pattern Rork-generated code produces most: a view observing an ObservableObject redraws for any property change, including unrelated ones. That used to be a guess-and-check exercise. Now the graph names the mutation responsible.
Once you know the cause, there are two ways to fix it:
// Before: observes the whole object — unrelated changes still redraw
struct ScoreBadge: View {
@ObservedObject var store: SessionStore
var body: some View { Text("\(store.score)") }
}
// After: takes only the value it needs; observation stays in the parent
struct ScoreBadge: View, Equatable {
let score: Int
var body: some View { Text("\(score)") }
static func == (l: Self, r: Self) -> Bool { l.score == r.score }
}Passing plain values and conforming to Equatable lets SwiftUI skip body re-evaluation when nothing changed. Rork Max tends to hand @ObservedObject down to leaf views, so converting just the high-frequency ones — list rows, mostly — measurably shrinks the time spent in Update Groups.