One Codebase for iOS, iPadOS, macOS, watchOS and visionOS

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Apple sells five device families, and a well-built SwiftUI app can reach all of them from one source tree. This page shows how FieldNotes does it, on the section’s baseline: iOS, iPadOS, macOS, watchOS and visionOS 27, built with Xcode 27 and Swift 6.4, with a deployment minimum of 26 on every platform. It covers the Xcode side (which targets exist, which destinations each one builds for, and how build settings vary per SDK) and the Swift side: the compile-time and run-time checks that keep platform-specific code out of the platforms that lack it. The language features themselves (#if compiler control statements and the @available attribute) are defined on Attributes and Compiler Control in the Swift reference. This page covers how an app project uses them.

The project model assumed here was introduced on Xcode Projects, Targets, Schemes and Previews, and the package split on Swift Packages and Modularizing an App. Each platform also gets its own page later in the section, covering what is unique to it:

One Target, Many Destinations

Before Xcode 14, a SwiftUI app for iPhone and Mac usually meant two app targets: FieldNotes (iOS) and FieldNotes (macOS). Each had its own bundle identifier, Info.plist, entitlements and file membership list, and they drifted apart over time. A multiplatform app target replaces that pair with one target. It holds one set of source files and one set of project settings, and it builds a separate product for every Supported Destination it lists. Apple’s rule is short: iOS, iPadOS, macOS, tvOS and visionOS apps can share a single target; watchOS apps remain in a separate target.

FieldNotes follows that rule exactly:

Target Builds for Why it is separate or shared

FieldNotes

iPhone, iPad, Mac, Apple Vision

One SwiftUI App, one set of settings. Each destination gets its own binary, but they all come from the same sources and the same bundle identifier, com.example.fieldnotes.

FieldNotesWatch

Apple Watch

watchOS apps can’t join a multiplatform target. The watch app has its own App entry point and a much smaller UI, but it links the same FieldNotesKit modules.

FieldNotesWidgets

iPhone, iPad, Mac, Apple Vision

A widget extension, added on Widgets, Complications and Controls (WidgetKit). Extensions are separate targets.

FieldNotesWatchWidgets

Apple Watch

The watch’s own widget extension, for complications and Smart Stack widgets. It also comes from Widgets, Complications and Controls (WidgetKit).

FieldNotesTests

Every destination the app supports

Swift Testing, added on Unit Testing with Swift Testing.

Figure: How FieldNotes’s packages feed its app targets

flowchart LR subgraph Packages["Local Swift packages (Packages/)"] M["FieldNotesModel
Note, Notebook"] S["FieldNotesServices
NoteStore, NoteSyncing, SummaryGenerating"] U["FieldNotesUI
shared views and styles"] M --> S M --> U S --> U end subgraph App["FieldNotes: multiplatform app target"] A["FieldNotesApp (@main)
WindowGroup + platform-specific scenes"] D1["iPhone / iPad"] D2["Mac (native)"] D3["Apple Vision"] A --> D1 A --> D2 A --> D3 end subgraph Watch["FieldNotesWatch: watchOS app target"] W["FieldNotesWatchApp (@main)
compact note list, dictation"] end U --> A S --> A M --> W S --> W

The packages sit at the bottom of the graph, and they are what makes sharing work. FieldNotesModel and FieldNotesServices contain no UI, so they compile for every platform, watchOS included. FieldNotesUI holds the shared SwiftUI views, and only the multiplatform target depends on it. The watch app draws its own, much smaller views. Each package declares the platforms it supports in its manifest:

// swift-tools-version: 6.4
// Packages/FieldNotesKit/Package.swift
import PackageDescription

let package = Package(
    name: "FieldNotesKit",
    platforms: [.iOS(.v26), .macOS(.v26), .watchOS(.v26), .visionOS(.v26)],
    products: [
        .library(name: "FieldNotesModel", targets: ["FieldNotesModel"]),
        .library(name: "FieldNotesServices", targets: ["FieldNotesServices"]),
    ],
    targets: [
        .target(name: "FieldNotesModel"),
        .target(
            name: "FieldNotesServices",
            dependencies: ["FieldNotesModel"],
            swiftSettings: [
                // Compile the spatial-only code paths only when building for visionOS.
                .define("FIELDNOTES_SPATIAL", .when(platforms: [.visionOS])),
            ]
        ),
    ]
)

The platforms array sets the minimum version on each platform. It doesn’t limit which platforms can use the package: a platform left out of the list falls back to SwiftPM’s oldest supported version. So a package that must not build for watchOS has to fail visibly, for example with an #if os(watchOS) / #error guard. It can’t opt out by omission. SwiftPM itself is covered on Swift Package Manager.

Adding and choosing Supported Destinations

In the project editor, select the FieldNotes target and open General → Supported Destinations. The + button adds a destination, and the menu it shows depends on the destinations already in the list. For the Mac and for Apple Vision, Xcode then asks which kind of experience you want:

Destination type What it builds FieldNotes’s choice

Mac

A native macOS app against the macOS SDK, using SwiftUI and AppKit. This is the default for multiplatform apps.

Yes. SwiftUI already gives FieldNotes a real Mac app, and the Mac build adds Settings and MenuBarExtra.

Mac Catalyst

The iPad app recompiled for macOS with UIKit, with standard controls adapted to Mac appearance.

No. It suits a large UIKit iPad codebase (see below).

Apple Vision

A native visionOS app against the visionOS SDK, using SwiftUI and RealityKit. This is the default for multiplatform apps.

Yes, so the app can offer the ImmersiveSpace map.

Designed for iPad

The unmodified iPad binary, running on an Apple silicon Mac or on Apple Vision Pro, with iPad appearance. This is the default Mac and Apple Vision destination for iPad-only apps.

Only as a fallback. A native destination replaces it.

Behind the checkboxes, the destinations are ordinary build settings. That matters when a team reviews a project.pbxproj diff or moves settings into an .xcconfig file:

// Config/FieldNotes.xcconfig -- what the Supported Destinations list writes for the FieldNotes target
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx xros xrsimulator
TARGETED_DEVICE_FAMILY = 1,2,7            // 1 = iPhone, 2 = iPad, 7 = Apple Vision
SUPPORTS_MACCATALYST = NO                 // a native Mac destination instead of Catalyst
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO // the native visionOS build replaces "Designed for iPad"

IPHONEOS_DEPLOYMENT_TARGET = 26.0
MACOSX_DEPLOYMENT_TARGET = 26.0
XROS_DEPLOYMENT_TARGET = 26.0

xros is the SDK name that visionOS inherited from its internal name, and it still appears in build settings and xcodebuild -sdk arguments. The per-platform *_DEPLOYMENT_TARGET settings are what General → Minimum Deployments edits. Every one of them is 26 for FieldNotes.

The separate watchOS target

FieldNotesWatch was added with File → New → Target → watchOS → App, using the "Watch app for Existing iOS App" option, so Xcode embeds it in the iPhone app and sets its companion bundle identifier. It has its own App type, because watchOS has no windows, no sidebar and no menu bar. It still reuses the model and services:

// FieldNotesWatch/FieldNotesWatchApp.swift
import SwiftUI
import FieldNotesModel
import FieldNotesServices

@main
struct FieldNotesWatchApp: App {
    @State private var store = NoteStore()

    var body: some Scene {
        WindowGroup {
            NavigationStack {
                WatchNoteList()          // pinned notes first, dictation button in the toolbar
            }
            .environment(store)
        }
    }
}

The watch app sets WKCompanionAppBundleIdentifier to com.example.fieldnotes and uses its own bundle identifier, com.example.fieldnotes.watchkitapp. It can also set WKRunsIndependentlyOfCompanionApp so it works without the phone nearby. Everything watch-specific (complications, Smart Stack, the Always On display) is on watchOS: App Structure, Connectivity, Workouts and the Smart Stack.

Per-SDK Build Settings

One target still needs a few settings that differ by platform. Xcode’s build settings editor handles this with conditional values. Next to a setting, click +, choose an SDK from the Any SDK menu, and enter a value for that SDK only. In an .xcconfig file, the same condition is written in square brackets after the setting name, and several conditions can be chained:

// Config/FieldNotes.xcconfig (continued)

// A different display name for the Mac and visionOS builds.
INFOPLIST_KEY_CFBundleDisplayName = FieldNotes
INFOPLIST_KEY_CFBundleDisplayName[sdk=macosx*] = FieldNotes for Mac
INFOPLIST_KEY_CFBundleDisplayName[sdk=xros*] = FieldNotes Spatial

// Per-platform entitlements: the Mac build is sandboxed and needs its own file.
CODE_SIGN_ENTITLEMENTS = FieldNotes/FieldNotes.entitlements
CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = FieldNotes/FieldNotes-macOS.entitlements

// A custom compilation condition, visible to #if in Swift, only in visionOS builds.
SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=xros*] = $(inherited) FIELDNOTES_SPATIAL

// Conditions combine: a Debug-only flag for the Simulator build of the iOS SDK.
OTHER_SWIFT_FLAGS[config=Debug][sdk=iphonesimulator*] = $(inherited) -D FIELDNOTES_SIM_FIXTURES

The * wildcard matters. [sdk=macosx*] matches macosx27.0 and every later SDK, while a bare [sdk=macosx27.0] would silently stop matching after next year’s Xcode. Whole files can also be limited to some platforms: in Build Phases → Compile Sources, change a file’s Filters column from Any Supported Platform to a subset. FieldNotes does this for MenuBarContent.swift (macOS only) and NotesMapSpace.swift (visionOS only). Those files then need no #if at all. How targets, configurations and .xcconfig layering work in general is covered on Xcode Projects, Targets, Schemes and Previews.

Conditional Compilation

Build settings decide which files and flags reach the compiler. Inside a shared file, Swift’s compiler control statements decide which lines do. The code a false branch contains is never type-checked for that platform, so it may call APIs that don’t exist there. All four conditions below are evaluated at compile time. None of them costs anything at run time.

#if os(...): the operating system

os() takes iOS, macOS, watchOS, tvOS, visionOS or Linux. There is no iPadOS condition: iPad builds use the iOS SDK, so os(iOS) is true on both iPhone and iPad. Size classes and the user interface idiom (below) tell the two apart at run time.

// FieldNotesUI/NoteRow.swift
struct NoteRow: View {
    let note: Note

    var body: some View {
        HStack {
            Text(note.title)
            Spacer()
            if note.isPinned { Image(systemName: "pin.fill") }
        }
        #if os(macOS)
        .padding(.vertical, 2)            // denser rows fit the Mac's pointer precision
        #elseif os(visionOS)
        .padding(.vertical, 10)           // larger hit targets for eyes-and-hands input
        .hoverEffect()
        #endif
    }
}

A #if placed between modifiers, as above, is a postfix member expression condition (Swift 5.5 and later). It keeps a platform-specific modifier inside the modifier chain, so the whole view doesn’t have to be duplicated in an #if/#else pair.

#if canImport(...): whether a module exists

canImport checks whether the SDK being built against contains a module. It is the right test when the framework is what varies, not the OS. It is also forward-compatible: if a later SDK adds the framework to another platform, the code starts compiling there with no edit.

// FieldNotesServices/HapticFeedback.swift
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

@MainActor
enum HapticFeedback {
    static func notePinned() {
        #if canImport(UIKit) && !os(visionOS) && !os(watchOS)
        UIImpactFeedbackGenerator(style: .light).impactOccurred()
        #elseif canImport(AppKit)
        NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default)
        #endif
        // visionOS and watchOS: no haptic for this action.
    }
}

