How is a watch app set up in Xcode alongside the iPhone app: targets, shared modules, and what races does Watch Connectivity create?

20.5.3 · L5 · Interview

Predict first

Where does the companion-installed check exist?

import WatchConnectivity

let installed = WCSession.default.isCompanionAppInstalled
  • It compiles on watchOS
  • It is unavailable on watchOS

Predict first

And on iOS:

import WatchConnectivity

let installed = WCSession.default.isCompanionAppInstalled
  • It compiles
  • It is an error: unavailable in iOS

Mental model. The iPhone and the watch are two offices with a courier between them. Either office can be closed, the courier delivers some packages immediately and others later, and a package can arrive after the office has moved on, so every message must make sense on its own.

Today a watch app is its own watchOS app target in the same project as the iPhone app (the old watch app and extension pair is gone for new projects). Two shapes: a watch app for an existing iPhone app (the watch target is embedded in the iPhone app’s bundle and installed through the Watch app on the iPhone, optionally with the “Show App on Apple Watch” choice) and an independent watch app (WKRunsIndependentlyOfCompanionApp, which can ship alone). The watch target has its own bundle id (conventionally the iPhone’s id plus .watchkitapp), Info.plist, capabilities (HealthKit, App Groups, Keychain Sharing, background modes) and entitlements, and it signs and deploys separately, so keep the bundle ids, app group and Keychain access group identifiers aligned with the phone app so data and credentials can be shared. Shared modules are the way to avoid duplication: put models, networking, persistence and view models in Swift packages that declare .iOS and .watchOS platforms and avoid UIKit-only APIs, and compile the shared UI (SwiftUI views) for both, with platform conditions at the edges. Watch builds must stay small and fast to start (link only what is needed), and CI must build both targets on every change (see the multiplatform release question).

Watch Connectivity races. The session is asynchronous and either side can be absent, so several races are common. Activation: nothing can be sent until activationDidCompleteWith fires; queue sends until then. Installation state: on the iPhone isWatchAppInstalled tells you the watch app exists (and isPaired that a watch is paired), while on the watch isCompanionAppInstalled tells you the iPhone app is installed (the API is unavailable on the other platform, which is a compile error if you mix them up). Do not send to a missing counterpart, and refresh the UI when installation changes (sessionWatchStateDidChange). Reachability changes constantly (sessionReachabilityDidChange), so use sendMessage only when reachable and have a fallback to a queued transfer. Ordering and duplicates: application context keeps only the latest value; user-info and file transfers queue and can arrive late, out of step with the app’s current state, and after a reinstall: make handlers idempotent and version the payloads. Outstanding transfers: inspect outstandingUserInfoTransfers and outstandingFileTransfers, cancel stale ones (for example old settings), and avoid unbounded queues. Multiple watches: the iPhone session deactivates when the user switches watches (sessionDidDeactivate: activate again). Race between app and complication: update shared state, then reload timelines, in that order. Test with one device offline, reinstall of either app, background launches, and sending while the counterpart is updating, and log every delegate callback with timestamps, since these races rarely reproduce on demand.

The watch asks whether its companion iPhone app is installed, so the property is a watchOS API.

On iOS the equivalent question is isWatchAppInstalled; the companion property is unavailable there.

Common traps

  • Mixing up isWatchAppInstalled and isCompanionAppInstalled. Each exists on one platform only; the wrong one is a compile error.
  • Sending before the session finishes activating. The calls fail or are dropped; queue until activation completes.

They might follow up with

  • Which side asks whether the iPhone app is installed? The watch, with isCompanionAppInstalled.
  • How do you avoid unbounded queued transfers? Inspect and cancel stale outstanding transfers.

Related questions

  • 20.5.2 How do on-watch workouts and health data, and networking directly or through the iPhone, work on watchOS?
  • 20.1.3 How do you run one shared core with several UI leaves on a single release train?
  • 19.6.2 How does Watch Connectivity work: the transfer types, reachability, and how do you choose between them?

← How do on-watch workouts and health data, and networking directly or…