Skip to content
C.W.K.
Stream
Lesson 06 of 06 · published

One Swift Package, Many Apps and Platforms

~16 min · packages-projects, shared-code, platform-fences, build-matrix, kit

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"443 tests passed four separate times while the iOS code did not compile at all."

Where Shared Code Lives

Two kinds of sharing show up in a family of Apple apps. Within one product, a Mac app and its iPhone app share a model, a wire protocol and a client core. Across products, a dozen apps share the same hardened HTTP transport, document store, outbox and watch link. The family answers both with Swift packages rather than copies. A Mac terminal-and-agent client, for example, keeps a protocol module with no platform guards at all and a client-core module where only the transport differs by platform — a child process on the Mac, a WebSocket to the home gateway on the phone. When the phone client arrived, it linked the same two modules instead of re-implementing them.

The shared family kit is one package with several products (core, web host, speech, media, Mac chrome, watch link, push, server, and server test helpers), consumed by path. A handful of tiny single-file helpers are still distributed as vendored copies, kept byte-identical by a sync script and a drift test in every consumer; the rule for which mechanism applies is simply whether the file depends on another kit file. Kit files are edited in the kit, never in a consumer.

Fences Decide What Compiles

Platform-specific code sits behind compile-time conditions: #if os(macOS), #if canImport(UIKit) && !os(watchOS), #if canImport(WatchConnectivity). A fence does not just skip code at run time — code outside the current platform is not compiled at all. That is where the family's kit fooled itself: swift test on a Mac builds the package for macOS only. On one day it ran 443 tests green four times while the iOS and watchOS code behind the fences contained a wrist enum that did not compile on iOS, a delegate conformance Swift 6 refused, completion handlers flagged as data races, and an API that does not exist on watchOS — the last of them landed thirty minutes after a commit, with a green suite.

The check is to build each product for each platform it claims, with xcodebuild pointed at the package directory and a generic destination. A shared -derivedDataPath keeps the matrix incremental: the family's matrix builds nine products for iOS and eight of them for watchOS, 17 builds in all, in about 49 seconds cold and 24 warm, and the kit's own test runner calls it.

Apps First, Kit Monitored

A shared layer that apps must wait for is a shared layer that ships nothing. The family's ruling is that an app implements a missing capability locally at once — labelled app-owned, pending migration, never presented as a kit copy — while watching the kit, and adopts the kit version once it can really replace the local one, deleting the duplicate and verifying behaviour did not change. Temporary duplication is allowed; unlabelled duplication is not, because no drift check can see a copy it does not know about.

Code

Build every product for every platform it claims (run in the package directory)·bash
#!/bin/zsh
set -euo pipefail
DERIVED="${TMPDIR}spark-kit-matrix"

typeset -A platforms
platforms=(
  SparkCore  "macOS iOS watchOS"
  SparkMac   "macOS"
  SparkPhone "iOS"
)

for product in ${(k)platforms}; do
  for platform in ${=platforms[$product]}; do
    log="${DERIVED}-${product}-${platform}.log"
    set +e
    xcodebuild -scheme "$product" -destination "generic/platform=$platform" \
      -derivedDataPath "$DERIVED" build > "$log" 2>&1
    code=$?
    set -e
    if (( code != 0 )) || grep -q '\*\* BUILD FAILED \*\*' "$log"; then
      print "HARD_FAIL $product on $platform (see $log)"; exit 1
    fi
    print "ok  $product  $platform"
  done
done
Fences: code outside the current platform is not compiled at all·swift
// Sources/SparkPhone/Haptics.swift
#if canImport(UIKit) && !os(watchOS)
import UIKit

@MainActor
public enum CaptureHaptics {
    public static func saved() {
        UINotificationFeedbackGenerator().notificationOccurred(.success)
    }
}
#endif
// `swift test` on a Mac never type-checks the lines above.
// `xcodebuild -scheme SparkPhone -destination 'generic/platform=iOS' build` does.

External links

Exercise

Add the Haptics file to SparkPhone, then deliberately introduce a type error inside the #if canImport(UIKit) block. Run swift test on your Mac and confirm it passes. Save the matrix script as scripts/build-matrix.sh in SparkKit, run it, and confirm it fails with a HARD_FAIL naming SparkPhone on iOS. Fix the error and record the cold and warm run times of the matrix.
Hint
Run the matrix twice in a row without cleaning; the second run reuses the shared derived-data folder. If xcodebuild cannot find a scheme, make sure you are in the package directory — xcodebuild discovers the package from the current directory, not from a flag.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.