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