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

The Watch Target: Embedded, Explicit, and Checked in the Archive

~16 min · on-the-wrist, watchos, xcodegen, info-plist, simulator

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"A build setting is a statement of intent; the bundle is the fact."

One Bundle, Three Products

A family watch app is not a separate app on App Store Connect. It is a watchOS application target embedded in the iOS app's Watch folder, and its complication is a WidgetKit extension embedded in the watch app. One archive, one build number, one TestFlight upload, and the watch app installs from the phone. In XcodeGen that is three targets and two embed dependencies. The watch app's bundle identifier extends the phone's (…mobile.watchkitapp), the complication's extends the watch app's (…watchkitapp.widgets), and the watch app's Info.plist names its companion with WKCompanionAppBundleIdentifier. Built for an iPhone Simulator for this lesson, the product had exactly that shape: SparkMobile.app/Watch/Spark.app with SparkWatchWidgets.appex inside.

The Upload That Was Refused Afterwards

The journal app's first TestFlight build with a watch app uploaded successfully and was then refused by App Store Connect: no icons found for the watch application, and CFBundleIconName missing from its Info.plist. Everything local was reassuring. The build was green, Assets.car sat in the watch bundle, and the build setting that names the icon was set. But GENERATE_INFOPLIST_FILE does not write CFBundleIconName for a watchOS target, and PlistBuddy on the built watch app said so. The fix is an explicit Info.plist for the watch target carrying CFBundleIconName, WKApplication and the companion identifier, and a check on the archive in the upload gate, which for a watch app requires that top-level key, a compiled catalog, and the complication's extension actually present.

Seeing It Without a Watch

Building only the watch scheme with -sdk watchsimulator failed in native Pippa before compiling anything, with "Multiple commands produce …/Pippa.app/Pippa": -sdk applies to every target the scheme touches, so the iOS app was built for the watch SDK too and collided with the watch app of the same product name. Build the iOS scheme for an iPhone Simulator instead. The embedded watch app installs directly onto a booted watch simulator with simctl install, no pairing required, which is enough to see the interface. It is not enough for anything involving dictation, which only a real watch proves.

A Real Watch by Cable Is Still a Build Check

Installing on a real watch by cable took three separate gates: Developer Mode switched on in the watch's own settings, a scheme for the watch target so xcodebuild can be pointed at the watch and register it, and Xcode's device layer knowing the watch, which xcrun xcdevice list shows as available. None of it is delivery. TestFlight installs the watch app through the phone, signed like a release.

Code

project.yml: the watch app embedded in the phone app, the complication embedded in the watch app·yaml
  SparkMobile:
    # ...
    dependencies:
      - target: SparkWatch               # the watch app rides inside the iOS bundle
        embed: true
        copy:
          destination: productsDirectory
          subpath: "$(CONTENTS_FOLDER_PATH)/Watch"

  SparkWatch:
    type: application
    platform: watchOS
    deploymentTarget: "10.0"
    scheme: {}                           # a watch build destination, for cable checks only
    sources:
      - path: Watch
    dependencies:
      - target: SparkWatchWidgets        # the complication rides inside the watch app
        embed: true
    settings:
      base:
        PRODUCT_NAME: Spark
        PRODUCT_BUNDLE_IDENTIFIER: com.example.spark.mobile.watchkitapp
        TARGETED_DEVICE_FAMILY: "4"
        GENERATE_INFOPLIST_FILE: NO      # it would not write CFBundleIconName for watchOS
        INFOPLIST_FILE: Watch/Info.plist # CFBundleIconName, WKApplication, WKCompanionAppBundleIdentifier
        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
        SKIP_INSTALL: YES

  SparkWatchWidgets:
    type: app-extension
    platform: watchOS
    deploymentTarget: "10.0"
    sources:
      - path: WatchWidgets
    info:
      path: WatchWidgets/Info.plist
      properties:
        CFBundleShortVersionString: $(MARKETING_VERSION)
        CFBundleVersion: $(CURRENT_PROJECT_VERSION)
        NSExtension:
          NSExtensionPointIdentifier: com.apple.widgetkit-extension
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.example.spark.mobile.watchkitapp.widgets
        TARGETED_DEVICE_FAMILY: "4"
        SKIP_INSTALL: YES
Build once, check the embedded bundles, and run the watch app on a simulator·bash
# Build the iOS scheme for an iPhone Simulator. Do NOT add -sdk watchsimulator: -sdk overrides
# every target in the scheme, and an iOS app and a watch app with the same product name then
# collide with "Multiple commands produce".
xcodebuild -project SparkMobile.xcodeproj -scheme SparkMobile \
  -destination 'generic/platform=iOS Simulator' -derivedDataPath build/dd build

app=$(find build/dd -path '*Debug-iphonesimulator*' -name SparkMobile.app | head -1)
ls "$app/Watch"                                                   # Spark.app
ls "$app/Watch/Spark.app/PlugIns"                                 # SparkWatchWidgets.appex
/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$app/Watch/Spark.app/Info.plist"   # AppIcon

# The embedded watch app installs straight onto a booted watch simulator; no pairing needed.
xcrun simctl install "$WATCH_SIMULATOR_ID" "$app/Watch/Spark.app"
xcrun simctl launch "$WATCH_SIMULATOR_ID" com.example.spark.mobile.watchkitapp
sleep 4 && xcrun simctl io "$WATCH_SIMULATOR_ID" screenshot build/watch.png
# A clock face in the screenshot is not a crash: read the log before deciding.

External links

Exercise

Add SparkWatch and SparkWatchWidgets to SparkMobile's project.yml, generate, and build the iOS scheme for an iPhone Simulator. Confirm the watch app and its extension are embedded and print the watch app's CFBundleIconName. Then set GENERATE_INFOPLIST_FILE: YES on the watch target with an AppIcon asset in its catalog, rebuild, and print the key again. Finally, install the watch app on a watch simulator and take a screenshot of it running.
Hint
If the screenshot shows the clock face right after booting, relaunch the app and take it again a few seconds later, and read xcrun simctl spawn <watch> log show --last 3m before calling it a crash. Put the upload gate's check for the watch icon key in your pipeline, not only in your memory.

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.