From 1a5e17bc6f9fdaa7bbb37356878876224c0818c2 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 10 Jan 2026 15:10:19 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- Agents.md | 176 ++++++++++++++++-- BusinessCard.xcodeproj/project.pbxproj | 46 +++-- .../xcschemes/xcschememanagement.plist | 2 +- BusinessCard/BusinessCard.entitlements | 4 +- .../Configuration/AppIdentifiers.swift | 55 ++++-- BusinessCard/Configuration/Base.xcconfig | 34 ++++ BusinessCard/Configuration/Debug.xcconfig | 7 + BusinessCard/Configuration/Release.xcconfig | 7 + BusinessCard/Info.plist | 6 + .../Configuration/Watch.xcconfig | 7 + DevAccount-Migration.md | 16 ++ README.md | 23 ++- ai_implementation.md | 27 ++- 13 files changed, 341 insertions(+), 69 deletions(-) create mode 100644 BusinessCard/Configuration/Base.xcconfig create mode 100644 BusinessCard/Configuration/Debug.xcconfig create mode 100644 BusinessCard/Configuration/Release.xcconfig create mode 100644 BusinessCardWatch Watch App/Configuration/Watch.xcconfig diff --git a/Agents.md b/Agents.md index e6b7ca7..eb91bf2 100644 --- a/Agents.md +++ b/Agents.md @@ -419,40 +419,182 @@ If you need different formats for different purposes: - Name constants semantically: `accent` not `pointSix`, `large` not `sixteen`. -## App Identifiers +## App Identifiers (xcconfig) -**Centralize all company-specific identifiers** in a single configuration file for easy migration. +**Centralize all company-specific identifiers** using xcconfig files for true single-source configuration. This enables one-line migration between developer accounts. -### Structure +### Why xcconfig? + +- **Single source of truth**: Change one file, everything updates +- **Build-time resolution**: Bundle IDs, entitlements, and Swift code all derive from same source +- **No manual updates**: Entitlements use variable substitution +- **Environment support**: Easy Debug/Release/Staging configurations + +### Setup Instructions + +#### Step 1: Create xcconfig Files + +Create `Configuration/Base.xcconfig`: + +``` +// Base.xcconfig - Source of truth for all identifiers +// MIGRATION: Update COMPANY_IDENTIFIER and DEVELOPMENT_TEAM below + +// ============================================================================= +// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION +// ============================================================================= + +COMPANY_IDENTIFIER = com.yourcompany +APP_NAME = YourAppName +DEVELOPMENT_TEAM = YOUR_TEAM_ID + +// ============================================================================= +// DERIVED IDENTIFIERS - DO NOT EDIT +// ============================================================================= + +APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME) +WATCH_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).watchkitapp +APPCLIP_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Clip +TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests +UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests + +APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME) +CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME) + +APPCLIP_DOMAIN = yourapp.example.com +``` + +Create `Configuration/Debug.xcconfig`: + +``` +// Debug.xcconfig +#include "Base.xcconfig" +// Add debug-specific settings here +``` + +Create `Configuration/Release.xcconfig`: + +``` +// Release.xcconfig +#include "Base.xcconfig" +// Add release-specific settings here +``` + +#### Step 2: Configure Xcode Project + +In `project.pbxproj`, add file references and set `baseConfigurationReference` for each build configuration: + +1. Add xcconfig file references to PBXFileReference section +2. Set `baseConfigurationReference` on Debug/Release configurations +3. Replace hardcoded values with variables: + +``` +PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; +DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; +INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(APP_BUNDLE_IDENTIFIER)"; +``` + +#### Step 3: Update Entitlements + +Use variable substitution in `.entitlements` files: + +```xml +com.apple.developer.icloud-container-identifiers + + $(CLOUDKIT_CONTAINER_IDENTIFIER) + +com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + +``` + +#### Step 4: Bridge to Swift via Info.plist + +Add keys to `Info.plist` that bridge xcconfig values to Swift: + +```xml +AppGroupIdentifier +$(APP_GROUP_IDENTIFIER) +CloudKitContainerIdentifier +$(CLOUDKIT_CONTAINER_IDENTIFIER) +AppClipDomain +$(APPCLIP_DOMAIN) +``` + +#### Step 5: Create Swift Interface Create `Configuration/AppIdentifiers.swift`: ```swift +import Foundation + enum AppIdentifiers { - // MARK: - Company Identifier (CHANGE THIS FOR MIGRATION) - static let companyIdentifier = "com.yourcompany" + // Read from Info.plist (values come from xcconfig) + static let appGroupIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String + ?? "group.com.yourcompany.AppName" + }() + + static let cloudKitContainerIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "CloudKitContainerIdentifier") as? String + ?? "iCloud.com.yourcompany.AppName" + }() + + static let appClipDomain: String = { + Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String + ?? "yourapp.example.com" + }() + + // Derived from bundle identifier + static var bundleIdentifier: String { + Bundle.main.bundleIdentifier ?? "com.yourcompany.AppName" + } - // MARK: - Derived Identifiers - static var bundleIdentifier: String { "\(companyIdentifier).AppName" } static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" } static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } - static var appGroupIdentifier: String { "group.\(companyIdentifier).AppName" } - static var cloudKitContainerIdentifier: String { "iCloud.\(companyIdentifier).AppName" } + + static func appClipURL(recordName: String) -> URL? { + URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)") + } } ``` -### Usage +### Data Flow -- Always use `AppIdentifiers.*` instead of hardcoding bundle IDs or container names. -- `AppIdentifiers.companyIdentifier` is the single source of truth. -- All other identifiers derive from it automatically. +``` +Base.xcconfig (source of truth) + ↓ +project.pbxproj (baseConfigurationReference) + ↓ +Build Settings → Bundle IDs, Team ID, etc. + ↓ +Info.plist (bridges values via $(VARIABLE)) + ↓ +AppIdentifiers.swift (Swift reads from Bundle.main) +``` + +### Usage in Code + +```swift +// Always use AppIdentifiers instead of hardcoding +FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier +) + +CKContainer(identifier: AppIdentifiers.cloudKitContainerIdentifier) +``` ### Migration -When migrating to a new developer account: -1. Change `companyIdentifier` in `AppIdentifiers.swift` -2. Update entitlements files manually (cannot be tokenized) -3. Update bundle IDs in Xcode project settings +To migrate to a new developer account, edit **one file** (`Base.xcconfig`): + +``` +COMPANY_IDENTIFIER = com.newcompany +DEVELOPMENT_TEAM = NEW_TEAM_ID +``` + +Then clean build (⇧⌘K) and rebuild. Everything updates automatically. ## Dynamic Type Instructions diff --git a/BusinessCard.xcodeproj/project.pbxproj b/BusinessCard.xcodeproj/project.pbxproj index e750612..58fc24c 100644 --- a/BusinessCard.xcodeproj/project.pbxproj +++ b/BusinessCard.xcodeproj/project.pbxproj @@ -54,6 +54,10 @@ EA8379302F105F2800077F87 /* BusinessCardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BusinessCardWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + EACONFIG0012F200000000001 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; + EACONFIG0012F200000000002 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + EACONFIG0012F200000000003 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + EACONFIG0012F200000000004 /* Watch.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Watch.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -387,6 +391,7 @@ /* Begin XCBuildConfiguration section */ EA8379422F105F2800077F87 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EACONFIG0012F200000000002 /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -420,7 +425,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -451,6 +456,7 @@ }; EA8379432F105F2800077F87 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EACONFIG0012F200000000003 /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -484,7 +490,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -514,7 +520,7 @@ CODE_SIGN_ENTITLEMENTS = BusinessCard/BusinessCard.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BusinessCard/Info.plist; @@ -530,7 +536,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -550,7 +556,7 @@ CODE_SIGN_ENTITLEMENTS = BusinessCard/BusinessCard.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BusinessCard/Info.plist; @@ -566,7 +572,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -584,11 +590,11 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardTests; + PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -606,11 +612,11 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardTests; + PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -627,10 +633,10 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardUITests; + PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -647,10 +653,10 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardUITests; + PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -670,18 +676,18 @@ CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.BusinessCard; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(APP_BUNDLE_IDENTIFIER)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard.watchkitapp; + PRODUCT_BUNDLE_IDENTIFIER = "$(WATCH_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; @@ -704,18 +710,18 @@ CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.BusinessCard; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(APP_BUNDLE_IDENTIFIER)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard.watchkitapp; + PRODUCT_BUNDLE_IDENTIFIER = "$(WATCH_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; diff --git a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index fba635b..f560082 100644 --- a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ BusinessCardWatch Watch App.xcscheme_^#shared#^_ orderHint - 0 + 2 diff --git a/BusinessCard/BusinessCard.entitlements b/BusinessCard/BusinessCard.entitlements index 046e211..114bb29 100644 --- a/BusinessCard/BusinessCard.entitlements +++ b/BusinessCard/BusinessCard.entitlements @@ -6,7 +6,7 @@ development com.apple.developer.icloud-container-identifiers - iCloud.com.mbrucedogs.BusinessCard + $(CLOUDKIT_CONTAINER_IDENTIFIER) com.apple.developer.icloud-services @@ -14,7 +14,7 @@ com.apple.security.application-groups - group.com.mbrucedogs.BusinessCard + $(APP_GROUP_IDENTIFIER) diff --git a/BusinessCard/Configuration/AppIdentifiers.swift b/BusinessCard/Configuration/AppIdentifiers.swift index f0a5463..887a098 100644 --- a/BusinessCard/Configuration/AppIdentifiers.swift +++ b/BusinessCard/Configuration/AppIdentifiers.swift @@ -1,31 +1,56 @@ import Foundation -/// Centralized app identifiers for easy migration between developer accounts. +/// Centralized app identifiers that read from Info.plist (which gets values from xcconfig). +/// +/// The source of truth is `Configuration/Base.xcconfig`. Values flow: +/// Base.xcconfig → Info.plist → AppIdentifiers (Swift) /// /// When migrating to a new developer account: -/// 1. Update `companyIdentifier` below -/// 2. Update entitlements files manually (cannot be tokenized) -/// 3. Update bundle IDs in Xcode project settings -/// 4. See DevAccount-Migration.md for full checklist +/// 1. Update `COMPANY_IDENTIFIER` and `DEVELOPMENT_TEAM` in Base.xcconfig +/// 2. The entitlements, bundle IDs, and Swift code all update automatically +/// 3. See DevAccount-Migration.md for complete checklist enum AppIdentifiers { - // MARK: - Company Identifier (CHANGE THIS FOR MIGRATION) + // MARK: - Runtime Identifiers (read from Info.plist) - /// The company's reverse domain identifier. - static let companyIdentifier = "com.mbrucedogs" + /// App Group identifier for sharing data between app and extensions. + static let appGroupIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String + ?? "group.com.mbrucedogs.BusinessCard" + }() + + /// CloudKit container identifier. + static let cloudKitContainerIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "CloudKitContainerIdentifier") as? String + ?? "iCloud.com.mbrucedogs.BusinessCard" + }() + + /// App Clip domain for sharing URLs. + static let appClipDomain: String = { + Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String + ?? "cards.example.com" + }() // MARK: - Derived Identifiers - static var bundleIdentifier: String { "\(companyIdentifier).BusinessCard" } - static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" } - static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } - static var appGroupIdentifier: String { "group.\(companyIdentifier).BusinessCard" } - static var cloudKitContainerIdentifier: String { "iCloud.\(companyIdentifier).BusinessCard" } + /// Bundle identifier of the main app. + static var bundleIdentifier: String { + Bundle.main.bundleIdentifier ?? "com.mbrucedogs.BusinessCard" + } - // MARK: - App Clip Configuration + /// Watch app bundle identifier (derived from main app). + static var watchBundleIdentifier: String { + "\(bundleIdentifier).watchkitapp" + } - static let appClipDomain = "cards.example.com" + /// App Clip bundle identifier (derived from main app). + static var appClipBundleIdentifier: String { + "\(bundleIdentifier).Clip" + } + // MARK: - App Clip URL Generation + + /// Generates an App Clip invocation URL for a shared card record. static func appClipURL(recordName: String) -> URL? { URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)") } diff --git a/BusinessCard/Configuration/Base.xcconfig b/BusinessCard/Configuration/Base.xcconfig new file mode 100644 index 0000000..c0dfc74 --- /dev/null +++ b/BusinessCard/Configuration/Base.xcconfig @@ -0,0 +1,34 @@ +// Base.xcconfig +// Shared identifiers for all targets and configurations +// +// MIGRATION: To change developer accounts, update COMPANY_IDENTIFIER below +// and the DEVELOPMENT_TEAM. Then follow DevAccount-Migration.md checklist. + +// ============================================================================= +// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION +// ============================================================================= + +COMPANY_IDENTIFIER = com.mbrucedogs +APP_NAME = BusinessCard +DEVELOPMENT_TEAM = 6R7KLBPBLZ + +// ============================================================================= +// DERIVED IDENTIFIERS - DO NOT EDIT (computed from above) +// ============================================================================= + +// Bundle identifiers +APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME) +WATCH_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).watchkitapp +APPCLIP_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Clip +TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests +UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests + +// Entitlement identifiers +APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME) +CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME) + +// ============================================================================= +// APP CLIP CONFIGURATION +// ============================================================================= + +APPCLIP_DOMAIN = cards.example.com diff --git a/BusinessCard/Configuration/Debug.xcconfig b/BusinessCard/Configuration/Debug.xcconfig new file mode 100644 index 0000000..f5c9523 --- /dev/null +++ b/BusinessCard/Configuration/Debug.xcconfig @@ -0,0 +1,7 @@ +// Debug.xcconfig +// Debug configuration settings + +#include "Base.xcconfig" + +// Debug-specific settings (if any) +// Add environment-specific API keys, endpoints, etc. here diff --git a/BusinessCard/Configuration/Release.xcconfig b/BusinessCard/Configuration/Release.xcconfig new file mode 100644 index 0000000..95dfc17 --- /dev/null +++ b/BusinessCard/Configuration/Release.xcconfig @@ -0,0 +1,7 @@ +// Release.xcconfig +// Release configuration settings + +#include "Base.xcconfig" + +// Release-specific settings (if any) +// Add production API keys, endpoints, etc. here diff --git a/BusinessCard/Info.plist b/BusinessCard/Info.plist index ca9a074..2ec3aff 100644 --- a/BusinessCard/Info.plist +++ b/BusinessCard/Info.plist @@ -6,5 +6,11 @@ remote-notification + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) + CloudKitContainerIdentifier + $(CLOUDKIT_CONTAINER_IDENTIFIER) + AppClipDomain + $(APPCLIP_DOMAIN) diff --git a/BusinessCardWatch Watch App/Configuration/Watch.xcconfig b/BusinessCardWatch Watch App/Configuration/Watch.xcconfig new file mode 100644 index 0000000..d82d152 --- /dev/null +++ b/BusinessCardWatch Watch App/Configuration/Watch.xcconfig @@ -0,0 +1,7 @@ +// Watch.xcconfig +// Watch app configuration - inherits from Base + +#include "../BusinessCard/Configuration/Base.xcconfig" + +// Watch-specific derived identifiers +PRODUCT_BUNDLE_IDENTIFIER = $(WATCH_BUNDLE_IDENTIFIER) diff --git a/DevAccount-Migration.md b/DevAccount-Migration.md index 871d724..c8a0046 100644 --- a/DevAccount-Migration.md +++ b/DevAccount-Migration.md @@ -2,6 +2,22 @@ This document provides a comprehensive guide for migrating the BusinessCard app from the personal `mbrucedogs` developer account to a new Apple Developer account. +## Quick Migration (xcconfig) + +With the xcconfig setup, migration is now a **single file change**: + +1. Open `BusinessCard/Configuration/Base.xcconfig` +2. Update these two lines: + ``` + COMPANY_IDENTIFIER = com.newcompany + DEVELOPMENT_TEAM = NEW_TEAM_ID + ``` +3. Clean build (⇧⌘K) and rebuild + +That's it! All bundle IDs, entitlements, and Swift code will automatically use the new values. + +--- + ## Current State Analysis ### Account Information diff --git a/README.md b/README.md index 571f4e5..67d9aff 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,10 @@ BusinessCard/ ├── Assets.xcassets/ │ └── SocialSymbols/ # Custom brand icons (LinkedIn, X, Instagram, etc.) ├── Configuration/ # App identifiers and configuration -│ └── AppIdentifiers.swift # Centralized company identifiers +│ ├── Base.xcconfig # Source of truth for all identifiers +│ ├── Debug.xcconfig # Debug configuration (imports Base) +│ ├── Release.xcconfig # Release configuration (imports Base) +│ └── AppIdentifiers.swift # Swift interface to configuration ├── Design/ # Design constants (extends Bedrock) ├── Localization/ # String helpers ├── Models/ @@ -199,15 +202,21 @@ Without this, the iOS app installs but the watch app does NOT install on the pai `iCloud.com.mbrucedogs.BusinessCard` -### App Identifiers +### App Identifiers (xcconfig) -All company-specific identifiers are centralized in `Configuration/AppIdentifiers.swift`: +All company-specific identifiers are centralized in `Configuration/Base.xcconfig`: -- Bundle IDs: `AppIdentifiers.bundleIdentifier`, `.watchBundleIdentifier`, `.appClipBundleIdentifier` -- CloudKit: `AppIdentifiers.cloudKitContainerIdentifier` -- App Group: `AppIdentifiers.appGroupIdentifier` +``` +COMPANY_IDENTIFIER = com.mbrucedogs +DEVELOPMENT_TEAM = 6R7KLBPBLZ +``` -See `DevAccount-Migration.md` for migration instructions. +This flows through: +- **Build settings**: Bundle IDs, Team ID (via xcconfig → project) +- **Entitlements**: CloudKit container, App Group (via variable substitution) +- **Swift code**: `AppIdentifiers.*` (via Info.plist → Bundle.main) + +**To migrate to a new developer account**: Update `Base.xcconfig` only. See `DevAccount-Migration.md`. ## Notes diff --git a/ai_implementation.md b/ai_implementation.md index 9931f49..f2005a0 100644 --- a/ai_implementation.md +++ b/ai_implementation.md @@ -55,14 +55,27 @@ App-specific extensions are in `Design/DesignConstants.swift`: ## Important Files -### Configuration +### Configuration (xcconfig) -- `Configuration/AppIdentifiers.swift` — Centralized company identifiers: - - `companyIdentifier` — Base identifier (e.g., "com.mbrucedogs") - - Derived: `bundleIdentifier`, `watchBundleIdentifier`, `appClipBundleIdentifier` - - Derived: `appGroupIdentifier`, `cloudKitContainerIdentifier` - - App Clip: `appClipDomain`, `appClipURL(recordName:)` - - **Migration**: Change `companyIdentifier` + update entitlements. See `DevAccount-Migration.md`. +Company identifiers are centralized using xcconfig files for true single-source configuration: + +- `Configuration/Base.xcconfig` — Source of truth for all identifiers: + - `COMPANY_IDENTIFIER` — Base identifier (e.g., "com.mbrucedogs") + - `DEVELOPMENT_TEAM` — Apple Developer Team ID + - Derived: `APP_BUNDLE_IDENTIFIER`, `WATCH_BUNDLE_IDENTIFIER`, `TESTS_BUNDLE_IDENTIFIER`, etc. + - Entitlements: `APP_GROUP_IDENTIFIER`, `CLOUDKIT_CONTAINER_IDENTIFIER` + +- `Configuration/Debug.xcconfig` — Imports Base, adds debug-specific settings +- `Configuration/Release.xcconfig` — Imports Base, adds release-specific settings + +- `Configuration/AppIdentifiers.swift` — Swift interface reading from Info.plist: + - `appGroupIdentifier`, `cloudKitContainerIdentifier`, `appClipDomain` + - `bundleIdentifier`, `watchBundleIdentifier`, `appClipBundleIdentifier` + - `appClipURL(recordName:)` — Generates App Clip invocation URLs + +**Data flow**: `Base.xcconfig` → `project.pbxproj` → `Info.plist` → `AppIdentifiers.swift` + +**Migration**: Change `COMPANY_IDENTIFIER` and `DEVELOPMENT_TEAM` in `Base.xcconfig`. Everything else updates automatically. See `DevAccount-Migration.md`. ### Models