#if targetEnvironment(...): Simulator and Mac Catalyst

targetEnvironment takes two values. simulator is true when building for any Simulator SDK. FieldNotes uses it to seed sample notes, because the Simulator has no iCloud data:

// FieldNotes/FieldNotesApp.swift (excerpt)
private func makeStore() -> NoteStore {
    let store = NoteStore()
    #if targetEnvironment(simulator)
    store.notes = Note.samples          // fixture data for screenshots and UI work
    #endif
    return store
}

macCatalyst is true only in a Mac Catalyst build. In such a build os(iOS) is also true, because a Catalyst app is compiled from the iOS sources. So check Catalyst first:

#if targetEnvironment(macCatalyst)
let exportLabel = "Export to Finder…"
#elseif os(iOS)
let exportLabel = "Share…"
#elseif os(macOS)
let exportLabel = "Export…"
#endif

FieldNotes doesn’t ship a Catalyst build, so this branch exists only in its shared FieldNotesUI package, which an iPad-first team could also build with Catalyst. The FIELDNOTES_SPATIAL flag defined earlier works the same way: #if FIELDNOTES_SPATIAL tests a custom condition set by build settings or by .define in a package.

Availability: @available, #available and #unavailable

Conditional compilation answers "does this platform have it?". Availability answers a different question: "does this OS version have it?". The distinction matters because FieldNotes builds with the 27 SDKs but deploys back to 26. An API introduced in iOS 27 compiles fine, but calling it on an iPhone that still runs iOS 26 would crash. The compiler therefore refuses the call unless it is guarded.

// FieldNotesServices/NoteStore+Summaries.swift
extension NoteStore {
    /// Runtime check: fall back gracefully on 26.
    func summaryButtonIsVisible() -> Bool {
        if #available(iOS 27, macOS 27, watchOS 27, visionOS 27, *) {
            return true                 // new in 27: the on-device summary path
        } else {
            return false
        }
    }

    /// Inverted check: run the migration only on OSes that predate 27.
    func migrateLegacyPinsIfNeeded() {
        if #unavailable(iOS 27, macOS 27, watchOS 27, visionOS 27) {
            notes = notes.map { var n = $0; n.isPinned = n.tags.contains("pinned"); return n }
        }
    }
}

/// Declaration annotation: the whole type needs the 27 releases.
@available(iOS 27, macOS 27, watchOS 27, visionOS 27, *)
struct SummaryCard: View {
    let note: Note
    var body: some View { Text(note.title) }   // body may use 27-only APIs freely
}

/// Removing an API from one platform entirely.
@available(watchOS, unavailable, message: "Exporting is not offered on Apple Watch")
func exportNotebook(_ notebook: Notebook) async throws -> URL { /* … */ fatalError() }

A few rules explain most availability errors:

  • The trailing * in #available(iOS 27, *) means "on every other platform, from its minimum deployment target". It is required, and it is why a condition that lists only iOS still compiles for macOS. On macOS such a check proves nothing beyond the deployment minimum of 26, so a call to an API that is new in macOS 27 inside it is still an error in the Mac build. List every platform the call needs, as FieldNotes does.

  • #unavailable (Swift 5.6) is the negation. It has no *, because "unavailable everywhere else" would be meaningless. Use it for work that must happen only on older systems, instead of an empty if #available branch with an else.

  • #available can appear only in the condition list of an if, guard or while. It can’t be negated with !, combined with ||, or stored in a variable. To gate a whole type or function, annotate it with @available. Callers then need one check, and the body needs none.

  • Minor versions work: #available(iOS 27.1, *) guards APIs introduced in a point release, such as ArrangementView below.

