Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
af64f0a7d9
commit
15219cd2ce
65
Agents.md
65
Agents.md
@ -4,9 +4,13 @@ This repository contains an Xcode project written with Swift and SwiftUI. Please
|
||||
|
||||
## Additional Context Files (Read First)
|
||||
|
||||
**These files are kept in sync and MUST be updated together when making changes:**
|
||||
|
||||
- `README.md` — Product scope, features, and project structure
|
||||
- `ai_implmentation.md` — AI implementation context and architecture notes
|
||||
|
||||
See **Documentation Instructions** section for mandatory sync requirements.
|
||||
|
||||
|
||||
## Role
|
||||
|
||||
@ -344,6 +348,43 @@ If SwiftData is configured to use CloudKit:
|
||||
- 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.
|
||||
@ -411,6 +452,30 @@ If SwiftData is configured to use CloudKit:
|
||||
|
||||
## Documentation Instructions
|
||||
|
||||
### MANDATORY: Keep Documentation Files in Sync
|
||||
|
||||
⚠️ **These three files MUST be updated together when making architectural changes:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Agents.md` | Agent instructions and coding standards |
|
||||
| `README.md` | Product scope, features, and project structure |
|
||||
| `ai_implmentation.md` | AI implementation context and architecture notes |
|
||||
|
||||
**When to update all three:**
|
||||
- Adding new features or services
|
||||
- Changing architecture patterns (e.g., WatchConnectivity vs App Groups)
|
||||
- Modifying model structure (e.g., adding/removing properties)
|
||||
- Learning important lessons (e.g., watch embedding requirements)
|
||||
- Changing project structure or file organization
|
||||
|
||||
**Update checklist:**
|
||||
1. ✅ `Agents.md` — Add instructions/guidelines for the change
|
||||
2. ✅ `README.md` — Update features, architecture, or structure sections
|
||||
3. ✅ `ai_implmentation.md` — Update models, services, files, or lessons learned
|
||||
|
||||
### General Documentation Rules
|
||||
|
||||
- **Keep `README.md` up to date** when adding new functionality.
|
||||
- Document new features, settings, or mechanics in the README.
|
||||
- Update the README when modifying existing behavior.
|
||||
|
||||
17
README.md
17
README.md
@ -96,9 +96,10 @@ Each field has:
|
||||
|
||||
### watchOS App
|
||||
|
||||
- Shows the default card QR code
|
||||
- Pick which card is the default on watch
|
||||
- **Syncs with iPhone** via WatchConnectivity
|
||||
- Shows the **default card QR code** full-screen (no card picker)
|
||||
- Default card is determined by the iPhone app
|
||||
- **Syncs with iPhone** via WatchConnectivity framework
|
||||
- QR codes are pre-generated on iPhone (CoreImage not available on watchOS)
|
||||
|
||||
## Data Sync
|
||||
|
||||
@ -108,10 +109,12 @@ Cards and contacts are stored using SwiftData with CloudKit sync enabled. Your d
|
||||
|
||||
### iPhone to Watch Sync
|
||||
|
||||
The iPhone app syncs card data to the paired Apple Watch via WatchConnectivity framework. When you create, edit, or delete cards on your iPhone, the changes are pushed to your watch automatically.
|
||||
The iPhone app syncs card data to the paired Apple Watch via WatchConnectivity framework using `updateApplicationContext`. When you create, edit, or delete cards on your iPhone, the changes are pushed to your watch automatically.
|
||||
|
||||
**Note**: The watch receives data from the iPhone. To update cards on the watch, make changes on the iPhone first. QR codes are pre-generated on iPhone since CoreImage is not available on watchOS.
|
||||
|
||||
**IMPORTANT**: Do NOT use App Groups for iPhone-Watch communication. App Groups only work between an app and its extensions on the SAME device. iPhone and Apple Watch are separate devices with separate file systems.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Clean Architecture**: Clear separation between Views, State, Services, and Models
|
||||
@ -119,10 +122,12 @@ The iPhone app syncs card data to the paired Apple Watch via WatchConnectivity f
|
||||
- **State is smart**: `@Observable` classes with all business logic
|
||||
- **Protocol-oriented**: Interfaces for services and stores
|
||||
- **SwiftData + CloudKit**: Persistent storage with iCloud sync
|
||||
- **WatchConnectivity**: iPhone to Apple Watch sync (NOT App Groups)
|
||||
- **One type per file**: Lean, maintainable files under 300 lines
|
||||
- **Reusable components**: Shared UI in `Views/Components/`
|
||||
- **Bedrock package**: Shared design system and utilities
|
||||
- **String Catalogs**: Localization for en, es-MX, fr-CA
|
||||
- **Single source of truth for names**: `BusinessCard.fullName` is computed from individual name fields (no stored displayName)
|
||||
|
||||
## Dependencies
|
||||
|
||||
@ -152,14 +157,14 @@ BusinessCard/
|
||||
│ └── ContactFieldType.swift # Field type definitions with icons & URLs
|
||||
├── Protocols/ # Protocol definitions
|
||||
├── Resources/ # String Catalogs (.xcstrings)
|
||||
├── Services/ # Share link service, watch sync
|
||||
├── Services/ # ShareLinkService, VCardFileService, WatchConnectivityService
|
||||
├── State/ # Observable stores (CardStore, ContactsStore)
|
||||
└── Views/
|
||||
├── Components/ # Reusable UI (ContactFieldPickerView, HeaderLayoutPickerView, etc.)
|
||||
├── Sheets/ # Modal sheets (ContactFieldEditorSheet, RecordContactSheet, etc.)
|
||||
└── [Feature].swift # Feature screens
|
||||
|
||||
BusinessCardWatch/ # watchOS app target
|
||||
BusinessCardWatch Watch App/ # watchOS app target (WatchConnectivity sync)
|
||||
BusinessCardTests/ # Unit tests
|
||||
```
|
||||
|
||||
|
||||
@ -25,9 +25,19 @@ BusinessCard is a SwiftUI app for building and sharing digital business cards wi
|
||||
- `ContactsStore` (contact list + search)
|
||||
- `ShareLinkService` (share URLs)
|
||||
- **SwiftData** with CloudKit for persistence and sync.
|
||||
- **App Groups** for iOS-Watch data sharing.
|
||||
- **WatchConnectivity** for iOS-Watch data sharing (NOT App Groups - see below).
|
||||
- Views read state via environment and render UI only.
|
||||
|
||||
### CRITICAL: WatchConnectivity vs App Groups
|
||||
|
||||
**App Groups do NOT work for iPhone ↔ Apple Watch communication.**
|
||||
|
||||
- App Groups only work between an app and its extensions on the SAME device
|
||||
- iPhone and Apple Watch are different devices with separate file systems
|
||||
- Use **WatchConnectivity** framework instead
|
||||
|
||||
The `WatchConnectivityService` on iOS uses `updateApplicationContext` to push card data to the watch. The watch receives this data in `WatchConnectivityService` and updates `WatchCardStore`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Bedrock Package
|
||||
@ -49,14 +59,18 @@ App-specific extensions are in `Design/DesignConstants.swift`:
|
||||
|
||||
- `Models/BusinessCard.swift` — SwiftData model with:
|
||||
- Name fields: prefix, firstName, middleName, lastName, maidenName, suffix, preferredName
|
||||
- Basic fields: role, company, email, phone, website, location
|
||||
- Basic fields: role, company
|
||||
- Rich fields: pronouns, bio, headline, accreditations (comma-separated tags)
|
||||
- **Dynamic contact fields**: `@Relationship` to array of `ContactField` objects
|
||||
- Legacy fields: social links (LinkedIn, Twitter, etc.) for backward compatibility
|
||||
- Photos: `photoData` (profile), `coverPhotoData` (banner background), `logoData` (company logo) stored with `@Attribute(.externalStorage)`
|
||||
- Computed: `theme`, `layoutStyle`, `vCardPayload`, `orderedContactFields`
|
||||
- Computed: `theme`, `layoutStyle`, `vCardPayload`, `orderedContactFields`, `fullName`, `vCardName`
|
||||
- Helper methods: `addContactField`, `removeContactField`, `reorderContactFields`
|
||||
|
||||
**Name Properties (Single Source of Truth):**
|
||||
- `fullName` — Computed from individual name fields. Includes special formatting: preferred name in quotes, maiden name and pronouns in parentheses. THIS IS THE ONLY SOURCE for display names.
|
||||
- `vCardName` — Plain name for vCard export (no quotes or parentheses formatting).
|
||||
- ⚠️ There is NO stored `displayName` property. Always use `fullName` for display.
|
||||
|
||||
- `Models/ContactField.swift` — SwiftData model for dynamic contact fields:
|
||||
- Properties: `typeId`, `value`, `title`, `orderIndex`
|
||||
- Relationship: `card` (inverse to BusinessCard)
|
||||
@ -99,7 +113,8 @@ App-specific extensions are in `Design/DesignConstants.swift`:
|
||||
### Services
|
||||
|
||||
- `Services/ShareLinkService.swift` — share URL helpers
|
||||
- `Services/WatchSyncService.swift` — App Group sync to watch
|
||||
- `Services/VCardFileService.swift` — vCard file generation for sharing
|
||||
- `Services/WatchConnectivityService.swift` — WatchConnectivity sync to watch (uses `updateApplicationContext`)
|
||||
|
||||
### Views
|
||||
|
||||
@ -160,11 +175,20 @@ Small utilities:
|
||||
|
||||
### watchOS
|
||||
|
||||
- `BusinessCardWatch/BusinessCardWatchApp.swift`
|
||||
- `BusinessCardWatch/Views/WatchContentView.swift`
|
||||
- `BusinessCardWatch/State/WatchCardStore.swift`
|
||||
- `BusinessCardWatch/Models/WatchCard.swift`
|
||||
- `BusinessCardWatch/Resources/Localizable.xcstrings`
|
||||
- `BusinessCardWatch Watch App/BusinessCardWatchApp.swift` — App entry point
|
||||
- `BusinessCardWatch Watch App/Views/WatchContentView.swift` — Shows default card QR code only (no picker)
|
||||
- `BusinessCardWatch Watch App/State/WatchCardStore.swift` — Receives cards from iPhone via WatchConnectivity
|
||||
- `BusinessCardWatch Watch App/Services/WatchConnectivityService.swift` — Receives `updateApplicationContext` from iPhone
|
||||
- `BusinessCardWatch Watch App/Models/WatchCard.swift` — Simplified card struct with `fullName`, `role`, `company`, `qrCodeData`, `isDefault`
|
||||
- `BusinessCardWatch Watch App/Design/WatchDesignConstants.swift` — Watch-specific design constants
|
||||
- `BusinessCardWatch Watch App/Resources/Localizable.xcstrings`
|
||||
|
||||
**Watch Architecture Notes:**
|
||||
- Watch displays ONLY the default card (no card picker UI)
|
||||
- Default card is determined by iPhone's `isDefault` flag
|
||||
- QR codes are pre-generated on iPhone (CoreImage not available on watchOS)
|
||||
- Cards sync via `updateApplicationContext` (persists even when apps not running)
|
||||
- Bundle ID must be prefixed with iOS bundle ID (e.g., `com.mbrucedogs.BusinessCard.watchkitapp`)
|
||||
|
||||
## File Guidelines
|
||||
|
||||
@ -213,6 +237,39 @@ Small utilities:
|
||||
- Widget previews are not WidgetKit extensions.
|
||||
- See `ROADMAP.md` for full feature status.
|
||||
|
||||
## Lessons Learned / Common Pitfalls
|
||||
|
||||
### Watch App Not Installing
|
||||
|
||||
If `isWatchAppInstalled` returns `false` even with both apps running:
|
||||
|
||||
1. **Check "Embed Watch Content" build phase** in iOS target's Build Phases
|
||||
2. **Ensure "Code Sign On Copy" is CHECKED** ← This is the #1 cause
|
||||
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"
|
||||
|
||||
### Simulator WatchConnectivity Issues
|
||||
|
||||
WatchConnectivity is **unreliable on simulators**:
|
||||
- `isWatchAppInstalled` often returns `false` even when running
|
||||
- `isReachable` may be `false` even with both apps running
|
||||
- `updateApplicationContext` may fail with "counterpart not installed"
|
||||
|
||||
**Solution**: Test real sync functionality on physical devices only. Use `#if targetEnvironment(simulator)` blocks for UI testing with sample data if needed.
|
||||
|
||||
### Single Source of Truth for Names
|
||||
|
||||
The `BusinessCard` model uses individual name fields (prefix, firstName, middleName, lastName, etc.) and computes `fullName` dynamically. There is NO stored `displayName` property.
|
||||
|
||||
- ✅ Use `card.fullName` for display everywhere
|
||||
- ✅ Use `card.vCardName` for vCard export
|
||||
- ❌ Never add a stored `displayName` property
|
||||
|
||||
### CoreImage on watchOS
|
||||
|
||||
CoreImage is NOT available on watchOS. QR codes must be generated on iOS and sent to the watch as `Data`.
|
||||
|
||||
## If You Extend The App
|
||||
|
||||
1. Add new strings to the String Catalogs.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user