diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2cb9146 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,752 @@ +# Agent Guide for Swift and SwiftUI + +This repository contains an Xcode project written with Swift and SwiftUI. Please follow the guidelines below so that the development experience is built on modern, safe API usage. + +## Additional Context Files + +Before starting work, read project documentation: + +- `WORKSPACE.md` — (if present) Multi-project workspace overview and project relationships +- `README.md` — Project scope, features, and architecture +- In multi-project workspaces, each project folder has its own `README.md` + +When making architectural changes, keep documentation files in sync with code changes. + + +## Role + +You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and related frameworks. Your code must always adhere to Apple's Human Interface Guidelines and App Review guidelines. + + +## Core Instructions + +- Target a min of iOS 17 or later. (Yes, it definitely exists.) +- Swift 5 using modern Swift concurrency. Our primary project is not on 6 yet. +- SwiftUI backed up by `@Observable` classes for shared data. +- **Prioritize Protocol-Oriented Programming (POP)** for reusability and testability. +- **Follow Clean Architecture principles** for maintainable, testable code. +- Do not introduce third-party frameworks without asking first. +- Avoid UIKit unless requested. + + +## Clean Architecture + +**Separation of concerns is mandatory.** Code should be organized into distinct layers with clear responsibilities and dependencies flowing inward. + +### File Organization Principles + +1. **One public type per file**: Each file should contain exactly one public struct, class, or enum. Private supporting types may be included if they are small and only used by the main type. + +2. **Keep files lean**: Aim for files under 300 lines. If a file exceeds this: + - Extract reusable sub-views into separate files in a `Components/` folder + - Extract sheets/modals into a `Sheets/` folder + - Move complex logic into dedicated types + +3. **No duplicate code**: Before writing new code, search for existing implementations. Extract common patterns into reusable components. + +4. **Logical grouping**: Organize files by feature, not by type: + ``` + Feature/ + ├── Views/ + │ ├── FeatureView.swift + │ ├── Components/ + │ │ ├── FeatureRowView.swift + │ │ └── FeatureHeaderView.swift + │ └── Sheets/ + │ └── FeatureEditSheet.swift + ├── Models/ + │ └── FeatureModel.swift + └── State/ + └── FeatureStore.swift + ``` + +### Layer Responsibilities + +| Layer | Contains | Depends On | +|-------|----------|------------| +| **Views** | SwiftUI views, UI components | State, Models | +| **State** | `@Observable` stores, view models | Models, Services | +| **Services** | Business logic, networking, persistence | Models | +| **Models** | Data types, entities, DTOs | Nothing | +| **Protocols** | Interfaces for services and stores | Models | + +### Architecture Rules + +1. **Views are dumb renderers**: No business logic in views. Views read state and call methods. +2. **State holds business logic**: All computations, validations, and data transformations. +3. **Services are stateless**: Pure functions where possible. Injected via protocols. +4. **Models are simple**: Plain data types. No dependencies on UI or services. + +### Example Structure + +``` +App/ +├── Design/ # Design constants, colors, typography +├── Localization/ # String helpers +├── Models/ # Data models (SwiftData, plain structs) +├── Protocols/ # Protocol definitions for DI +├── Services/ # Business logic, API clients, persistence +├── State/ # Observable stores, app state +└── Views/ + ├── Components/ # Reusable UI components + ├── Sheets/ # Modal presentations + └── [Feature]/ # Feature-specific views +``` + + +## Protocol-Oriented Programming (POP) + +**Protocol-first architecture is a priority.** When designing new features, always think about protocols and composition before concrete implementations. + +### When Architecting New Code + +1. **Start with the protocol**: Before writing a concrete type, ask "What capability am I defining?" and express it as a protocol. +2. **Identify shared behavior**: If multiple types will need similar functionality, define a protocol first. +3. **Use protocol extensions for defaults**: Provide sensible default implementations to reduce boilerplate. +4. **Prefer composition over inheritance**: Combine multiple protocols rather than building deep class hierarchies. + +### When Reviewing Existing Code + +1. **Look for duplicated patterns**: Similar logic across files is a candidate for protocol extraction. +2. **Identify common interfaces**: Types that expose similar properties/methods should conform to a shared protocol. +3. **Check before implementing**: Search for existing protocols that could be adopted or extended. +4. **Propose refactors proactively**: When you spot an opportunity to extract a protocol, mention it. + +### Protocol Design Guidelines + +- **Name protocols for capabilities**: Use `-able`, `-ing`, or `-Provider` suffixes (e.g., `Shareable`, `DataProviding`, `Persistable`). +- **Keep protocols focused**: Each protocol should represent one capability (Interface Segregation Principle). +- **Use associated types sparingly**: Prefer concrete types or generics at the call site when possible. +- **Constrain to `AnyObject` only when needed**: Prefer value semantics unless reference semantics are required. + +### Benefits + +- **Reusability**: Shared protocols work across features +- **Testability**: Mock types can conform to protocols for unit testing +- **Flexibility**: New features can adopt existing protocols immediately +- **Maintainability**: Fix a bug in a protocol extension, fix it everywhere +- **Discoverability**: Protocols document the expected interface clearly + + +## View/State Separation (MVVM-lite) + +**Views should be "dumb" renderers.** All business logic belongs in stores or dedicated view models. + +### What Belongs in State/Store + +- **Business logic**: Calculations, validations, rules +- **Computed properties based on data**: Hints, recommendations, derived values +- **State checks**: `canSubmit`, `isLoading`, `hasError` +- **Data transformations**: Filtering, sorting, aggregations + +### What is Acceptable in Views + +- **Pure UI layout logic**: Adaptive layouts based on size class +- **Visual styling**: Color selection based on state +- **@ViewBuilder sub-views**: Breaking up complex layouts (keep in same file if small) +- **Accessibility labels**: Combining data into accessible descriptions + +### Example + +```swift +// ❌ BAD - Business logic in view +struct MyView: View { + @Bindable var state: FeatureState + + private var isValid: Bool { + !state.name.isEmpty && state.email.contains("@") + } +} + +// ✅ GOOD - Logic in State, view just reads +// In FeatureState: +var isValid: Bool { + !name.isEmpty && email.contains("@") +} + +// In View: +Button("Save") { state.save() } + .disabled(!state.isValid) +``` + + +## Swift Instructions + +- Always mark `@Observable` classes with `@MainActor`. +- Assume strict Swift concurrency rules are being applied. +- Prefer Swift-native alternatives to Foundation methods where they exist. +- Prefer modern Foundation API (e.g., `URL.documentsDirectory`, `appending(path:)`). +- Never use C-style number formatting; use `format:` modifiers instead. +- Prefer static member lookup to struct instances (`.circle` not `Circle()`). +- Never use old-style GCD; use modern Swift concurrency. +- Filtering text based on user-input must use `localizedStandardContains()`. +- Avoid force unwraps and force `try` unless unrecoverable. + + +## SwiftUI Instructions + +- Always use `foregroundStyle()` instead of `foregroundColor()`. +- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`. +- Always use the `Tab` API instead of `tabItem()`. +- Never use `ObservableObject`; always prefer `@Observable` classes. +- Never use `onChange()` in its 1-parameter variant. +- Never use `onTapGesture()` unless you need tap location/count; use `Button`. +- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)`. +- Never use `UIScreen.main.bounds` to read available space. +- Do not break views up using computed properties; extract into new `View` structs. +- Do not force specific font sizes; prefer Dynamic Type. +- Use `NavigationStack` with `navigationDestination(for:)`. +- If using an image for a button label, always specify text alongside. +- Prefer `ImageRenderer` to `UIGraphicsImageRenderer`. +- Use `bold()` instead of `fontWeight(.bold)`. +- Avoid `GeometryReader` if newer alternatives work (e.g., `containerRelativeFrame()`). +- When enumerating in `ForEach`, don't convert to Array first. +- Hide scroll indicators with `.scrollIndicators(.hidden)`. +- Avoid `AnyView` unless absolutely required. +- **Never use raw numeric literals** for padding, spacing, opacity, etc.—use Design constants. +- **Never use inline colors**—define all colors with semantic names. +- Avoid UIKit colors in SwiftUI code. + + +## watchOS Development (CRITICAL) + +**Read this entire section before implementing any watch functionality.** + +### Creating a Watch Target + +When adding a watchOS target to an existing iOS app: + +1. **File → New → Target → "Watch App for watchOS"** +2. Choose **"Watch App for Existing iOS App"** (NOT standalone) +3. Name it appropriately (e.g., `AppNameWatch`) +4. Xcode creates a folder like `AppNameWatch Watch App/` + +### CRITICAL: Embedding the Watch App + +⚠️ **THIS IS THE #1 CAUSE OF "WATCH APP NOT INSTALLED" ERRORS** ⚠️ + +The watch app MUST be embedded in the iOS app for deployment to real devices: + +1. Select the **iOS target** in Xcode +2. Go to **Build Phases** tab +3. Verify there's an **"Embed Watch Content"** phase +4. **CRITICAL**: Ensure **"Code Sign On Copy"** is CHECKED ✓ + +If "Embed Watch Content" doesn't exist: +1. Click **"+"** → **"New Copy Files Phase"** +2. Rename to **"Embed Watch Content"** +3. Set **Destination** to **"Products Directory"** +4. Set **Subpath** to `$(CONTENTS_FOLDER_PATH)/Watch` +5. Add the watch app (e.g., `AppNameWatch Watch App.app`) +6. **CHECK "Code Sign On Copy"** ← This is critical! + +Without proper embedding, the iOS app installs but the watch app does NOT install on the paired Apple Watch. + +### Bundle Identifiers + +Watch app bundle IDs MUST be prefixed with the iOS app's bundle ID: + +``` +iOS app: com.company.AppName +Watch app: com.company.AppName.watchkitapp ← MUST start with iOS bundle ID +``` + +Also verify `WKCompanionAppBundleIdentifier` in the watch target's build settings matches the iOS app's bundle ID exactly. + +### Data Sync: WatchConnectivity (NOT App Groups) + +**DO NOT use App Groups for iPhone ↔ Watch data sharing.** + +App Groups: +- ❌ Do NOT work between iPhone and Apple Watch +- ❌ Different container paths on each device +- ❌ Will waste hours debugging why data isn't syncing +- ✅ Only work between an app and its extensions on the SAME device + +**Use WatchConnectivity framework instead:** + +```swift +// iOS side - WatchConnectivityService.swift +import WatchConnectivity + +@MainActor +final class WatchConnectivityService: NSObject, WCSessionDelegate { + static let shared = WatchConnectivityService() + + private override init() { + super.init() + if WCSession.isSupported() { + WCSession.default.delegate = self + WCSession.default.activate() + } + } + + func syncData(_ data: [String: Any]) { + guard WCSession.default.activationState == .activated, + WCSession.default.isPaired, + WCSession.default.isWatchAppInstalled else { return } + + try? WCSession.default.updateApplicationContext(data) + } +} +``` + +### WatchConnectivity Methods + +| Method | Use Case | +|--------|----------| +| `updateApplicationContext` | Latest state that persists (use this for most syncs) | +| `sendMessage` | Immediate delivery when counterpart is reachable | +| `transferUserInfo` | Queued delivery, guaranteed but not immediate | + +### watchOS Framework Limitations + +These iOS frameworks are NOT available on watchOS: + +- ❌ `CoreImage` - Generate QR codes on iOS, send image data to watch +- ❌ `UIKit` (mostly) - Use SwiftUI +- ❌ `AVFoundation` (limited) + +### Simulator Limitations + +WatchConnectivity on simulators is **unreliable**: + +- `isWatchAppInstalled` often returns `false` even when running +- `isReachable` may be `false` even with both apps running +- `updateApplicationContext` may fail with "counterpart not installed" + +**Workarounds for simulator testing:** +1. Add `#if targetEnvironment(simulator)` blocks with sample data +2. Test real sync functionality on physical devices only + +### Debugging Watch Sync Issues + +If `isWatchAppInstalled` returns `false`: + +1. ✅ Check "Embed Watch Content" build phase exists +2. ✅ Check "Code Sign On Copy" is enabled +3. ✅ Verify bundle ID is prefixed correctly +4. ✅ Clean build folder (⇧⌘K) and rebuild +5. ✅ On iPhone, open Watch app → verify app appears under "Installed" + +### NSObject Requirement + +`WCSessionDelegate` is an Objective-C protocol, so conforming classes must inherit from `NSObject`: + +```swift +final class WatchConnectivityService: NSObject, WCSessionDelegate { + // NSObject is required for WCSessionDelegate conformance +} +``` + + +## SwiftData Instructions + +If SwiftData is configured to use CloudKit: + +- Never use `@Attribute(.unique)`. +- Model properties must have default values or be optional. +- All relationships must be marked optional. + + +## Model Design: Single Source of Truth + +**Computed properties should be the single source of truth for derived data.** + +### Name Fields Pattern + +When a model has multiple name components (prefix, firstName, middleName, lastName, suffix, etc.), use a computed property for the display name: + +```swift +// ✅ GOOD - Computed from individual fields +var fullName: String { + var parts: [String] = [] + if !prefix.isEmpty { parts.append(prefix) } + if !firstName.isEmpty { parts.append(firstName) } + if !lastName.isEmpty { parts.append(lastName) } + // ... etc + return parts.joined(separator: " ") +} + +// ❌ BAD - Stored displayName that can get out of sync +var displayName: String // Never add this +``` + +### Benefits + +- **Always up to date**: Changes to individual fields are immediately reflected +- **No sync bugs**: No risk of stored value diverging from component fields +- **Simpler code**: No need to update displayName when editing name fields + +### Related Properties + +If you need different formats for different purposes: + +- `fullName` — For display (may include formatting like quotes, parentheses) +- `vCardName` — For export (plain format, no special formatting) + + +## Localization Instructions + +- Use **String Catalogs** (`.xcstrings` files) for localization. +- SwiftUI `Text("literal")` views automatically look up strings in the catalog. +- For strings outside of `Text` views, use `String(localized:)` or a helper extension. +- Store all user-facing strings in the String Catalog. +- Support at minimum: English (en), Spanish-Mexico (es-MX), French-Canada (fr-CA). +- Never use `NSLocalizedString`; prefer `String(localized:)`. + + +## Design Constants + +**Never use raw numeric literals or hardcoded colors directly in views.** + +### Values That MUST Be Constants + +- **Spacing & Padding**: `Design.Spacing.medium` not `.padding(12)` +- **Corner Radii**: `Design.CornerRadius.large` not `cornerRadius: 16` +- **Font Sizes**: `Design.BaseFontSize.body` not `size: 14` +- **Opacity Values**: `Design.Opacity.strong` not `.opacity(0.7)` +- **Colors**: `Color.Primary.accent` not `Color(red:green:blue:)` +- **Line Widths**: `Design.LineWidth.medium` not `lineWidth: 2` +- **Shadow Values**: `Design.Shadow.radiusLarge` not `radius: 10` +- **Animation Durations**: `Design.Animation.quick` not `duration: 0.3` +- **Component Sizes**: `Design.Size.avatar` not `frame(width: 56)` + +### Organization + +- Create a `DesignConstants.swift` file using enums for namespacing. +- Extend `Color` with semantic color definitions. +- View-specific constants go at the top of the view struct with a comment. +- Name constants semantically: `accent` not `pointSix`, `large` not `sixteen`. + + +## App Identifiers (xcconfig) + +**Centralize all company-specific identifiers** using xcconfig files for true single-source configuration. This enables one-line migration between developer accounts. + +### 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:** + +``` +/* Use SOURCE_ROOT and full path from project root */ +EACONFIG001 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Base.xcconfig; sourceTree = SOURCE_ROOT; }; +EACONFIG002 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; }; +EACONFIG003 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppName/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; }; +``` + +**IMPORTANT**: Use `sourceTree = SOURCE_ROOT` (not `""`) and include the full path from project root (e.g., `AppName/Configuration/Base.xcconfig`). + +**2. Set `baseConfigurationReference` on project-level Debug/Release configurations:** + +``` +EA123456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EACONFIG002 /* Debug.xcconfig */; + buildSettings = { ... }; +}; +``` + +**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 + +**Why this is needed:** Swift code cannot read xcconfig files directly. The xcconfig values flow through Info.plist, and this Swift file provides a clean API to access them at runtime. Without this file, you'd have to call `Bundle.main.object(forInfoDictionaryKey:)` everywhere you need an identifier. + +**When to use:** Any Swift code that needs App Group identifiers, CloudKit containers, custom domains, or other configuration values must use `AppIdentifiers.*` instead of hardcoding strings. + +Create `Configuration/AppIdentifiers.swift`: + +```swift +import Foundation + +enum AppIdentifiers { + // 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" + } + + static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" } + static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } + + static func appClipURL(recordName: String) -> URL? { + URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)") + } +} +``` + +### Data Flow + +``` +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) +``` + +### Adding New Targets + +When adding new targets (Widgets, Intents, App Clips, etc.), follow this pattern: + +#### 1. Add Bundle ID Variable to Base.xcconfig + +``` +// In Base.xcconfig, add new derived identifier +WIDGET_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Widget +INTENT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Intent +``` + +#### 2. Set Target to Use xcconfig + +For the new target's Debug/Release configurations in `project.pbxproj`: + +``` +EA_NEW_TARGET_DEBUG /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EACONFIG002 /* Debug.xcconfig */; + buildSettings = { + PRODUCT_BUNDLE_IDENTIFIER = "$(WIDGET_BUNDLE_IDENTIFIER)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + // ... other settings + }; +}; +``` + +#### 3. Configure Entitlements (if needed) + +If the target needs App Groups or CloudKit access, create an entitlements file using variables: + +```xml + +com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + +``` + +#### 4. Share Code via App Groups + +Extensions must use App Groups to share data with the main app: + +```swift +// In extension code +let sharedDefaults = UserDefaults(suiteName: AppIdentifiers.appGroupIdentifier) +let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier +) +``` + +#### 5. Update AppIdentifiers.swift (if needed) + +Add new computed properties for target-specific identifiers: + +```swift +static var widgetBundleIdentifier: String { "\(bundleIdentifier).Widget" } +static var intentBundleIdentifier: String { "\(bundleIdentifier).Intent" } +``` + +#### Common Target Types and Bundle ID Patterns + +| Target Type | Bundle ID Variable | Example Value | +|-------------|-------------------|---------------| +| Widget Extension | `WIDGET_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Widget` | +| Intent Extension | `INTENT_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Intent` | +| App Clip | `APPCLIP_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).Clip` | +| Watch App | `WATCH_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).watchkitapp` | +| Notification Extension | `NOTIFICATION_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).NotificationExtension` | +| Share Extension | `SHARE_BUNDLE_IDENTIFIER` | `$(APP_BUNDLE_IDENTIFIER).ShareExtension` | + +#### Checklist for New Targets + +- [ ] Add bundle ID variable to `Base.xcconfig` +- [ ] Set `baseConfigurationReference` to Debug/Release xcconfig +- [ ] Use `$(VARIABLE)` for `PRODUCT_BUNDLE_IDENTIFIER` +- [ ] Use `$(DEVELOPMENT_TEAM)` for team +- [ ] Create entitlements with `$(APP_GROUP_IDENTIFIER)` if sharing data +- [ ] Add to `AppIdentifiers.swift` if Swift code needs the identifier +- [ ] Register App ID in Apple Developer Portal (uses same App Group) + +### Migration + +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—including all extension targets. + + +## Dynamic Type Instructions + +- Always support Dynamic Type for accessibility. +- Use `@ScaledMetric` to scale custom dimensions. +- Choose appropriate `relativeTo` text styles based on semantic purpose. +- For constrained UI elements, you may use fixed sizes but document the reason. +- Prefer system text styles: `.font(.body)`, `.font(.title)`, `.font(.caption)`. + + +## VoiceOver Accessibility Instructions + +- All interactive elements must have meaningful `.accessibilityLabel()`. +- Use `.accessibilityValue()` for dynamic state. +- Use `.accessibilityHint()` to describe what happens on interaction. +- Use `.accessibilityAddTraits()` for element type. +- Hide decorative elements with `.accessibilityHidden(true)`. +- Group related elements to reduce navigation complexity. +- Post accessibility announcements for important events. + + +## Project Structure + +- Use a consistent project structure organized by feature. +- Follow strict naming conventions for types, properties, and methods. +- **One public type per file**—break types into separate files. +- Write unit tests for core application logic. +- Only write UI tests if unit tests are not possible. +- Add code comments and documentation as needed. +- Never include secrets or API keys in the repository. + + +## Documentation Instructions + +- **Keep `README.md` files up to date** when adding new functionality. +- In multi-project workspaces, update the relevant project's `README.md`. +- Document new features, settings, or mechanics in the appropriate README. +- Update documentation when modifying existing behavior. +- Include configuration options and special interactions. +- README updates should be part of the same commit as the feature. + + +## PR Instructions + +- If installed, ensure SwiftLint returns no warnings or errors. +- Verify that documentation reflects any new functionality. +- Check for duplicate code before submitting. +- Ensure all new files follow the one-type-per-file rule. diff --git a/SecureStorageSample.code-workspace b/SecureStorageSample.code-workspace new file mode 100644 index 0000000..edc6410 --- /dev/null +++ b/SecureStorageSample.code-workspace @@ -0,0 +1,20 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "localPackages/SharedPackage" + }, + { + "path": "../../_Packages/LocalData" + } + ], + "settings": { + "terminal.integrated.enablePersistentSessions": true, + "terminal.integrated.persistentSessionReviveProcess": "onExitAndWindowClose", + "task.allowAutomaticTasks": "off", + "swift.disableAutoResolve": true, + "swift.disableSwiftPackageManagerIntegration": true + } +} diff --git a/SecureStorageSample.xcodeproj/project.pbxproj b/SecureStorageSample.xcodeproj/project.pbxproj index 5da6fb1..c65f894 100644 --- a/SecureStorageSample.xcodeproj/project.pbxproj +++ b/SecureStorageSample.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ EA65D70D2F17DDEB00C48466 /* SecureStorageSample Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; }; EA65D9452F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; }; + EA756CEA2F465F31006196BB /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA756CE92F465F31006196BB /* LocalData */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -122,6 +123,7 @@ files = ( EA2390152F18361C00AC8894 /* SharedKit in Frameworks */, EA238DFB2F1832C600AC8894 /* SharedKit in Frameworks */, + EA756CEA2F465F31006196BB /* LocalData in Frameworks */, EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */, EA179D562F17379800B1D54A /* LocalData in Frameworks */, EA238DF82F18328100AC8894 /* LocalData in Frameworks */, @@ -241,6 +243,7 @@ EA238DF72F18328100AC8894 /* LocalData */, EA238DFA2F1832C600AC8894 /* SharedKit */, EA2390142F18361C00AC8894 /* SharedKit */, + EA756CE92F465F31006196BB /* LocalData */, ); productName = SecureStorageSample; productReference = EA179D012F1722BB00B1D54A /* SecureStorageSample.app */; @@ -406,8 +409,8 @@ mainGroup = EA179CF82F1722BB00B1D54A; minimizedProjectReferenceProxies = 1; packageReferences = ( - EA238DF62F18328100AC8894 /* XCLocalSwiftPackageReference "../remotePackages/LocalData" */, EA2390132F18361C00AC8894 /* XCLocalSwiftPackageReference "localPackages/SharedPackage" */, + EA756CE82F465F31006196BB /* XCLocalSwiftPackageReference "../../_Packages/LocalData" */, ); preferredProjectObjectVersion = 77; productRefGroup = EA179D022F1722BB00B1D54A /* Products */; @@ -579,6 +582,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -643,6 +647,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -979,14 +984,14 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - EA238DF62F18328100AC8894 /* XCLocalSwiftPackageReference "../remotePackages/LocalData" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = ../remotePackages/LocalData; - }; EA2390132F18361C00AC8894 /* XCLocalSwiftPackageReference "localPackages/SharedPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = localPackages/SharedPackage; }; + EA756CE82F465F31006196BB /* XCLocalSwiftPackageReference "../../_Packages/LocalData" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../_Packages/LocalData; + }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1015,6 +1020,10 @@ isa = XCSwiftPackageProductDependency; productName = SharedKit; }; + EA756CE92F465F31006196BB /* LocalData */ = { + isa = XCSwiftPackageProductDependency; + productName = LocalData; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = EA179CF92F1722BB00B1D54A /* Project object */;