Figure: API-availability patterns

Pattern Evaluated Question it answers FieldNotes example

#if os(macOS)

Compile time

Is this build for this OS?

Settings and MenuBarExtra scenes; denser NoteRow padding on Mac.

#if canImport(UIKit)

Compile time

Does this SDK contain the module?

UIKit or AppKit haptics in HapticFeedback.

#if targetEnvironment(simulator)

Compile time

Is this a Simulator build?

Seeding Note.samples into NoteStore.

#if targetEnvironment(macCatalyst)

Compile time

Is this iOS code built as a Catalyst app?

Mac-specific labels in the shared FieldNotesUI package.

#if FIELDNOTES_SPATIAL

Compile time

Is a custom flag defined for this build?

visionOS-only code in FieldNotesServices.

if #available(iOS 27, macOS 27, watchOS 27, visionOS 27, *)

Run time

Is the running OS new enough?

The on-device summary button (new in 27).

if #unavailable(iOS 27, …)

Run time

Is the running OS older than this?

One-time pin migration on 26.

@available(iOS 27, …, *)

Compile time (enforced at call sites)

Can this declaration be used from this code?

SummaryCard; callers must be guarded.

@available(watchOS, unavailable)

Compile time

Should this declaration be absent from one platform?

exportNotebook(_:) on Apple Watch.

ProcessInfo.processInfo.isiOSAppOnMac / isiOSAppOnVision

Run time

Is this iPhone/iPad binary running on a Mac or Vision Pro?

Hiding the camera-scan button in "Designed for iPad" runs.

The Objective-C equivalents, TARGET_OS_* macros, @available(…​) and API_AVAILABLE(...), are covered on Mixing Swift, Objective-C and C++ in an App, and in Apple’s Marking API availability in Objective-C.

Platform-Specific Scenes

Views adapt with modifiers. The app’s scene structure is where the platforms differ most, and a single App type describes all of them. FieldNotes’s entry point is the one place with three #if blocks, and each block adds a scene that exists on only one platform:

// FieldNotes/FieldNotesApp.swift
import SwiftUI
import FieldNotesServices

@main
struct FieldNotesApp: App {
    @State private var store = makeStore()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(store)
        }
        #if os(macOS)
        .defaultSize(width: 1100, height: 720)
        #endif

        #if os(macOS)
        Settings {                                    // the app menu's Settings… window (macOS only)
            SettingsView()
                .environment(store)
        }

        MenuBarExtra("FieldNotes", systemImage: "note.text") {
            QuickCaptureMenu()                        // new note from anywhere, recent notes
                .environment(store)
        }
        #endif

        #if os(visionOS)
        ImmersiveSpace(id: "map") {                   // notes pinned to places, around the reader
            NotesMapSpace()
                .environment(store)
        }
        #endif
    }
}

Settings and MenuBarExtra are macOS-only types, and ImmersiveSpace is visionOS-only. Mentioning them outside the matching #if would fail to compile on the other platforms, and the #if is what makes the file legal everywhere. On visionOS, a button in NoteDetail opens the space through the environment:

// FieldNotesUI/NoteDetail.swift (excerpt)
#if os(visionOS)
@Environment(\.openImmersiveSpace) private var openImmersiveSpace

private var mapButton: some View {
    Button("Show on Map", systemImage: "map") {
        Task { await openImmersiveSpace(id: "map") }
    }
}
#endif

The scene types themselves (WindowGroup, Window, DocumentGroup, scene phases and the UIKit/AppKit life cycles) are covered on App Structure and Life Cycle (SwiftUI, UIKit, AppKit).

Native macOS, Mac Catalyst or "Designed for iPad"?

There are three ways to put an iPad app on a Mac, and they differ in effort, fidelity and reach:

Native macOS (SwiftUI/AppKit) Mac Catalyst Designed for iPad

Source changes

SwiftUI code mostly shared; AppKit where needed.

Recompile the UIKit app; adapt layouts and menus.

None. The iPad binary runs as-is.

SDK and frameworks

