Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-10 15:10:19 -06:00
parent c449af3806
commit 1a5e17bc6f
13 changed files with 341 additions and 69 deletions

176
Agents.md
View File

@ -419,40 +419,182 @@ If you need different formats for different purposes:
- Name constants semantically: `accent` not `pointSix`, `large` not `sixteen`. - 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
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>$(APP_GROUP_IDENTIFIER)</string>
</array>
```
#### Step 4: Bridge to Swift via Info.plist
Add keys to `Info.plist` that bridge xcconfig values to Swift:
```xml
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>CloudKitContainerIdentifier</key>
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
<key>AppClipDomain</key>
<string>$(APPCLIP_DOMAIN)</string>
```
#### Step 5: Create Swift Interface
Create `Configuration/AppIdentifiers.swift`: Create `Configuration/AppIdentifiers.swift`:
```swift ```swift
import Foundation
enum AppIdentifiers { enum AppIdentifiers {
// MARK: - Company Identifier (CHANGE THIS FOR MIGRATION) // Read from Info.plist (values come from xcconfig)
static let companyIdentifier = "com.yourcompany" 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 watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" }
static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } 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. Base.xcconfig (source of truth)
- All other identifiers derive from it automatically.
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 ### Migration
When migrating to a new developer account: To migrate to a new developer account, edit **one file** (`Base.xcconfig`):
1. Change `companyIdentifier` in `AppIdentifiers.swift`
2. Update entitlements files manually (cannot be tokenized) ```
3. Update bundle IDs in Xcode project settings COMPANY_IDENTIFIER = com.newcompany
DEVELOPMENT_TEAM = NEW_TEAM_ID
```
Then clean build (⇧⌘K) and rebuild. Everything updates automatically.
## Dynamic Type Instructions ## Dynamic Type Instructions

View File

@ -54,6 +54,10 @@
EA8379302F105F2800077F87 /* BusinessCardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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; }; 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 = "<group>"; };
EACONFIG0012F200000000002 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
EACONFIG0012F200000000003 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
EACONFIG0012F200000000004 /* Watch.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Watch.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@ -387,6 +391,7 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
EA8379422F105F2800077F87 /* Debug */ = { EA8379422F105F2800077F87 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = EACONFIG0012F200000000002 /* Debug.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -420,7 +425,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -451,6 +456,7 @@
}; };
EA8379432F105F2800077F87 /* Release */ = { EA8379432F105F2800077F87 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = EACONFIG0012F200000000003 /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -484,7 +490,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -514,7 +520,7 @@
CODE_SIGN_ENTITLEMENTS = BusinessCard/BusinessCard.entitlements; CODE_SIGN_ENTITLEMENTS = BusinessCard/BusinessCard.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusinessCard/Info.plist; INFOPLIST_FILE = BusinessCard/Info.plist;
@ -530,7 +536,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -550,7 +556,7 @@
CODE_SIGN_ENTITLEMENTS = BusinessCard/BusinessCard.entitlements; CODE_SIGN_ENTITLEMENTS = BusinessCard/BusinessCard.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusinessCard/Info.plist; INFOPLIST_FILE = BusinessCard/Info.plist;
@ -566,7 +572,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -584,11 +590,11 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardTests; PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -606,11 +612,11 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardTests; PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -627,10 +633,10 @@
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardUITests; PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -647,10 +653,10 @@
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCardUITests; PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -670,18 +676,18 @@
CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements"; CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch; INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.BusinessCard; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(APP_BUNDLE_IDENTIFIER)";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = "$(WATCH_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -704,18 +710,18 @@
CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements"; CODE_SIGN_ENTITLEMENTS = "BusinessCardWatch Watch App/BusinessCardWatch.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch; INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.BusinessCard; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(APP_BUNDLE_IDENTIFIER)";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BusinessCard.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = "$(WATCH_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

@ -12,7 +12,7 @@
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key> <key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>2</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>

View File

@ -6,7 +6,7 @@
<string>development</string> <string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key> <key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>iCloud.com.mbrucedogs.BusinessCard</string> <string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
</array> </array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
@ -14,7 +14,7 @@
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.mbrucedogs.BusinessCard</string> <string>$(APP_GROUP_IDENTIFIER)</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -1,31 +1,56 @@
import Foundation 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: /// When migrating to a new developer account:
/// 1. Update `companyIdentifier` below /// 1. Update `COMPANY_IDENTIFIER` and `DEVELOPMENT_TEAM` in Base.xcconfig
/// 2. Update entitlements files manually (cannot be tokenized) /// 2. The entitlements, bundle IDs, and Swift code all update automatically
/// 3. Update bundle IDs in Xcode project settings /// 3. See DevAccount-Migration.md for complete checklist
/// 4. See DevAccount-Migration.md for full checklist
enum AppIdentifiers { enum AppIdentifiers {
// MARK: - Company Identifier (CHANGE THIS FOR MIGRATION) // MARK: - Runtime Identifiers (read from Info.plist)
/// The company's reverse domain identifier. /// App Group identifier for sharing data between app and extensions.
static let companyIdentifier = "com.mbrucedogs" 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 // MARK: - Derived Identifiers
static var bundleIdentifier: String { "\(companyIdentifier).BusinessCard" } /// Bundle identifier of the main app.
static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" } static var bundleIdentifier: String {
static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } Bundle.main.bundleIdentifier ?? "com.mbrucedogs.BusinessCard"
static var appGroupIdentifier: String { "group.\(companyIdentifier).BusinessCard" } }
static var cloudKitContainerIdentifier: String { "iCloud.\(companyIdentifier).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? { static func appClipURL(recordName: String) -> URL? {
URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)") URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)")
} }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
// Release.xcconfig
// Release configuration settings
#include "Base.xcconfig"
// Release-specific settings (if any)
// Add production API keys, endpoints, etc. here

View File

@ -6,5 +6,11 @@
<array> <array>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>CloudKitContainerIdentifier</key>
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
<key>AppClipDomain</key>
<string>$(APPCLIP_DOMAIN)</string>
</dict> </dict>
</plist> </plist>

View File

@ -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)

View File

@ -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. 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 ## Current State Analysis
### Account Information ### Account Information

View File

@ -148,7 +148,10 @@ BusinessCard/
├── Assets.xcassets/ ├── Assets.xcassets/
│ └── SocialSymbols/ # Custom brand icons (LinkedIn, X, Instagram, etc.) │ └── SocialSymbols/ # Custom brand icons (LinkedIn, X, Instagram, etc.)
├── Configuration/ # App identifiers and configuration ├── 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) ├── Design/ # Design constants (extends Bedrock)
├── Localization/ # String helpers ├── Localization/ # String helpers
├── Models/ ├── 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` `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` COMPANY_IDENTIFIER = com.mbrucedogs
- App Group: `AppIdentifiers.appGroupIdentifier` 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 ## Notes

View File

@ -55,14 +55,27 @@ App-specific extensions are in `Design/DesignConstants.swift`:
## Important Files ## Important Files
### Configuration ### Configuration (xcconfig)
- `Configuration/AppIdentifiers.swift` — Centralized company identifiers: Company identifiers are centralized using xcconfig files for true single-source configuration:
- `companyIdentifier` — Base identifier (e.g., "com.mbrucedogs")
- Derived: `bundleIdentifier`, `watchBundleIdentifier`, `appClipBundleIdentifier` - `Configuration/Base.xcconfig` — Source of truth for all identifiers:
- Derived: `appGroupIdentifier`, `cloudKitContainerIdentifier` - `COMPANY_IDENTIFIER` — Base identifier (e.g., "com.mbrucedogs")
- App Clip: `appClipDomain`, `appClipURL(recordName:)` - `DEVELOPMENT_TEAM` — Apple Developer Team ID
- **Migration**: Change `companyIdentifier` + update entitlements. See `DevAccount-Migration.md`. - 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 ### Models