Technical Architecture Proposal

Cubby — Shared Homes

Enabling two users on different iPhones with different Apple IDs to share a Cubby home. When one adds an item, it appears for the other.

Version 1.2 Updated February 16, 2026 Status Draft Platform iOS / SwiftUI
Watercolor owl on books
The archivist reviews the current state of affairs

Current Architecture

Data Models SwiftData @Model

CloudKit Integration

Photos

Entitlements

Architecture Options, Ranked

🥈 Option 2: Direct CloudKit API (CKShare + CKRecord + Custom Sync)

Use CloudKit framework directly. Create CKShare objects for shared zones, manage CKRecords manually, build your own sync engine.

Maximum control, but you're building a sync engine from scratch — conflict resolution, error handling, retry logic, offline support — all on you. Months of work.

🥉 Option 3: Hybrid — SwiftData (Private) + Core Data (Shared)

Keep SwiftData for private data, add a parallel Core Data stack specifically for shared homes.

Two persistence stacks is complex. Data duplication risk. If a home transitions from private to shared, you need to migrate records between stores.

Option 4: SharePlay / Group Activities Not Suitable

SharePlay is for real-time collaborative sessions (watching movies, whiteboarding). Requires FaceTime/Messages. Does NOT provide persistent shared state.

Option 5: SwiftData Shared Database Not Possible

As of iOS 19 / WWDC 2025, SwiftData still does NOT support CloudKit shared or public databases. Apple DTS explicitly confirms this.

Option 6: Custom Server (Firebase, Supabase, etc.) Overkill

Adds server costs, account management, and a dependency outside Apple's ecosystem. Only consider this if you need sharing with non-Apple users.

Option 7: iCloud Family Sharing Not Applicable

Family Sharing shares purchases, subscriptions, and storage plans. It does not provide a shared CloudKit database or any mechanism for sharing app data.

Watercolor owl in flight with key
The chosen path forward

Core Data + NSPersistentCloudKitContainer Sharing

This is the only Apple-supported path for sharing structured data between iCloud accounts with automatic sync.

Key Concepts

Three CloudKit Database Scopes

  1. Private — User's own data (current Cubby behavior)
  2. Shared — Data shared with this user by others (received shares)
  3. Public — App-wide data visible to all users (not relevant here)

CKShare

A CKShare is a CloudKit record that represents a sharing relationship. It lives in a shared zone in the owner's private database, contains participant info, has a url property for generating share links, and supports read-only or read-write permissions per participant.

How NSPersistentCloudKitContainer Sharing Works

  1. Owner creates a CKShare for a Core Data object (the Home)
  2. The container automatically creates a shared CloudKit zone for that object and its relationships
  3. Owner sends the share URL to the participant
  4. Participant opens the URL, triggering userDidAcceptCloudKitShareWith in the app
  5. The container automatically mirrors shared records into the participant's shared persistent store
  6. Both users see the same data; changes sync automatically

Architecture Design

┌─────────────────────────────────────────────────────┐ NSPersistentCloudKitContainer ┌───────────────────┐ ┌─────────────────────┐ Private Store Shared Store (my own homes) (homes shared with me) CloudKit CloudKit Private DB Shared DB └───────────────────┘ └─────────────────────┘ └─────────────────────────────────────────────────────┘

You need two persistent store descriptions: one for the private CloudKit database (user's own data) and one for the shared CloudKit database (data shared by others).

Setting Up the Container

Swiftimport CoreData
import CloudKit

class PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentCloudKitContainer

    // The store that maps to the private CloudKit database
    private var privatePersistentStore: NSPersistentStore?
    // The store that maps to the shared CloudKit database
    private var sharedPersistentStore: NSPersistentStore?

    init() {
        container = NSPersistentCloudKitContainer(name: "Cubby")

        // Private store
        guard let privateDescription = container.persistentStoreDescriptions.first else {
            fatalError("No store descriptions found")
        }
        privateDescription.url = Self.privateStoreURL
        privateDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
            containerIdentifier: "iCloud.com.barronroth.Cubby"
        )
        privateDescription.cloudKitContainerOptions?.databaseScope = .private

        // Shared store — separate SQLite file
        let sharedDescription = NSPersistentStoreDescription(url: Self.sharedStoreURL)
        sharedDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
            containerIdentifier: "iCloud.com.barronroth.Cubby"
        )
        sharedDescription.cloudKitContainerOptions?.databaseScope = .shared

        container.persistentStoreDescriptions = [privateDescription, sharedDescription]

        // Enable persistent history tracking (required)
        for description in container.persistentStoreDescriptions {
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
            description.setOption(true as NSNumber,
                forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        }

        container.loadPersistentStores { description, error in
            if let error { fatalError("Failed to load store: \(error)") }
        }

        // Assign store references
        for store in container.persistentStoreCoordinator.persistentStores {
            let scope = store.cloudKitContainerOptions?.databaseScope
            if scope == .private { privatePersistentStore = store }
            else if scope == .shared { sharedPersistentStore = store }
        }

        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
}

Creating a Share

Swiftextension PersistenceController {
    func shareHome(
        _ home: Home,
        completion: @escaping (CKShare?, Error?) -> Void
    ) {
        guard let privatePersistentStore else { return }

        // Shares the Home AND all relationships (StorageLocations, Items)
        container.share(
            [home],
            to: nil, // nil = create new share
            completion: { objectIDs, share, container, error in
                if let error {
                    completion(nil, error)
                    return
                }
                share?[CKShare.SystemFieldKey.title] = home.name
                share?.publicPermission = .none
                self.container.persistCKSharesAndWait()
                completion(share, nil)
            }
        )
    }
}

Presenting the Share UI

Swiftstruct CloudSharingView: UIViewControllerRepresentable {
    let share: CKShare
    let container: NSPersistentCloudKitContainer

    func makeUIViewController(context: Context) -> UICloudSharingController {
        let controller = UICloudSharingController(
            share: share,
            container: CKContainer(identifier: "iCloud.com.barronroth.Cubby")
        )
        controller.delegate = context.coordinator
        controller.availablePermissions = [.allowReadWrite, .allowPrivate]
        return controller
    }
    // ... coordinator with UICloudSharingControllerDelegate
}

Accepting a Share

Swift@main
struct CubbyApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext,
                    persistenceController.container.viewContext)
                .onOpenURL { url in
                    acceptShare(from: url)
                }
        }
    }

    private func acceptShare(from url: URL) {
        let ckContainer = CKContainer(identifier: "iCloud.com.barronroth.Cubby")
        CKContainer.default().fetchShareMetadata(with: url) { metadata, error in
            guard let metadata else { return }
            persistenceController.container.acceptShareInvitations(
                from: [metadata],
                into: persistenceController.sharedPersistentStore!
            ) { _, error in
                if let error { print("Failed to accept share: \(error)") }
            }
        }
    }
}

Querying Across Both Stores

Swift// Fetch all homes (both private and shared)
let request = NSFetchRequest<Home>(entityName: "Home")

// Only shared homes:
request.affectedStores = [sharedPersistentStore!]

// Only private homes:
request.affectedStores = [privatePersistentStore!]

// Is a specific object shared?
func isShared(_ object: NSManagedObject) -> Bool {
    guard let store = object.objectID.persistentStore else { return false }
    return store == sharedPersistentStore
}

// Determine edit permissions
func canEdit(_ home: Home) -> Bool {
    guard let share = share(for: home) else { return true }
    if share.currentUserParticipant?.role == .owner { return true }
    return share.currentUserParticipant?.permission == .readWrite
}

Alternative: Direct CloudKit

If you want to avoid Core Data entirely, you could use CKSyncEngine (iOS 17+) with shared zones. However:

⚠️ Not recommended

Unless you have a specific reason to avoid Core Data, this path means building your own sync engine — a significant and error-prone undertaking.

Watercolor owl in moving box
Moving in — the user experience

User Flow Design

Sharing a Home (Owner)

  1. Owner taps "Share" button on a Home's detail/settings screen
  2. App creates a CKShare via NSPersistentCloudKitContainer.share(_:to:completion:)
  3. System presents UICloudSharingController — Apple's standard share sheet
  4. Owner chooses sharing method: iMessage, Mail, AirDrop, copy link
  5. Owner sets permissions: Read Only or Read & Write
  6. Share link is sent to the participant

Accepting a Share (Participant)

  1. Participant taps the share link (e.g., in iMessage)
  2. iOS opens the Cubby app (or App Store if not installed)
  3. App handles the URL in onOpenURL / userDidAcceptCloudKitShareWith
  4. acceptShareInvitations(from:into:) processes it
  5. Shared home data syncs into participant's shared store
  6. Home appears in the home list with a Shared badge

Managing a Share