macOS SDK. All AppKit and macOS-only APIs.

iOS SDK built for Mac, plus Catalyst-specific UIKit APIs.

iOS SDK. Features the Mac lacks are unavailable.

Look and feel

Fully Mac.

iPad-like (Scale Interface to Match iPad) or Mac-like (Optimize Interface for Mac).

iPad appearance in a window.

Macs it runs on

Apple silicon; Intel Macs too, up to macOS 26 (a universal binary).

Apple silicon; Intel Macs too, up to macOS 26.

Apple silicon only.

Best for

New or SwiftUI apps: FieldNotes.

Large UIKit iPad apps that want a real Mac product.

iPad apps that need no Mac investment at all.

For a SwiftUI app the choice is easy: the native Mac destination gives the best result for the least work, and it is the default for multiplatform targets. Mac Catalyst remains the right tool for a UIKit codebase. An app that chooses it adds the Mac Catalyst destination, and then picks an idiom in the target’s General tab. With Optimize Interface for Mac, controls look and behave like their AppKit counterparts, and code can branch on the idiom at run time:

// A UIKit settings screen in a Catalyst build
let pinToTop = UISwitch()
pinToTop.preferredStyle = .checkbox                 // renders as an AppKit-style checkbox
if traitCollection.userInterfaceIdiom == .mac {
    pinToTop.title = "Always show pinned notes at the top"
}

macOS 27 (Golden Gate) runs only on Apple silicon, and it is the last macOS release that includes Rosetta. With a deployment minimum of 26, FieldNotes’s Mac build still runs on Intel Macs that stay on macOS 26 (Tahoe). The section’s Mac-specific material (the menu bar, windows, AppKit, sandboxing) is on macOS: Windows, Menus, Commands, Settings and Menu Bar Extras, and distribution outside the App Store is on Distributing Mac Apps with Developer ID and Notarization.

iPhone and iPad Apps on Apple Silicon Macs and Apple Vision Pro

Even with no Mac or visionOS destination, an iPhone or iPad app reaches both devices:

  • On a Mac with Apple silicon, iOS Apps on Mac runs the unmodified binary on the same infrastructure Catalyst uses. The App Store offers it to Mac users unless you opt out in App Store Connect. iPad apps that adopt multitasking get resizable windows. iPhone apps always run in a fixed-size window.

  • On Apple Vision Pro, compatible iPad and iPhone apps run as a window in the Shared Space. Xcode adds a Designed for iPad run destination automatically, so the compatibility build can be tested in the visionOS Simulator.

In both cases the app is running on hardware it wasn’t designed for: there may be no touch screen, no rear camera, no GPS and no accelerometer. Apple’s guidance is to detect features, not devices, and to use the "which host am I on?" flags only as a last resort:

// FieldNotesUI/CaptureButton.swift (excerpt) -- only matters for the iPad binary
var showsDocumentScanner: Bool {
    #if os(iOS)
    let info = ProcessInfo.processInfo
    if info.isiOSAppOnMac { return false }           // no rear camera on a Mac
    if #available(iOS 26.1, *), info.isiOSAppOnVision { return false }
    return true
    #else
    return false
    #endif
}

isiOSAppOnMac is true only for an iPhone/iPad binary running on a Mac. It is false in a Catalyst build, where isMacCatalystApp is true instead. isiOSAppOnVision (iOS 26.1 and later) is the visionOS counterpart. FieldNotes ships native Mac and visionOS builds, so its App Store listing replaces the compatibility versions. The check still matters in the window between a platform being added and that build being approved.

Moving from a compatible app to a native visionOS build is the Apple Vision destination described above. After adding it, remove the frameworks the visionOS SDK doesn’t include (for example WidgetKit and ActivityKit in the widget code paths), keep them behind canImport, and replace UIScreen-based layout with size-driven layout:

#if canImport(ActivityKit)
import ActivityKit          // Live Activities: false in visionOS builds, whose SDK lacks ActivityKit
#endif

