Vedran Burojevic
← WritingAugust 16, 2026

Multiplatform SwiftUI without fake parity: iPhone, iPad, and Mac

How to share SwiftUI code across iPhone, iPad, and Mac while respecting each platform’s navigation, input, windowing, and design expectations.

On this page

Multiplatform SwiftUI is easy to misunderstand.

The pitch sounds clean: one declarative UI framework, one codebase, three platforms. Build the app once, let SwiftUI adapt it, and enjoy the sudden disappearance of product complexity. That story holds until the app needs keyboard shortcuts, multiple windows, menu commands, sidebar behavior, pointer hover, file import, drag and drop, compact phones, Stage Manager, or a Mac user who expects the app to behave like it belongs there.

The trap is fake parity: sharing screens because they compile, not because they serve the same product job on each platform.

A good multiplatform app should feel related across iPhone, iPad, and Mac. It should not feel like the same phone app got stretched until morale improved.

1. Start with product parity, not screen parity

The first question is not “can this view be shared?”

The first question is “what should the user be able to accomplish on each platform?”

For a packing app, the answer might be:

  1. iPhone: capture items quickly, check the list while moving, handle last-minute edits.
  2. iPad: plan larger trips, compare categories, drag items between sections, review more context.
  3. Mac: prepare templates, manage reusable lists, bulk-edit, import data, export or print.

For a snippet or clipboard app:

  1. iPhone: search, copy, capture a useful note, run one focused action.
  2. iPad: organize collections while using another app nearby.
  3. Mac: live in the menu bar, respond to global shortcuts, support fast keyboard workflows.

Those are related products, not identical screens.

Once the product jobs are clear, sharing decisions become easier. Share the domain model, formatting, validation, routing grammar, persistence boundaries, and core components. Split layout, navigation, commands, and interaction details where the platform expects different behavior.

That is not duplication. That is respecting the platform boundary before it becomes a pile of conditional modifiers.

2. Share the model aggressively

The best place to share code is below the UI.

A healthy multiplatform SwiftUI app usually shares:

  • domain models
  • persistence and sync rules
  • validation
  • formatting
  • feature flags
  • analytics events
  • deep-link parsing
  • business operations
  • view-independent presentation models

This gives the app one answer to product questions.

If “mark packed,” “archive snippet,” “resolve sync conflict,” or “start focus mode” means different things on iPhone, iPad, and Mac, the problem is not SwiftUI. The product model is drifting.

A useful shape is:

Shared domain package

      ├── iPhone presentation
      ├── iPad presentation
      └── Mac presentation

The shared package owns meaning. Platform presentations own fit.

That distinction keeps the app coherent without forcing every surface through the same view hierarchy.

3. Do not let #if os(...) become the architecture

Conditional compilation is useful. It is also how many multiplatform codebases quietly become unreadable.

A few platform checks are fine:

#if os(macOS)
import AppKit
#else
import UIKit
#endif

Or small platform-specific affordances:

#if os(macOS)
.toolbar(removing: .title)
#endif

The problem starts when a single view contains the whole product argument:

if horizontalSizeClass == .compact {
    PhoneLayout(model: model)
} else if ProcessInfo.processInfo.isiOSAppOnMac {
    WeirdCompromiseLayout(model: model)
} else {
    MaybeDesktopLayout(model: model)
}

At that point the view is not adaptive. It is carrying too many platform decisions in the least helpful place.

Prefer explicit platform shells:

struct RootScene: Scene {
    var body: some Scene {
        #if os(macOS)
        MacRootScene()
        #else
        MobileRootScene()
        #endif
    }
}

Then share the pieces that deserve to be shared inside those shells.

This keeps platform decisions close to the platform entry point instead of scattering them through every feature screen.

4. Navigation is where fake parity shows first

Navigation is not a skin.

On iPhone, a stack often makes sense. The user drills from list to detail to edit and back again. Space is constrained, the thumb path matters, and the screen usually has one primary focus.

