
Recommendation for this task: request a Figment M-series Mac from $95/month so a single rented machine can build and simulate iOS, macOS and watchOS from one Xcode project. The specific chip, RAM and disk are confirmed when Figment returns the quote; physical plans collect requests until supply is confirmed, so ask for an evaluation window before you commit. If your targets are iOS plus watchOS only and you do not need arm64-only simulators, the i3 Mac at $49/month is a cheaper entry. The $29 virtual Intel macOS plan runs Intel-compatible Xcode toolchains (Xcode 26 or earlier) and can cross-compile and sign supported arm64 device apps, but it cannot execute arm64-only binaries or Xcode 27. Start a request at /mac-hosting/start.
A multiplatform Xcode project ships one project file with schemes whose destinations cover iOS, macOS and watchOS. The Swift package manifest declares its minimum deployment version per platform, and Xcode honours those versions for each destination:
// Package.swift
let package = Package(
name: "MySharedLib",
platforms: [
.iOS(.v16),
.macOS(.v13),
.watchOS(.v9),
]
)
Shared source gates platform-only APIs behind #if os(...) so the compiler only sees symbols that exist for the current destination:
import SwiftUI
struct HeartRateView: View {
var body: some View {
#if os(watchOS)
HeartRateSensor() // HealthKit live session
#elseif os(iOS)
HeartRateChart() // HealthKit history on iOS
#else
Text("Synced from your paired device.")
#endif
}
}
For a dependency that only makes sense on some platforms, attach a condition rather than wrapping the import in every file. The .when(platforms:) modifier is documented in Apple's PackageDescription reference:
.target(
name: "MyFeature",
dependencies: [
"SharedModels",
.product(name: "Analytics", package: "analytics-swift",
condition: .when(platforms: [.iOS, .macOS]))
]
)
The destination syntax (platform=...,name=...,OS=...) is defined in Apple's TN2339 "Building from the Command Line with Xcode FAQ". Run this from the project root on the hosted Mac. Replace the workspace and scheme names with your own — find them with xcodebuild -list -workspace YourApp.xcworkspace.
#!/bin/zsh
# -e intentionally omitted so all three destinations run even if one fails.
set -uo pipefail
WORKSPACE="YourApp.xcworkspace"
IOS_SCHEME="YourApp"
MACOS_SCHEME="YourApp (macOS)"
WATCH_SCHEME="YourApp Watch App"
RESULT_FILE=$(mktemp "${TMPDIR:-/tmp}/build-summary.XXXXXX")
trap 'rm -f "$RESULT_FILE"' EXIT
run_build() {
local label="$1" scheme="$2" destination="$3"
echo "==> Building $label..."
if xcodebuild build \
-workspace "$WORKSPACE" \
-scheme "$scheme" \
-destination "$destination" \
-derivedDataPath "./DerivedData/$label" \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \
2>&1 | tee "./build_${label}.log" | tail -3; then
echo "$label: PASS" >> "$RESULT_FILE"
else
echo "$label: FAIL (see build_${label}.log)" >> "$RESULT_FILE"
fi
}
run_build "iOS" "$IOS_SCHEME" "generic/platform=iOS Simulator"
run_build "macOS" "$MACOS_SCHEME" "platform=macOS,arch=arm64"
run_build "watchOS" "$WATCH_SCHEME" "generic/platform=watchOS Simulator"
echo
echo "===== Build Summary ====="
cat "$RESULT_FILE"
A few notes so the script matches reality:
tee because set -o pipefail is on.generic/platform=... compiles for the simulator SDK without booting a device. Switch to platform=iOS Simulator,name=iPhone 16,OS=latest only when a specific simulator is needed for tests.arch=arm64 builds the macOS target for Apple silicon. On the $29 Intel VM substitute arch=x86_64; it cannot execute arm64 code.platform=macOS,variant=Mac Catalyst.After the build loop is clean, run tests per destination with real simulator names so the harness actually boots:
xcodebuild test -workspace "$WORKSPACE" -scheme "YourApp" \
-destination "platform=iOS Simulator,name=iPhone 16,OS=latest"
xcodebuild test -workspace "$WORKSPACE" -scheme "YourApp (macOS)" \
-destination "platform=macOS,arch=arm64"
xcodebuild test -workspace "$WORKSPACE" -scheme "YourApp Watch App" \
-destination "platform=watchOS Simulator,name=Apple Watch Series 10 (46mm),OS=latest"
A failed xcodebuild test exits non-zero. Use set -e if you want the run to abort on the first failing platform, or keep the earlier loop pattern if you want every destination to report.
Buyers comparing entry prices for an Apple-silicon rental should see both numbers. CloudMac publishes $99.99/month on an M4 with 16 GB RAM and 256 GB SSD, and the home page lists a $999/year option on the same base tier — that works out to $83.25/month effective, and $999 is $141 cheaper than twelve months at Figment's $95 entry (12 × $95 = $1,140). CloudMac's 512 GB and 1 TB rows currently show "Fully reserved, Next allocation not scheduled", so availability is per-SKU.
Figment's $95 is an unspecified M-series entry, not a confirmed M4/16 GB match, so this is an entry-price comparison, not equal hardware. When the useful difference is CI work rather than a rental seat, Figment's Managed Build Mac at $249/month bundles one M-series Mac, CI setup, backups, monitoring and 30 minutes of monthly support — work neither of the self-serve rental tiers performs for you. Tax and any setup fees are whatever appears on the actual quote.
Three clean builds for a mid-size app typically accumulate several GB of derived data. Keeping each platform under its own -derivedDataPath (as the loop does) stops a stale product on one platform from masking a real error on another. Xcode already schedules independent targets in parallel within a scheme, so measure wall-clock time on your own project before buying more RAM: memory contention and duplicated transitive dependencies often matter more than core count.
Add the loop to a self-hosted runner on your Figment Mac to catch cross-platform regressions on every push. Request your machine at /mac-hosting/start.
Related guides: