8.9 KiB
App Clip Implementation Plan for BusinessCard App
This document outlines the detailed steps to implement the high-priority App Clip feature for instant card sharing, as identified in ROADMAP.md. The goal is to allow recipients to scan a QR code and instantly view/add a full business card (including photo) via an App Clip, without needing to install the full app. This uses CloudKit for temporary storage of a vCard 3.0 payload (including PHOTO field) to address QR code size limitations for large data like images.
The plan follows the phases from ROADMAP.md, with specific code snippets, file changes, and architecture notes. No actual code changes will be made until this plan is reviewed and approved. All new code will adhere to the guidelines in Agents.md: clean architecture, POP, one type per file, SwiftUI with @Observable, etc.
User Flow
Sender Flow (Main App)
- User selects a card and chooses "Share via App Clip".
- App generates vCard 3.0 payload with PHOTO and uploads to CloudKit.
- App generates QR code with App Clip URL pointing to the CloudKit record.
- User shares/shows the QR code.
Recipient Flow (App Clip)
- Recipient scans QR code, launching App Clip.
- App Clip fetches vCard data from CloudKit using the URL's ID.
- App Clip parses vCard and displays preview (name, photo, role, company, etc.).
- Recipient taps "Add to Contacts", parsing vCard to CNContact and saving to contacts app.
- Optional: Mark record as saved or delete from CloudKit.
Sequence Diagram (Text-Based)
Sender (Main App) CloudKit Recipient (App Clip) Contacts App
|-------------------| |----------------| |---------------------| |------------|
1. Generate vCard 3.0 w/ PHOTO
|
|---> Upload SharedCard (vCardData)
|
|<--- Record ID
2. Generate QR with App Clip URL + ID
[QR Shared/Scanned]
3. Launch App Clip w/ URL
|
|---> Fetch SharedCard by ID
|
|<--- vCardData
4. Parse vCard to display preview
5. User taps "Add to Contacts"
|
|---> Parse vCard to CNContact
|
|---> Save Contact
|
|<--- Success
Phase 1: CloudKit Setup
Steps
-
Enable CloudKit Capability:
- In Xcode, go to the iOS target Signing & Capabilities tab.
- Add iCloud capability and enable CloudKit.
- Ensure the container is
iCloud.com.mbrucedogs.BusinessCard(matches existing).
-
Create SharedCard Model:
- Add a new file:
Models/SharedCard.swift. - This is a CloudKit-backed model for ephemeral card sharing.
- Properties: full vCard 3.0 data (with PHOTO), expiration date.
Code Snippet (SharedCard.swift):
import CloudKit import SwiftData @Model final class SharedCard { @Attribute(.unique) var id: UUID var vCardData: Data // Full vCard 3.0 payload including PHOTO var expiresAt: Date init(id: UUID = UUID(), vCardData: Data, expiresAt: Date) { self.id = id self.vCardData = vCardData self.expiresAt = expiresAt } } - Add a new file:
-
Implement Upload Function:
- Add to
Services/ShareLinkService.swift(or create a newCloudKitService.swiftif better separation). - Function to upload a SharedCard with vCard 3.0 (including PHOTO) and return a share URL.
Code Snippet (in ShareLinkService.swift):
func uploadSharedCard(_ card: BusinessCard) async throws -> URL { let vCardPayload = try VCardFileService.generateVCardData(for: card, includePhoto: true) // vCard 3.0 with PHOTO field let sharedCard = SharedCard( vCardData: vCardPayload, expiresAt: Date().addingTimeInterval(60 * 60 * 24) // 24 hours ) // Assuming SwiftData context modelContext.insert(sharedCard) try await modelContext.save() // This syncs to CloudKit // Generate URL like appclips://yourapp.com/shared/\(sharedCard.id) return URL(string: "https://yourapp.com/appclip/shared/\(sharedCard.id.uuidString)")! } - Add to
-
Implement Cleanup:
- In
AppState.swiftor app launch, query and delete expired SharedCards.
Code Snippet:
func cleanupExpiredSharedCards() async { let predicate = #Predicate<SharedCard> { $0.expiresAt < Date() } let descriptor = FetchDescriptor<SharedCard>(predicate: predicate) if let expired = try? modelContext.fetch(descriptor) { for card in expired { modelContext.delete(card) } try? modelContext.save() } } - In
Documentation Updates
- Update README.md: Add section on App Clip sharing, emphasizing vCard with photo.
- Update ai_implementation.md: Add SharedCard model and CloudKit/vCard notes.
- Update Agents.md: Add guidelines for CloudKit and vCard 3.0 usage.
Phase 2: App Clip Target
Steps
-
Create App Clip Target:
- In Xcode: File > New > Target > App Clip.
- Name: BusinessCardAppClip.
- Bundle ID: com.mbrucedogs.BusinessCard.appclip.
- Keep size under 15MB (minimal UI, no heavy assets).
-
Configure Associated Domains:
- Add
appclips:yourapp.comto entitlements. - Set up server-side .well-known/apple-app-site-association file.
- Add
-
Build Minimal UI:
- Create
AppClipView.swift: Display parsed vCard preview with "Add to Contacts" button. - Use SwiftUI for quick loading. Parse vCard for display.
Code Snippet (AppClipView.swift):
import SwiftUI import Contacts struct AppClipView: View { let sharedCard: SharedCard // Fetched from CloudKit @State private var parsedContact: CNContact? // Parsed from vCard var body: some View { VStack { if let contact = parsedContact { Text(contact.givenName + " " + contact.familyName).font(.title) if let photoData = contact.imageData, let image = UIImage(data: photoData) { Image(uiImage: image).resizable().scaledToFit().frame(width: 100, height: 100) } Text(contact.jobTitle).font(.subheadline) Text(contact.organizationName) Button("Add to Contacts") { addToContacts(contact) } } else { Text("Loading...") } } .onAppear { parsedContact = parseVCard(sharedCard.vCardData) } } private func addToContacts(_ contact: CNContact) { let store = CNContactStore() let saveRequest = CNSaveRequest() saveRequest.add(contact.mutableCopy() as! CNMutableContact, toContainerWithIdentifier: nil) try? store.execute(saveRequest) } private func parseVCard(_ data: Data) -> CNContact? { return try? CNContactVCardSerialization.contacts(with: data).first } } - Create
-
Fetch from CloudKit:
- In App Clip's entry point, parse URL parameter to fetch SharedCard by ID.
Phase 3: Main App Integration
-
Add Share Option:
- In
Views/ShareCardView.swift, add a new button for "Share via App Clip".
Code Snippet:
Button("Share via App Clip") { Task { let url = try await shareLinkService.uploadSharedCard(currentCard) // Generate QR with url qrCodeImage = QRCodeGenerator.generateQRCode(from: url.absoluteString) } } - In
-
Generate QR Code:
- Use existing QRCodeGenerator from Bedrock. The QR encodes the App Clip URL, which loads the full vCard from CloudKit.
Phase 4: Testing and Configuration
- Test on device with App Clip invocation, verifying photo inclusion in saved contacts.
- Configure in App Store Connect.
- Add unit tests for vCard generation (with PHOTO), upload, fetch, parsing, and cleanup.
Additional Considerations
- Security: Ensure CloudKit records are public-readable but short-lived. Add authentication if needed for private shares.
- Error Handling: Handle cases like expired records, parsing failures, or no internet in App Clip.
- Performance: Compress photos before including in vCard PHOTO field.
- Accessibility: Ensure App Clip UI supports VoiceOver and Dynamic Type.
- Localization: Add strings for App Clip to String Catalogs.
Risks and Considerations
- CloudKit quotas and costs for storing vCard data with photos.
- Ensure vCard payloads are optimized/compressed.
- Handle parsing errors for vCard data.
- Update all three docs (Agents.md, README.md, ai_implementation.md) upon completion.
- Use vCard 3.0 format with PHOTO field to include images in saved contacts, addressing QR code size limitations for large data like photos.
This plan can be referenced as XAI-Plan-AppClip.md. If approved, we can proceed to implementation in ACT MODE.