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 |
|---|---|---|
|
iPhone, iPad, Mac, Apple Vision |
One SwiftUI |
|
Apple Watch |
watchOS apps can’t join a multiplatform target. The watch app has its own |
|
iPhone, iPad, Mac, Apple Vision |
A widget extension, added on Widgets, Complications and Controls (WidgetKit). Extensions are separate targets. |
|
Apple Watch |
The watch’s own widget extension, for complications and Smart Stack widgets. It also comes from Widgets, Complications and Controls (WidgetKit). |
|
Every destination the app supports |
Swift Testing, added on Unit Testing with Swift Testing. |
Figure: How FieldNotes’s packages feed its app targets
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 |
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 |
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 emptyif #availablebranch with anelse. -
#availablecan appear only in the condition list of anif,guardorwhile. 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 asArrangementViewbelow.
Figure: API-availability patterns
| Pattern | Evaluated | Question it answers | FieldNotes example |
|---|---|---|---|
|
Compile time |
Is this build for this OS? |
|
|
Compile time |
Does this SDK contain the module? |
UIKit or AppKit haptics in |
|
Compile time |
Is this a Simulator build? |
Seeding |
|
Compile time |
Is this iOS code built as a Catalyst app? |
Mac-specific labels in the shared |
|
Compile time |
Is a custom flag defined for this build? |
visionOS-only code in |
|
Run time |
Is the running OS new enough? |
The on-device summary button (new in 27). |
|
Run time |
Is the running OS older than this? |
One-time pin migration on 26. |
|
Compile time (enforced at call sites) |
Can this declaration be used from this code? |
|
|
Compile time |
Should this declaration be absent from one platform? |
|
|
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. -
.overlaylayers 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:
-
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. -
Prefer
canImportand availability overos(). Capability checks survive new platforms and new OS releases, whileos()lists need editing every time. -
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. -
Build every destination in CI. An iOS-only CI job won’t catch a macOS or watchOS compile error. Xcode Cloud and
xcodebuildcan build each destination (-destination 'generic/platform=visionOS'), as shown on CI/CD with Xcode Cloud and the Command Line. -
Preview on each platform.
#Previewcanvases can switch run destination, so a view inFieldNotesUIcan 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.
References
-
Apple Developer Documentation — Configuring a multiplatform app
-
Apple Developer Documentation — Running code on a specific platform or OS version
-
Apple Developer Documentation — Adding a build configuration file to your project
-
Apple Developer Documentation — Configuring the build settings of a target
-
Apple Developer Documentation — Setting up a watchOS project
-
Apple Developer Documentation — Marking API availability in Objective-C
-
Apple Developer Documentation — Choosing a user interface idiom for your Mac app
-
Apple Developer Documentation — Running your iOS apps in macOS
-
Apple Developer Documentation — Bringing your existing apps to visionOS
-
Apple Developer Documentation — Making your existing app compatible with visionOS
-
Apple Developer Documentation — Food Truck: Building a SwiftUI multiplatform app