307 lines
14 KiB
Markdown
307 lines
14 KiB
Markdown
# AI Implementation Context
|
||
|
||
This file summarizes project-specific context, architecture, and conventions to speed up future AI work.
|
||
|
||
## Project Summary
|
||
|
||
BusinessCard is a SwiftUI app for building and sharing digital business cards with QR codes. It includes iOS screens for cards, sharing, customization, contact tracking, and widget previews, plus a watchOS companion to show a default card QR code.
|
||
|
||
## Key Constraints
|
||
|
||
- iOS 26+, watchOS 12+, Swift 6.2.
|
||
- SwiftUI with `@Observable` classes and `@MainActor`.
|
||
- Protocol‑oriented architecture is prioritized.
|
||
- Clean Architecture with separation of concerns.
|
||
- One public type per file, keep files under 300 lines.
|
||
- No UIKit unless explicitly requested.
|
||
- String Catalogs only (`.xcstrings`).
|
||
- No magic numbers in views; use Bedrock's `Design` constants.
|
||
- Uses **Bedrock** package for shared design system and utilities.
|
||
|
||
## Core Data Flow
|
||
|
||
- `AppState` owns:
|
||
- `CardStore` (cards and selection)
|
||
- `ContactsStore` (contact list + search)
|
||
- `ShareLinkService` (share URLs)
|
||
- **SwiftData** with CloudKit for persistence and sync.
|
||
- **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
|
||
|
||
Located at `/Frameworks/Bedrock` (local package). Provides:
|
||
|
||
- `Design.Spacing`, `Design.CornerRadius`, `Design.Opacity`, etc.
|
||
- `QRCodeGenerator` and `QRCodeImageView` for QR codes
|
||
- Reusable settings components
|
||
|
||
App-specific extensions are in `Design/DesignConstants.swift`:
|
||
- `Design.CardSize` - card dimensions, avatar, QR sizes
|
||
- `Design.Shadow.offsetNone` - zero offset extension
|
||
- `Color.AppBackground`, `Color.CardPalette`, `Color.AppAccent`, `Color.AppText`
|
||
|
||
## Important Files
|
||
|
||
### Configuration (xcconfig)
|
||
|
||
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
|
||
|
||
- `Models/BusinessCard.swift` — SwiftData model with:
|
||
- Name fields: prefix, firstName, middleName, lastName, maidenName, suffix, preferredName
|
||
- Basic fields: role, company
|
||
- Rich fields: pronouns, bio, headline, accreditations (comma-separated tags)
|
||
- **Dynamic contact fields**: `@Relationship` to array of `ContactField` objects
|
||
- Photos: `photoData` (profile), `coverPhotoData` (banner background), `logoData` (company logo) stored with `@Attribute(.externalStorage)`
|
||
- 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)
|
||
- Computed: `fieldType`, `displayName`, `iconImage()`, `iconColor`, `buildURL()`
|
||
- Supports multiple fields of same type, drag-to-reorder
|
||
|
||
- `Models/ContactFieldType.swift` — Struct defining 30+ field types:
|
||
- Categories: contact, social, developer, messaging, payment, creator, scheduling, other
|
||
- Properties: `id`, `displayName`, `systemImage`, `isCustomSymbol`, `iconColor`
|
||
- Properties: `valueLabel`, `valuePlaceholder`, `titleSuggestions`, `keyboardType`
|
||
- Method: `iconImage()` — returns correct Image (asset vs SF Symbol)
|
||
- Method: `urlBuilder` — closure to build deep link URLs
|
||
- Static instances: `.email`, `.phone`, `.linkedIn`, `.twitter`, `.instagram`, etc.
|
||
- Uses custom assets from `Assets.xcassets/SocialSymbols/`
|
||
|
||
- `Models/Contact.swift` — SwiftData model with:
|
||
- Basic fields: name, role, company
|
||
- Annotations: notes, tags (comma-separated), followUpDate, whereYouMet
|
||
- Received cards: isReceivedCard, email, phone
|
||
- Photo: `photoData` stored with `@Attribute(.externalStorage)` - editable via PhotosPicker in ContactDetailView and AddContactSheet
|
||
- Computed: `tagList`, `hasFollowUp`, `isFollowUpOverdue`
|
||
- Static: `fromVCard(_:)` parser
|
||
|
||
- `Models/CardTheme.swift` — card theme palette
|
||
- `Models/CardLayoutStyle.swift` — stacked/split/photo
|
||
- `Models/AppTab.swift` — tab bar enum
|
||
|
||
### Protocols (POP)
|
||
|
||
- `Protocols/BusinessCardProviding.swift` — card selection interface
|
||
- `Protocols/ContactTracking.swift` — contact management interface
|
||
- `Protocols/ShareLinkProviding.swift` — share URL generation interface
|
||
|
||
### State
|
||
|
||
- `State/AppState.swift` — central state container
|
||
- `State/CardStore.swift` — card CRUD, selection, watch sync
|
||
- `State/ContactsStore.swift` — contacts, search, received cards
|
||
|
||
### Services
|
||
|
||
- `Services/ShareLinkService.swift` — share URL helpers
|
||
- `Services/VCardFileService.swift` — vCard file generation for sharing
|
||
- `Services/WatchConnectivityService.swift` — WatchConnectivity sync to watch (uses `updateApplicationContext`)
|
||
|
||
### Views
|
||
|
||
Main screens:
|
||
- `Views/RootTabView.swift` — tabbed shell (3 tabs: My Cards, Contacts, Widgets) + floating share button
|
||
- `Views/CardsHomeView.swift` — full-screen swipeable card view with edit button
|
||
- `Views/ShareCardView.swift` — QR + share actions + track share (opened as sheet from floating button)
|
||
- `Views/ContactsView.swift` — contact list with sections
|
||
- `Views/WidgetsView.swift` — widget preview mockups
|
||
|
||
Feature views:
|
||
- `Views/BusinessCardView.swift` — card display with layouts
|
||
- `Views/CardEditorView.swift` — create/edit cards with PhotosPicker for 3 image types (profile, cover, logo)
|
||
- `Views/ContactDetailView.swift` — full contact view with annotations
|
||
- `Views/QRScannerView.swift` — camera-based QR scanner
|
||
- `Views/QRCodeView.swift` — QR code image generator
|
||
|
||
Reusable components (in `Views/Components/`):
|
||
- `AvatarBadgeView.swift` — circular avatar with photo or icon
|
||
- `IconRowView.swift` — icon + text row for details
|
||
- `LabelBadgeView.swift` — small badge labels
|
||
- `ActionRowView.swift` — generic action row with chevron
|
||
- `ContactFieldPickerView.swift` — grid picker for selecting contact field types
|
||
- `ContactFieldsManagerView.swift` — orchestrates picker + added fields list
|
||
- `AddedContactFieldsView.swift` — displays added fields with drag-to-reorder
|
||
- `PhotoSourcePicker.swift` — generic photo source picker sheet (library, camera, remove)
|
||
- `CameraCaptureView.swift` — UIImagePickerController wrapper for camera capture
|
||
|
||
Sheets (in `Views/Sheets/`):
|
||
- `RecordContactSheet.swift` — track share recipient
|
||
- `ContactFieldEditorSheet.swift` — add/edit contact field with type-specific UI
|
||
- `AddContactSheet.swift` — manually add a new contact
|
||
- `PhotoCropperSheet.swift` — 2-step photo editor with pinch-to-zoom and square crop
|
||
|
||
Small utilities:
|
||
- `Views/EmptyStateView.swift` — empty state placeholder
|
||
- `Views/PrimaryActionButton.swift` — styled action button
|
||
|
||
### Assets
|
||
|
||
- `Assets.xcassets/SocialSymbols/` — custom brand icons as `.symbolset` files:
|
||
- Social: linkedin, x-twitter, instagram, facebook, tiktok, threads, bluesky, mastodon, reddit, twitch
|
||
- Developer: github.fill
|
||
- Messaging: telegram, discord.fill, slack, matrix
|
||
- Creator: patreon.fill, ko-fi
|
||
|
||
**Icon Rendering:**
|
||
- Custom symbols use `Image("symbolName")` (asset catalog)
|
||
- SF Symbols use `Image(systemName: "symbolName")`
|
||
- `ContactFieldType.isCustomSymbol` flag determines which to use
|
||
- `ContactFieldType.iconImage()` returns the correct Image type
|
||
|
||
### Design + Localization
|
||
|
||
- `Design/DesignConstants.swift` — extends Bedrock
|
||
- `Resources/Localizable.xcstrings` — string catalog
|
||
- `Localization/String+Localization.swift` — string helpers
|
||
|
||
### watchOS
|
||
|
||
- `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
|
||
|
||
### Size Limits
|
||
- Main views: aim for under 300 lines
|
||
- Extract reusable sub-views to `Components/`
|
||
- Extract sheets/modals to `Sheets/`
|
||
- Private structs in same file OK if under 50 lines
|
||
|
||
### Current File Sizes
|
||
| File | Lines | Status |
|
||
|------|-------|--------|
|
||
| ContactFieldType | ~650 | 30+ field definitions, acceptable |
|
||
| CardEditorView | ~520 | Complex form + field manager, acceptable |
|
||
| BusinessCardView | ~320 | Clickable fields + legacy fallback, acceptable |
|
||
| QRScannerView | ~310 | Camera + parsing, acceptable |
|
||
| ShareCardView | ~235 | Good |
|
||
| ContactDetailView | ~235 | Good |
|
||
| ContactsView | ~220 | Good |
|
||
| CardsHomeView | ~150 | Full-screen swipeable cards, good |
|
||
| AddedContactFieldsView | ~135 | Drag-to-reorder, good |
|
||
| ContactFieldPickerView | ~100 | Grid picker, good |
|
||
| All others | <110 | Good |
|
||
|
||
## Localization
|
||
|
||
- All user-facing strings are in `.xcstrings`.
|
||
- Supported locales: en, es‑MX, fr‑CA.
|
||
- Use `String.localized("Key")` for non-Text strings.
|
||
|
||
## Testing
|
||
|
||
- `BusinessCardTests/BusinessCardTests.swift` covers:
|
||
- vCard payload formatting
|
||
- Card CRUD operations
|
||
- Contact search and filtering
|
||
- Social links detection
|
||
- Contact notes/tags
|
||
- Follow-up status
|
||
- vCard parsing for received cards
|
||
|
||
## Known Stubs / TODOs
|
||
|
||
- Apple Wallet and NFC flows are alert-only placeholders.
|
||
- Share URLs are sample placeholders.
|
||
- 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.
|
||
2. Use `Design.*` from Bedrock for spacing, opacity, etc.
|
||
3. Add app-specific constants to `DesignConstants.swift`.
|
||
4. Keep view logic UI-only; push business logic to state classes.
|
||
5. Prefer protocols for new capabilities.
|
||
6. Add unit tests for new model logic.
|
||
7. Update `ROADMAP.md` when adding features.
|
||
8. **Keep files under 300 lines** — extract components when needed.
|
||
9. **No duplicate code** — check for existing components first.
|
||
10. **One public type per file** — private helpers OK if small.
|