SwiftUI promises “write once, run everywhere” —
but real products quickly discover:
- navigation behaves differently
- layouts break on macOS
- input models diverge
- windows appear unexpectedly
- keyboard, pointer, and focus change everything
Multi-platform SwiftUI is not about conditional views — it’s about architectural separation.
This post shows how to design one codebase that scales cleanly across:
- iPhone
- iPad
- macOS
- visionOS
without becoming a pile of #if os() hacks.
🧠 The Core Principle
Share behavior.
Specialize presentation.
Business logic should be 100% shared.
UI composition adapts per platform.
🧱 1. Split by Feature, Not by Platform
Bad structure:
iOS/
macOS/
Shared/
Good structure:
Features/
Home/
Profile/
Settings/
Platform/
iOS/
macOS/
visionOS/
Each feature contains:
- ViewModel
- State
- Business logic
Platform folders contain:
- wrappers
- layout adapters
- platform-specific views
🧩 2. Platform Abstraction Layer
Create small adapters:
protocol PlatformMetrics {
var sidebarWidth: CGFloat { get }
var toolbarHeight: CGFloat { get }
}
Implement per platform:
struct iOSMetrics: PlatformMetrics {
let sidebarWidth: CGFloat = 0
let toolbarHeight: CGFloat = 44
}
struct macOSMetrics: PlatformMetrics {
let sidebarWidth: CGFloat = 240
let toolbarHeight: CGFloat = 52
}
Inject:
.environment(\.metrics, currentMetrics)
No conditionals in views.
🧭 3. Navigation Architecture per Platform
iPhone
- single stack
- push-based
iPad
- split view
- column navigation
macOS
- sidebar + detail
- multi-window
visionOS
- spatial stacks
- scene-based navigation
Do not force one navigation model.
Instead:
struct RootView: View {
var body: some View {
PlatformContainer {
HomeFeature()
}
}
}
Where PlatformContainer switches implementation.
🪟 4. Window & Scene Management
macOS & visionOS are window-first platforms.
Design explicitly:
WindowGroup {
RootView()
}
WindowGroup(id: "inspector") {
InspectorView()
}
On iOS, this is ignored.
On macOS, this is essential.
Architecture must assume multiple instances.
🧠 5. Input Model Differences
iOS
- touch
- gestures
- swipe
macOS
- pointer
- hover
- right-click
- keyboard
visionOS
- gaze
- pinch
- spatial focus
Never write:
.onTapGesture { }
As your only interaction.
Always support:
- Button
- keyboard shortcuts
- focus
- context menus
⌨️ 6. Keyboard & Command System
macOS & iPad need:
.commands {
CommandGroup(replacing: .newItem) {
Button("New Item") {
create()
}
.keyboardShortcut("N")
}
}
Your architecture should expose actions:
func create()
func delete()
func refresh()
UI wires them.
Logic stays shared.
📐 7. Layout Strategy
Avoid fixed layouts.
Use:
- adaptive stacks
- flexible grids
- dynamic type
- size classes
Pattern:
if horizontalSizeClass == .compact {
CompactLayout()
} else {
RegularLayout()
}
But isolate this in layout containers — not in feature views.
🧪 8. Testing Multi-Platform Correctness
Test:
- window creation
- deep linking per platform
- keyboard navigation
- focus behavior
- split view state
- multi-window restore
Most multi-platform bugs are state bugs, not UI bugs.
❌ 9. Common Anti-Patterns
Avoid:
-
#if os(iOS)inside feature views - platform checks inside ViewModels
- duplicated business logic
- navigation hacks
- forced identical UI everywhere
If it feels forced, it is.
🧠 10. Mental Model
Think in layers:
Business Logic (shared)
State Management (shared)
Navigation Model (per platform)
Layout System (per platform)
Input Model (per platform)
Presentation (per platform)
Only the bottom 3 change.
🚀 Final Thoughts
Multi-platform SwiftUI is not a UI problem — it’s an architecture problem.
When designed correctly:
- features stay reusable
- code stays clean
- platforms feel native
- maintenance stays sane
- visionOS becomes trivial to support
Top comments (0)