Hardware-Aware Layout: ArrangementView (new in iOS 27.1)

Most adaptivity so far has been a compile-time choice between platforms. The newest SwiftUI container adapts at run time to the device’s shape. ArrangementView, new in iOS and iPadOS 27.1, takes a primary and a secondary view. It computes their arrangement from the environment: available size, size class, and hardware features such as the hinge of the foldable iPhone that ships with iOS 27.1. Its style decides the family of layouts:

  • .split (the default, via .automatic) places the two views side by side, along whichever axis fits. The .split.axes(_:) modifier limits the allowed axes.

  • .overlay layers the primary view over the secondary one, and can move them side by side when the device unfolds.

FieldNotes uses it inside NoteDetail on iPhone and iPad. The note text is primary, and a sidebar of linked notes and tags is secondary. Folded, the sidebar stacks below the text. Unfolded, the two sit on either side of the hinge:

// FieldNotesUI/NoteDetail.swift (excerpt)
struct NoteDetail: View {
    let note: Note

    var body: some View {
        #if os(iOS)
        if #available(iOS 27.1, *) {                   // new in iOS 27.1
            ArrangementView {
                NoteEditor(noteID: note.id)
            } secondary: {
                RelatedNotesSidebar(note: note)        // linked notes and TagChip list
            }
            .arrangementViewStyle(.split.axes([.horizontal, .vertical]))
        } else {
            NoteEditor(noteID: note.id)
                .inspector(isPresented: .constant(true)) {
                    RelatedNotesSidebar(note: note)
                }
        }
        #else
        NoteEditor(noteID: note.id)
        #endif
    }
}

ArrangementView exists only on iOS and iPadOS, so the #if os(iOS) keeps it out of the Mac and visionOS builds, and the #available(iOS 27.1, *) check keeps it off devices on 26.x and 27.0. The inspector fallback is the pre-27.1 way to show the same sidebar. The companion 27.1 APIs for reserving space around the hinge (ReservedRegion, DeviceHinge/UIHinge) belong to layout and are covered on SwiftUI Layout and iPadOS: Windows, Multitasking, Pointer and Apple Pencil.

Keeping a Multiplatform Codebase Healthy

A few habits keep one codebase from turning into five apps in one repository:

  1. Push platform differences to the edges. Model and service code (FieldNotesKit) should contain almost no #if. If a service needs a platform capability, define a protocol there and implement it per platform in the app target. This is the injection pattern described on App Architecture the Apple Way.

  2. Prefer canImport and availability over os(). Capability checks survive new platforms and new OS releases, while os() lists need editing every time.

  3. Prefer file filters to whole-file #if. A file that exists for one platform should say so in its target membership, not in a #if os(macOS) around 300 lines.

  4. Build every destination in CI. An iOS-only CI job won’t catch a macOS or watchOS compile error. Xcode Cloud and xcodebuild can build each destination (-destination 'generic/platform=visionOS'), as shown on CI/CD with Xcode Cloud and the Command Line.

  5. Preview on each platform. #Preview canvases can switch run destination, so a view in FieldNotesUI can be checked on iPhone, Mac and Apple Vision without running the app.

# Build every destination of the FieldNotes scheme, then the watch app
for platform in iOS macOS visionOS; do
  xcodebuild build -project FieldNotes.xcodeproj -scheme FieldNotes \
    -destination "generic/platform=${platform}"
done
xcodebuild build -project FieldNotes.xcodeproj -scheme FieldNotesWatch \
  -destination 'generic/platform=watchOS'

Apple’s Food Truck sample is the canonical reference for this whole page. It is one app target that builds for iOS, iPadOS and macOS, with a NavigationSplitView layout like FieldNotes’s ContentView, and a separate widget extension. The WWDC22 session Use Xcode to develop a multiplatform app walks through converting a two-target project into one multiplatform target.