From SwiftUI Prototype to UIKit Production
Perdiem is a UIKit app. Every screen is built with manual frame layout, custom view controllers, and hand-rolled animations. But the design process starts in SwiftUI.
I maintain a separate Xcode project — perdiem-ios-design — that contains SwiftUI prototypes of every screen. When I need to explore a new feature's UI, I build it in SwiftUI first. Once I'm happy with the design, I translate it to UIKit for production. This sounds redundant, but it's the fastest workflow I've found.
Why Not Just Build in SwiftUI?
The honest answer: UIKit gives me more control where it matters.
Custom transitions. Perdiem's navigation stack uses bespoke push, pop, and slide-up transitions with interactive gesture handling. SwiftUI's NavigationStack has improved significantly, but customising transition animations beyond the defaults is still limited.
Frame-by-frame animation control. When a user drags to dismiss a screen, I need to update multiple views' positions, scales, and opacities on every frame. UIKit's UIPanGestureRecognizer plus manual layout makes this straightforward. SwiftUI's gesture system abstracts away the frame-level control I need.
Predictable layout. Manual frame layout means I know exactly where every pixel is. No geometry reader surprises, no alignment guide edge cases.
Why SwiftUI for Prototyping?
SwiftUI's strengths are exactly what you want during design exploration:
Speed. I can build a rough screen in 20 minutes. Previews update live, I can inject sample data inline, and I don't need to wire up navigation or state management. The goal is to see whether an idea works visually, not to build a production screen.
Iteration. When I'm exploring layouts, I often create multiple versions — SummaryView.swift, SummaryView2.swift, SummaryView3.swift. Each tries a different approach. In UIKit, creating three versions of a screen layout takes significantly more code. In SwiftUI, it's mostly rearranging modifiers.
Design communication. SwiftUI previews are effectively interactive mockups. I can review designs on different device sizes without running the simulator, test with real data models, and share screenshots directly from the preview canvas.
The Translation Process
Once a SwiftUI design is finalised, I translate it to UIKit following consistent patterns:
Layout Mapping
| SwiftUI | UIKit |
|---|---|
VStack | Manual vertical positioning in layoutSubviews() |
HStack | Manual horizontal positioning |
Spacer | Arithmetic with remaining space |
padding() | Insets in frame calculations |
.frame(width:height:) | Explicit size in CGRect |
State Mapping
| SwiftUI | UIKit |
|---|---|
@State | Instance property with didSet |
@Binding | Delegate callback or closure |
@ObservedObject | KVO, NotificationCenter, or delegate |
.onChange(of:) | Property observer or manual check |
A Concrete Example
A SwiftUI horizontal header might look like:
HStack(spacing: 12) {
Circle()
.frame(width: 40, height: 40)
VStack(alignment: .leading, spacing: 2) {
Text(user.name).font(.headline)
Text(user.handle).font(.caption).foregroundColor(.secondary)
}
Spacer()
Text(date).font(.caption).foregroundColor(.secondary)
}
.padding(.horizontal, 16)
The UIKit equivalent in layoutSubviews():
self.avatarView.zeroedContextualFrame = {
var frame = CGRect()
frame.size = CGSize(width: 40, height: 40)
frame.origin.x = 16
frame.origin.y = (size.height - 40) * 0.5
return frame
}()
self.nameLabel.zeroedContextualFrame = {
let labelSize = self.nameLabel.sizeThatFits(/* ... */)
var frame = CGRect()
frame.origin.x = self.avatarView.contextualFrame.maxX + 12
frame.size = labelSize
frame.origin.y = self.avatarView.contextualFrame.minY + 2
return frame
}()
More verbose, yes. But every pixel is explicit, and the layout plays nicely with transforms and animations.
When This Process Breaks Down
The approach works best for custom, visually distinct screens. For standard CRUD forms or settings pages, the SwiftUI prototype step adds overhead without much benefit — you could build those directly in UIKit (or even in SwiftUI, embedded via UIHostingController).
The discipline is knowing when to prototype and when to just build. Explore in SwiftUI when you're unsure about the design. Build directly in UIKit when the pattern is established and you're just applying it.