name: Strand options: deploymentTarget: macOS: "13.0" createIntermediateGroups: true groupSortPosition: top # Base/development language. The in-source UI strings are authored in English (US); # the String Catalog (Strand/Resources/Localizable.xcstrings) carries this as the # source language and is where additional languages are added. developmentLanguage: en # Project-level xcconfig: Xcode layers this beneath every target's OWN configFiles (a target-level # xcconfig only shadows keys it redefines itself), so this is the one place BUNDLE_ID_PREFIX is # defined — every target picks it up automatically, including Strand/StrandTests/NOOPiOS, which also # carry Strand/Oura/OuraConfig.xcconfig for their unrelated Oura credentials. See Config/BundleId.xcconfig. configFiles: Debug: Config/BundleId.xcconfig Release: Config/BundleId.xcconfig settings: base: MARKETING_VERSION: "11.1.0" # iOS build number (CFBundleVersion). GLOBAL so the iOS app AND its widget extension inherit the # SAME value — they must match or iOS warns "extension version must match parent app" (#416). # macOS sets its own CFBundleVersion on the Strand target and is unaffected by this. CURRENT_PROJECT_VERSION: "294" SWIFT_VERSION: "5.0" # Emit localizable strings so Xcode auto-extracts every LocalizedStringKey into the # String Catalog on build (the English (US) base entries). SWIFT_EMIT_LOC_STRINGS: YES SWIFT_STRICT_CONCURRENCY: minimal CODE_SIGN_STYLE: Automatic # Single source of truth for the App Group id shared by the iOS app + its widget / Live Activity # extension. Referenced as $(APP_GROUP_ID) by both targets' entitlements and Info.plist, and read # at runtime via the AppGroupIdentifier Info.plist key (WidgetSnapshot.suiteName) so the value is # never hard-coded in Swift. Built from $(BUNDLE_ID_PREFIX) (see Config/BundleId.xcconfig) — set # BUNDLE_ID_PREFIX in a gitignored Config/BundleIdSecrets.xcconfig to build under your own Apple ID. APP_GROUP_ID: group.$(BUNDLE_ID_PREFIX).noop.staging # Build for both Apple Silicon and Intel so the release runs on every Mac. ARCHS: "$(ARCHS_STANDARD)" configs: Release: # Distribution builds must produce a universal binary, not just the host arch. ONLY_ACTIVE_ARCH: NO packages: # Renders the Coach's LLM replies (GitHub-flavored Markdown — bold, lists, # headings, tables). Pinned exactly for reproducible community builds. MarkdownUI: url: https://github.com/gonzalezreal/swift-markdown-ui exactVersion: 2.4.1 WhoopProtocol: path: Packages/WhoopProtocol WhoopStore: path: Packages/WhoopStore StrandAnalytics: path: Packages/StrandAnalytics StrandImport: path: Packages/StrandImport StrandDesign: path: Packages/StrandDesign # Clean-room local Oura ring BLE protocol (framing, auth, decoders, driver). Pure value types, # zero CoreBluetooth, so it stays headless/JVM-testable; the app target imports it for OuraLiveSource. OuraProtocol: path: Packages/OuraProtocol # Clean-room local Polar sensor facts: model identification + PMD stream capability catalog (pure value # types, zero CoreBluetooth, headless/JVM-testable). The app target imports it so StandardHRSource can # log which Polar model a connecting strap is, and the Test Centre can auto-detect it from the paired # record. The live PMD data path is not wired yet — this is identification/diagnostics only. PolarProtocol: path: Packages/PolarProtocol # Compresses the full-database `.noopbak` backup (single-entry deflate ZIP) in DataBackup.swift. # Already resolved transitively via StrandImport; linked directly so the app target can # `import ZIPFoundation`. Same exact version as StrandImport's, so SPM resolves one copy. # Supply-chain: pinned EXACT (not `from:`) so a clean resolve can't auto-pull a newer — # potentially compromised — upstream release. Bump deliberately, in lockstep with the # Packages/*/Package.swift exact constraints and the committed Package.resolved. ZIPFoundation: url: https://github.com/weichsel/ZIPFoundation.git exactVersion: 0.9.20 targets: Strand: type: application platform: macOS deploymentTarget: "13.0" sources: - path: Strand excludes: - "Resources/Info.plist" - "Resources/Strand.entitlements" # The liquid Today is now cross-platform: its iOS-only chrome (topBarTrailing / navigationBarTitleDisplayMode # / presentationCompactAdaptation / two-param onChange / sensoryFeedback) is guarded, so the mac split-view # shell hosts LiquidTodayView too. All Liquid/* now compile into the mac target. # Oura BYO-app OAuth credentials. OuraConfig.xcconfig is a tracked wrapper that optionally # includes the untracked, gitignored Strand/Oura/OuraSecrets.xcconfig (template at # OuraSecrets.example.xcconfig) — absent secrets leave the $(OURA_*) references in the # info.properties below empty, which disables the Oura Cloud lane at runtime. configFiles: Debug: Strand/Oura/OuraConfig.xcconfig Release: Strand/Oura/OuraConfig.xcconfig info: path: Strand/Resources/Info.plist properties: CFBundleName: NOOP CFBundleDisplayName: NOOP CFBundleShortVersionString: "$(MARKETING_VERSION)" CFBundleVersion: "198" LSMinimumSystemVersion: "13.0" LSApplicationCategoryType: public.app-category.healthcare-fitness NSBluetoothAlwaysUsageDescription: "NOOP connects directly to your WHOOP strap over Bluetooth to read heart rate, R-R intervals, battery, and sensor data locally on your Mac. Nothing leaves your device." NSLocationWhenInUseUsageDescription: "NOOP records your route during an outdoor workout to show distance, pace, and a map. The route stays on your device. Nothing leaves it." # The AI Coach's "Custom (OpenAI-compatible)" provider talks to a LOCAL LLM server over plain # HTTP on loopback (Ollama / LM Studio / llama.cpp at http://localhost:…). ATS blocks cleartext # by default; NSAllowsLocalNetworking exempts loopback/.local/unqualified hosts ONLY — public # HTTPS endpoints are unaffected. No arbitrary-loads blanket exemption. NSAppTransportSecurity: NSAllowsLocalNetworking: true # Oura BYO-app OAuth credentials (values come from configFiles above); read at runtime via # OuraCredentials.fromBundle. Absent/blank values cleanly disable the Connect-Oura lane. OURA_CLIENT_ID: $(OURA_CLIENT_ID) OURA_CLIENT_SECRET: $(OURA_CLIENT_SECRET) OURA_REDIRECT_URI: $(OURA_REDIRECT_URI) # Registers the noop:// URL scheme so ASWebAuthenticationSession's OAuth redirect # (noop://oura/callback) reaches OuraOAuthProvider instead of bouncing off the OS. CFBundleURLTypes: - CFBundleURLSchemes: [noop] entitlements: path: Strand/Resources/Strand.entitlements properties: com.apple.security.app-sandbox: true com.apple.security.device.bluetooth: true com.apple.security.files.user-selected.read-write: true # Outbound client access — used ONLY by the opt-in, consent-gated, BYOK AI Coach. # The ad-hoc distributed build applies NO entitlements (unsigned build + ad-hoc # re-sign), so this matters only for a SIGNED/sandboxed build (where the sandbox # otherwise blocks the Coach's request — #128). Data path stays network-free. com.apple.security.network.client: true settings: base: PRODUCT_BUNDLE_IDENTIFIER: $(BUNDLE_ID_PREFIX).noop.staging PRODUCT_NAME: "NOOP Staging" # Pin the Swift module name so `@testable import Strand` resolves even though PRODUCT_NAME is # NOOP — the test target and shared sources refer to the app module as `Strand`. PRODUCT_MODULE_NAME: Strand ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon GENERATE_INFOPLIST_FILE: NO # Hardened Runtime closes the DYLD_INSERT_LIBRARIES injection vector for the # distributed (ad-hoc) build. The app uses no JIT / unsigned-exec memory, so it's safe. ENABLE_HARDENED_RUNTIME: YES COMBINE_HIDPI_IMAGES: YES dependencies: - package: WhoopProtocol - package: WhoopStore - package: StrandAnalytics - package: StrandImport - package: StrandDesign - package: MarkdownUI - package: ZIPFoundation - package: OuraProtocol - package: PolarProtocol StrandTests: type: bundle.unit-test platform: macOS deploymentTarget: "13.0" sources: - StrandTests # Compile the exact cross-target snapshot source into the test bundle so the AltStore App Group # resolver is covered without adding this iOS/widget-only type to the macOS application module. - StrandiOSShared/WidgetSnapshot.swift # Same wrapper as the app target, so the OURA_CLOUD_IMPORT-gated Oura tests compile exactly # when the app's Oura lane does (flag + creds both come from the untracked OuraSecrets.xcconfig). configFiles: Debug: Strand/Oura/OuraConfig.xcconfig Release: Strand/Oura/OuraConfig.xcconfig dependencies: - target: Strand - package: WhoopProtocol - package: WhoopStore - package: OuraProtocol settings: base: GENERATE_INFOPLIST_FILE: YES PRODUCT_BUNDLE_IDENTIFIER: $(BUNDLE_ID_PREFIX).strandtests # Host the unit tests in the app so `@testable import Strand` links against the real app module. # NOTE: must track the app target's PRODUCT_NAME ("NOOP Staging" since the staging rename). TEST_HOST: "$(BUILT_PRODUCTS_DIR)/NOOP Staging.app/Contents/MacOS/NOOP Staging" BUNDLE_LOADER: "$(TEST_HOST)" # --------------------------------------------------------------------------- # iOS app target (community build-from-source; never distributed via the App # Store — NOOP stays anonymous). Reuses the five shared packages + most of the # macOS app's Strand/ sources; the macOS-only scene/menu-bar/notification files # are excluded, and the iOS app shell + HealthKit + widgets/intents live under # StrandiOS/. See docs/IOS.md. NOOPiOS: type: application platform: iOS deploymentTarget: "17.0" sources: - path: Strand excludes: - "Resources/Info.plist" - "Resources/Strand.entitlements" - "Resources/Assets.xcassets" - "App/StrandApp.swift" - "App/RootView.swift" # ContentView embeds RootView() (the macOS NavigationSplitView sidebar shell), which is # excluded above — so ContentView cannot compile on iOS. The iOS shell is RootTabView, # wired in StrandiOS/App/StrandiOSApp.swift (which reproduces ContentView's onboarding/ # Terms/What's-New gates around the TabView). Without this exclude the iOS target fails to # resolve RootView(). - "App/ContentView.swift" - "MenuBar" - "Data/NotificationSettingsStore.swift" - "Screens/NotificationSettingsView.swift" # macOS-only AppIntents (BuzzStrap/MarkMoment) — iOS uses StrandiOS/System/NOOPAppIntents.swift. # Without this exclude both compile into the iOS target and collide on NOOPAppIntents.stringsdata. - "System/NOOPAppIntents.swift" - path: StrandiOS excludes: - "Resources/Info.plist" - "Resources/NOOP.entitlements" - path: StrandiOSShared # Same tracked wrapper as the macOS target (Strand/Oura/ is shared, not per-platform). configFiles: Debug: Strand/Oura/OuraConfig.xcconfig Release: Strand/Oura/OuraConfig.xcconfig info: path: StrandiOS/Resources/Info.plist properties: CFBundleName: NOOP CFBundleDisplayName: NOOP CFBundleShortVersionString: "$(MARKETING_VERSION)" CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" # Surfaces APP_GROUP_ID to runtime (WidgetSnapshot.suiteName) so the App Group id isn't # hard-coded in Swift; stays in sync with the application-groups entitlement (both $(APP_GROUP_ID)). AppGroupIdentifier: "$(APP_GROUP_ID)" LSApplicationCategoryType: public.app-category.healthcare-fitness UILaunchScreen: {} # Alternate app icons (v3 "Titanium & Gold"). The primary icon stays the asset-catalog # AppIcon (machined titanium); the alternate "AppIcon-Navy" (blued titanium) is declared # here so UIApplication.setAlternateIconName("AppIcon-Navy") can switch to it at runtime. # CFBundleIconFiles is intentionally empty — the artwork lives in the asset catalog, shipped # via ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS, so no loose PNGs are listed here. CFBundleIcons: CFBundleAlternateIcons: AppIcon-Navy: CFBundleIconFiles: [] UIPrerenderedIcon: false NSSupportsLiveActivities: true UISupportedInterfaceOrientations: - UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad: - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UIBackgroundModes: - bluetooth-central # #524: keep recording an outdoor workout's GPS route while the app is backgrounded (screen off), # matching Android's foreground-service capture. Required by GpsWorkoutRecorder's # allowsBackgroundLocationUpdates; without this mode iOS crashes when background updates start. - location # #510: the scheduled debug auto-export submits a BGAppRefreshTaskRequest so iOS can wake the # backgrounded app near the user's chosen time to write the overnight strap-log drop. BGAppRefresh # needs the "fetch" background mode; the task id below must also be permitted (and registered at # launch in StrandiOSApp.init) or the submit fails gracefully and only the foreground/"Run now" # paths run. - fetch # #1538: the re-score continuation submits a BGProcessingTaskRequest, which needs the # "processing" mode — "fetch" only covers the metered BGAppRefresh kind. A strap offload # completes while backgrounded and the pass it triggers ran to nearly eight minutes on the # reporter's install, which no bluetooth-central wake survives; a processing task is the long, # deferrable kind that can finish it. - processing # Permit every background-task identifier. Each derives from Bundle.main at runtime so it # tracks BUNDLE_ID_PREFIX, and each is registered from StrandiOSApp.init before launch finishes. BGTaskSchedulerPermittedIdentifiers: - $(PRODUCT_BUNDLE_IDENTIFIER).debugexport # Best-effort fallback that writes already-banked WHOOP data to HealthKit when iOS grants # background time. Fresh offloads also write immediately from their completion hook. - $(PRODUCT_BUNDLE_IDENTIFIER).healthwriteback # #1538: finishes a re-score that a backgrounded offload started and could not complete. The # only BGProcessingTask of the three; the other two are refresh tasks. - $(PRODUCT_BUNDLE_IDENTIFIER).rescore NSBluetoothAlwaysUsageDescription: "NOOP connects directly to your WHOOP strap over Bluetooth to read heart rate, R-R intervals, battery, and sensor data locally on your iPhone. Nothing leaves your device." NSLocationWhenInUseUsageDescription: "NOOP records your route during an outdoor workout to show distance, pace, and a map, and keeps recording while the screen is off. The route stays on your device. Nothing leaves it." NSHealthShareUsageDescription: "NOOP reads your own Apple Health data on-device to compute recovery, strain, and sleep. Nothing leaves your device." NSHealthUpdateUsageDescription: "NOOP writes the metrics it computes from your strap back into Apple Health, on-device and only when you allow it." NSMotionUsageDescription: "NOOP reads your iPhone's step count for a walking or running workout to show steps in the activity summary. It stays on your device. Nothing leaves it." # #187: the opt-in AI Coach's "Custom (OpenAI-compatible)" provider can point at a LOCAL LLM on # the LAN (Ollama / LM Studio at http://192.168.x.x:11434). ATS blocks cleartext by default; # NSAllowsLocalNetworking exempts loopback/.local/private-LAN hosts ONLY — public HTTPS endpoints # are unaffected, so cloud providers stay TLS. Mirrors the macOS target. NSAppTransportSecurity: NSAllowsLocalNetworking: true # #155: expose the app's Documents folder to Files/Shortcuts so the user's Siri Shortcut can # read the opt-in noop_sync.txt drop file (Apple-Health-free export for sideloaded installs). # Both keys are required for the folder to actually appear in Files. UIFileSharingEnabled: true LSSupportsOpeningDocumentsInPlace: true # Oura BYO-app OAuth credentials (values come from configFiles above); read at runtime via # OuraCredentials.fromBundle. Absent/blank values cleanly disable the Connect-Oura lane. OURA_CLIENT_ID: $(OURA_CLIENT_ID) OURA_CLIENT_SECRET: $(OURA_CLIENT_SECRET) OURA_REDIRECT_URI: $(OURA_REDIRECT_URI) # Registers the noop:// URL scheme so ASWebAuthenticationSession's OAuth redirect # (noop://oura/callback) reaches OuraOAuthProvider instead of bouncing off the OS. CFBundleURLTypes: - CFBundleURLSchemes: [noop] entitlements: path: StrandiOS/Resources/NOOP.entitlements properties: com.apple.developer.healthkit: true # Required by HealthKit on iOS 15+ for the observer queries registered by HealthKitBridge. com.apple.developer.healthkit.background-delivery: true com.apple.developer.healthkit.access: [] com.apple.security.application-groups: - $(APP_GROUP_ID) settings: base: PRODUCT_BUNDLE_IDENTIFIER: $(BUNDLE_ID_PREFIX).noop PRODUCT_NAME: "NOOP Staging" ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # Ship every *.appiconset (not just the primary AppIcon) so the alternate "AppIcon-Navy" # set is compiled into the bundle and is available to UIApplication.setAlternateIconName. ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES GENERATE_INFOPLIST_FILE: NO TARGETED_DEVICE_FAMILY: "1,2" SWIFT_EMIT_LOC_STRINGS: YES dependencies: - package: WhoopProtocol - package: WhoopStore - package: StrandAnalytics - package: StrandImport - package: StrandDesign - package: MarkdownUI - package: ZIPFoundation - package: PolarProtocol - target: NOOPiOSWidgets # Embed the watchOS app so the iPhone app carries it (NOOP.app/Watch/NOOPWatch.app). A watch app # installs by riding inside its iOS companion; without this it would never reach a paired watch. # Build-from-source delivers it for certain; sideload install of the embedded watch app under a free # Apple ID is unproven (an Apple limitation), so the docs steer watch users to a source build for now. - target: NOOPWatch # WidgetKit + Live Activity extension — the iOS analogue of the macOS menu-bar # extra. Reads a tiny snapshot the app publishes into the shared App Group. NOOPiOSWidgets: type: app-extension platform: iOS deploymentTarget: "17.0" sources: - path: StrandiOSWidgets excludes: - "Info.plist" - "NOOPWidgets.entitlements" - path: StrandiOSShared info: path: StrandiOSWidgets/Info.plist properties: CFBundleDisplayName: NOOP # Version keys MUST live here in the generated-Info.plist properties — NOT hand-edited into the # file — because xcodegen regenerates StrandiOSWidgets/Info.plist on every `xcodegen generate` # (which every release runs for the version bump) and would otherwise rewrite them to its # defaults 1.0 / 1. Inheriting $(MARKETING_VERSION)/$(CURRENT_PROJECT_VERSION) keeps the widget # extension lockstep with the parent app, so iOS stops warning "extension version must match # parent app" (#416). This is the durable fix; the earlier file edit silently reverted. CFBundleShortVersionString: "$(MARKETING_VERSION)" CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" # Mirrors the app target: exposes APP_GROUP_ID to the extension at runtime (WidgetSnapshot.suiteName). AppGroupIdentifier: "$(APP_GROUP_ID)" NSExtension: NSExtensionPointIdentifier: com.apple.widgetkit-extension entitlements: path: StrandiOSWidgets/NOOPWidgets.entitlements properties: com.apple.security.application-groups: - $(APP_GROUP_ID) settings: base: PRODUCT_BUNDLE_IDENTIFIER: $(BUNDLE_ID_PREFIX).noop.widgets PRODUCT_NAME: NOOPWidgets GENERATE_INFOPLIST_FILE: NO TARGETED_DEVICE_FAMILY: "1,2" dependencies: - package: StrandDesign # --------------------------------------------------------------------------- # watchOS app (M3 — Apple-only). The iPhone is the brain: M1 computes Charge/ # Effort/Rest with confidence + provenance, and the phone-side WatchSessionBridge # pushes a WatchScoreSnapshot to the watch. This app DISPLAYS that snapshot # (3 rings + numbers + live HR + sleep line); it does NOT recompute scores. # A calibrating score renders as "needs more data" (a dash + cal marker), never # a fabricated number. Live HR comes from the watch's OWN HealthKit sensor. # Modern single-target watchOS app (WKApplication, watchOS 9+ SwiftUI lifecycle). NOOPWatch: type: application platform: watchOS deploymentTarget: "10.0" sources: # Includes NOOPWatch/Localizable.xcstrings, the watch app's OWN string catalog (the watch cannot # read the phone app's Strand/Resources catalog; a target only ships the catalogs in its sources). # Shared StrandDesign strings localize via the package's own catalog + bundle: .module. - path: NOOPWatch excludes: - "Info.plist" - "NOOPWatch.entitlements" info: path: NOOPWatch/Info.plist properties: CFBundleName: NOOP CFBundleDisplayName: NOOP CFBundleShortVersionString: "$(MARKETING_VERSION)" CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" # Surfaces APP_GROUP_ID to runtime (WatchScoreStore.suiteName) so the App Group id isn't # hard-coded in Swift; stays in sync with the application-groups entitlement (both $(APP_GROUP_ID)). AppGroupIdentifier: "$(APP_GROUP_ID)" # Modern single-target watchOS app — no separate WatchKit extension. WKApplication: true # Pairs this watch app to the iPhone companion (must match NOOPiOS PRODUCT_BUNDLE_IDENTIFIER). # Built from the same $(BUNDLE_ID_PREFIX) rather than $(PRODUCT_BUNDLE_IDENTIFIER) because that # build setting would resolve to THIS target's own id, not NOOPiOS's. WKCompanionAppBundleIdentifier: $(BUNDLE_ID_PREFIX).noop NSHealthShareUsageDescription: "NOOP reads your heart rate from the Watch sensor to show a live readout on your wrist. It stays on your device." NSHealthUpdateUsageDescription: "NOOP records a basic workout session on your Watch for a higher-fidelity heart rate. The session stays on your device." entitlements: path: NOOPWatch/NOOPWatch.entitlements properties: com.apple.developer.healthkit: true com.apple.developer.healthkit.access: [] com.apple.security.application-groups: - $(APP_GROUP_ID) settings: base: PRODUCT_BUNDLE_IDENTIFIER: $(BUNDLE_ID_PREFIX).noop.watch # The bundle FILE is NOOPWatch.app so it doesn't collide with the iPhone app's NOOP.app when it is # embedded at NOOP.app/Watch/NOOPWatch.app. The user still sees "NOOP" on the wrist via the # CFBundleName / CFBundleDisplayName above. PRODUCT_NAME: NOOPWatch GENERATE_INFOPLIST_FILE: NO # The watch app icon (NOOPWatch/Assets.xcassets/AppIcon.appiconset — the NOOP mark, watchOS single-size). ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # watchOS family. TARGETED_DEVICE_FAMILY: "4" SWIFT_EMIT_LOC_STRINGS: YES dependencies: - package: StrandDesign - package: StrandAnalytics # The complication WidgetKit extension is embedded inside the watch app. - target: NOOPWatchComplications # WidgetKit complication extension for the watch face. Reads the same shared-App-Group # snapshot the watch app displays, so the complication + glance can never disagree. # Charge ring is the headline; accessoryRectangular shows all three scores. Honours # confidence (a calibrating score shows a dash + cal marker, not a fabricated number). NOOPWatchComplications: type: app-extension platform: watchOS deploymentTarget: "10.0" sources: # Includes NOOPWatchComplications/Localizable.xcstrings: the complication is its own bundle and # needs its own catalog (it cannot read the watch app's, let alone the phone's). - path: NOOPWatchComplications excludes: - "Info.plist" - "NOOPWatchComplications.entitlements" info: path: NOOPWatchComplications/Info.plist properties: CFBundleDisplayName: NOOP # Version keys live here as generated-plist properties so xcodegen keeps them lockstep with the # parent watch app on every regenerate (mirrors the NOOPiOSWidgets rationale, #416). CFBundleShortVersionString: "$(MARKETING_VERSION)" CFBundleVersion: "$(CURRENT_PROJECT_VERSION)" # Mirrors the watch app: exposes APP_GROUP_ID to the extension at runtime (WatchScoreStore.suiteName). AppGroupIdentifier: "$(APP_GROUP_ID)" NSExtension: NSExtensionPointIdentifier: com.apple.widgetkit-extension entitlements: path: NOOPWatchComplications/NOOPWatchComplications.entitlements properties: com.apple.security.application-groups: - $(APP_GROUP_ID) settings: base: PRODUCT_BUNDLE_IDENTIFIER: $(BUNDLE_ID_PREFIX).noop.watch.complications PRODUCT_NAME: NOOPWatchComplications GENERATE_INFOPLIST_FILE: NO TARGETED_DEVICE_FAMILY: "4" # Parity with the other UI targets: auto-extract LocalizedStringKey / String(localized:) usages # into this target's Localizable.xcstrings on build. SWIFT_EMIT_LOC_STRINGS: YES dependencies: - package: StrandDesign # Explicit schemes so `xcodebuild -scheme Strand …` builds + tests the macOS app (with StrandTests # wired in) and `-scheme NOOPiOS …` builds the iOS app. Names match the app-build CI matrix. schemes: Strand: build: targets: Strand: all run: config: Debug test: config: Debug targets: - StrandTests NOOPiOS: build: targets: NOOPiOS: all run: config: Debug # `xcodebuild -scheme NOOPWatch …` compile-verifies the watchOS app + its embedded complication # extension (M3). Name matches the app-build CI matrix entry the verify script adds. NOOPWatch: build: targets: NOOPWatch: all run: config: Debug