Required Codebase Changes

1. Migration from SwiftData to Core Data Major

This is the biggest change:

Model Mapping

SwiftData (@Model)Core Data (NSManagedObject)
HomeCDHome entity
StorageLocationCDStorageLocation entity
InventoryItemCDInventoryItem entity

2. Dual Persistent Store Setup

3. Share UI

4. Permission-Aware UI

5. Entitlements

✅ Already Sufficient

Current entitlements (CloudKit, push notifications, container identifier) are all that's needed. Verify that CKSharingSupported key is in Info.plist.

Photo Sharing Strategy

Option A: CKAsset (Simpler alternative)

Store photos as CKAsset on the InventoryItem CloudKit record. Automatically shared when the parent is shared. 250 MB limit per asset. Increases CloudKit storage usage.

Option C: Separate Photo Sync Service

Upload to a shared location (CloudKit assets, S3, etc.), store URL on the item. More complex but more control over caching and bandwidth.

Limitations & Gotchas

🚫 #1 Constraint: SwiftData Cannot Be Used for Sharing

SwiftData (as of iOS 19 / 2025) does not support CloudKit shared or public databases. Apple DTS has confirmed this repeatedly. You must use Core Data.

NSPersistentCloudKitContainer Quirks

  1. Entire zone is shared, not individual records. When you share a Home, ALL records in that zone are shared. This is actually what you want.
  2. Conflict resolution is last-writer-wins. No merge strategy for field-level conflicts. For a household inventory app, this is acceptable.
  3. Shared store is read-only from the container's perspective. Write operations for shared data go through the same context, but under the hood they write to the owner's zone.
  4. No real-time sync. Eventually consistent. Changes typically propagate in seconds to minutes.
  5. CloudKit quotas. ~25 MB free per user. Large photo libraries could hit limits.
  6. Offline behavior. Changes queue locally and sync on reconnect. Conflicts resolved by last-writer-wins.
  7. Share acceptance requires the app. If not installed, share link goes to App Store. Participant must tap link again after install.
  8. UICloudSharingController is UIKit. Needs a UIViewControllerRepresentable wrapper. Not SwiftUI-native.
  9. Testing is painful. Two different iCloud accounts on two devices. Simulator support is limited.
  10. Schema migration. Adding fields is easy (additive). Removing/changing types requires careful handling.

What You Lose by Moving to Core Data

What You Gain

Migration Strategy

Given that Cubby is relatively early-stage and the model layer is simple (3 entities, straightforward relationships), the cleanest path is a full Core Data migration:

  1. Create Core Data model (.xcdatamodeld) matching current SwiftData models
  2. Build PersistenceController with dual private/shared stores
  3. Create NSManagedObject subclasses (or use Xcode codegen)
  4. Port views from @Query to @FetchRequest / @SectionedFetchRequest
  5. Add sharing UI (share button, UICloudSharingController wrapper, share acceptance)
  6. Add data migration for existing users (read SwiftData → write Core Data → delete SwiftData store)
  7. Port photo storage to Core Data binary attribute with external storage
  8. Test thoroughly with two iCloud accounts

Timeline Estimate

PhaseEffort
Core Data model + persistence controller2–3 days
Port views from SwiftData to Core Data3–5 days
Sharing implementation (CKShare, UI, acceptance)3–4 days
Photo sync via CKAsset1–2 days
Data migration for existing users1–2 days
Testing (2 accounts, edge cases, offline)3–5 days
Total~2–3 weeks
Watercolor owl with magnifying glass
The verdict is in

Summary

ApproachFeasibilityEffortRecommendation
Core Data + NSPersistentCloudKitContainer sharingProvenHigh (2–3 weeks)🥇 Recommended
Direct CloudKit (CKSyncEngine + CKShare)PossibleVery High🥈 Only if avoiding Core Data
SwiftData shared DBNot supportedN/ANot possible
Hybrid SwiftData + Core DataComplexHigh🥉 Compromise option
Custom server (Firebase, etc.)PossibleVery HighOverkill
SharePlayWrong toolN/ANot for persistent data
Family SharingNot applicableN/ADoesn't share app data
Bottom line: Migrate to Core Data + NSPersistentCloudKitContainer with dual private/shared stores. It's the only Apple-supported path for CloudKit sharing with a local database, and it's mature enough for production use. The migration from SwiftData is the main cost, but Cubby's simple 3-entity model makes this manageable.

Implementation Checklist