On iPad, the same feature may need a sidebar, a content column, and a detail pane. Selection becomes persistent. Empty states matter because a user can select nothing while the rest of the interface remains visible.

On Mac, navigation often mixes sidebars, split views, windows, tabs, menu commands, inspector panels, and keyboard shortcuts. The app may have several documents, windows, or utility panels open at once.

Trying to hide all of that behind one NavigationStack usually produces the worst result: the phone app gets too complex, the iPad app feels underused, and the Mac app feels like it was mailed from a simulator.

A better pattern is to share routes and destinations, not necessarily navigation containers.

enum AppRoute: Hashable {
    case trip(UUID)
    case packingItem(UUID)
    case settings
    case search(String)
}

Each platform can interpret those routes differently:

  • iPhone pushes onto a stack.
  • iPad updates sidebar selection and detail state.
  • Mac opens a window, selects a sidebar row, or focuses an existing scene.

The route grammar is shared. The navigation behavior is platform-specific.

That is the line to protect.

5. Design adaptive components, not universal screens

Small components can often be shared well.

Buttons, empty states, status badges, cards, settings rows, search fields, progress indicators, and validation messages usually benefit from common language. They should use the same tokens, typography roles, accessibility labels, and state contracts.

Full screens are less likely to be universal.

A TripSummaryCard can be shared because its job is narrow:

struct TripSummaryCard: View {
    let title: String
    let packedCount: Int
    let totalCount: Int
    let nextAction: TripAction?
 
    var body: some View {
        // Shared content, platform-aware styling where needed.
    }
}

A complete TripPlanningScreen probably should not be identical across iPhone, iPad, and Mac. The amount of visible context, editing affordances, drag targets, toolbar placement, and keyboard behavior all change.

The useful rule:

  1. Share components when the product job is the same.
  2. Split screens when the interaction model is different.
  3. Keep state contracts shared so the split does not become semantic drift.

This gives the codebase reuse without pretending all reuse has equal value.

6. Treat input as a platform feature

Touch, pointer, keyboard, trackpad, Apple Pencil, context menus, command menus, and drag and drop are not minor details. They shape the app.

An iPhone screen can rely on direct touch and compact actions. An iPad app should often support pointer hover, drag and drop, hardware keyboard shortcuts, multiwindow, and larger layout density. A Mac app without keyboard-first operation feels unfinished almost immediately.

This affects implementation:

  • Primary actions may live in a bottom bar on iPhone.
  • The same actions may move to a toolbar or command menu on iPad.
  • On Mac, they may need menu items, keyboard shortcuts, contextual menus, and toolbar customization.

SwiftUI gives you shared primitives for some of this:

.commands {
    CommandMenu("Trip") {
        Button("Add Item") {
            model.addItem()
        }
        .keyboardShortcut("n", modifiers: [.command])
    }
}

But the product decision is yours. A shortcut is not just faster access to a button. It is a promise that the operation is stable enough to call without visual hunting.

If the app only works by tapping through the phone flow, the Mac version is not finished. It is visiting.

7. Use platform-specific scenes deliberately

Scenes are one of the strongest reasons to stop thinking in single-screen terms.

On iPhone, the app may only need one main window group.

On iPad, users may expect multiple windows for the same app, especially for document-like work, planning, reference, or side-by-side workflows.

On Mac, windows are part of the product model. A preferences window, inspector, quick-capture panel, document window, menu bar extra, or focused utility panel may each deserve a different scene.

A multiplatform app can still share most of its domain code while exposing different scene structures:

@main
struct ProductApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
        }
 
        #if os(macOS)
        Settings {
            SettingsView()
        }
 
        MenuBarExtra("Capture", systemImage: "text.cursor") {
            QuickCaptureView()
        }
        #endif
    }
}

Do not force Mac behavior through the iPhone scene model because it is convenient. That is how you get a desktop app that technically launches and emotionally does not.

Scene structure should describe how the product is used on that platform.

8. Keep shared styling semantic

Visual consistency should come from tokens and roles, not copied measurements.

A good shared design layer says:

  • textPrimary
  • textSecondary
  • surfaceRaised
  • borderSubtle
  • actionPrimary
  • warning
  • danger
  • contentSpacing
  • controlRadius

It should not say “this is 16 points everywhere because the Figma file said so once.”

Spacing, density, hover states, control size, sidebar width, and toolbar behavior often need platform variation. Semantic roles let you vary those details without changing product meaning.

For example:

enum AppSpacing {
    static let section: CGFloat = {
        #if os(macOS)
        20
        #else
        16
        #endif
    }()
}

Use this sparingly. The point is not to create a giant abstraction layer over every pixel. The point is to name repeated decisions and allow platform fit where it matters.

If a design token needs a paragraph of documentation to use correctly, the abstraction is probably too clever.

9. Test the same behavior through different platform paths

Multiplatform testing should prove two things:

  1. the shared behavior means the same thing everywhere
  2. each platform path can reach that behavior naturally

That means unit tests should focus on the shared domain operations:

  • validation
  • sync conflict resolution
  • entitlement checks
  • persistence migrations
  • route parsing
  • formatting
  • data transformations

UI and integration checks should be platform-specific:

  • iPhone compact navigation and one-handed flow
  • iPad split view, sidebar selection, Stage Manager, keyboard shortcuts
  • Mac menu commands, window restoration, toolbar actions, focus, file import, drag and drop

Do not let one passing iPhone UI test certify the Mac app. It proves the phone path works. The Mac path is a different client.

This is the same principle as widgets, App Intents, and extensions: shared logic is valuable, but every surface still needs verification in the context where users invoke it.

10. Watch for accidental phone assumptions

Most fake parity starts as an iPhone assumption that nobody named.

Common examples:

  • one visible detail at a time
  • one window
  • one active account context
  • no persistent selection
  • no right-click or context menu
  • no keyboard shortcuts
  • no hover state
  • no drag destination
  • one modal at a time
  • toolbar actions that assume compact space
  • navigation state stored where scene state should live

These assumptions can be fine on iPhone. They become strange on iPad and visibly wrong on Mac.

Review shared code for hidden phone bias. If a model, router, or view assumes a single global navigation stack, a single selected object, or a single modal slot, it may need to move down into the iPhone presentation layer or become scene-scoped.

This is not theoretical purity. It is how you avoid bugs where opening a second window changes selection in the first one because some shared singleton got ambitious.

11. Decide when not to ship a platform

A weak Mac app can hurt more than no Mac app.

The same applies to iPad support that is technically present but ignores multitasking, keyboard input, and larger layouts. Users can tell when a platform exists because a checkbox was cheap.

Before shipping a platform, ask:

  1. Does it support the platform’s main input methods?
  2. Does navigation match platform expectations?
  3. Does window or scene behavior make sense?
  4. Are the important commands discoverable?
  5. Does layout use available space without becoming noisy?
  6. Are platform-specific edge cases tested?
  7. Would I recommend this version to someone who primarily uses that device?

If the answer is no, ship the narrower platform well first.

There is no medal for distributing disappointment across more devices.

12. The baseline I would ship

For a production multiplatform SwiftUI app, I would want this baseline:

  1. shared domain model, validation, persistence, routing grammar, and product operations
  2. platform-specific root shells for iPhone, iPad, and Mac
  3. shared components only where the product job and state contract match
  4. route handling that allows different navigation containers per platform
  5. scene structure that respects windows, settings, menu bar extras, and multiwindow behavior where relevant
  6. keyboard, pointer, menu, drag-and-drop, and touch behavior treated as product features
  7. semantic design tokens with platform-aware density where needed
  8. tests that cover shared behavior and platform-specific paths separately
  9. a written list of platform assumptions the app intentionally supports or rejects

The goal is not to write three apps.

The goal is to write one product with platform-specific expressions where they matter. Share meaning. Share operations. Share language. Then let each platform be itself.

That is the difference between a coherent multiplatform SwiftUI app and a phone UI distributed to platforms it never really designed for.

Command menu

Navigate the site or run an action