Add CloudKit sync for cross-device data persistence

- Switch CoreDataStack from NSPersistentContainer to NSPersistentCloudKitContainer
- Configure CloudKit container: iCloud.com.t-t.PlantGuide
- Remove uniqueness constraints from all Core Data entities (CloudKit incompatible)
- Add CloudKit container identifier to entitlements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-23 14:21:52 -06:00
parent be0d298d9f
commit d125216a95
3 changed files with 12 additions and 29 deletions

View File

@@ -6,6 +6,7 @@
// for plant identification iOS app. // for plant identification iOS app.
// //
import CloudKit
import CoreData import CoreData
import Foundation import Foundation
@@ -105,7 +106,7 @@ final class CoreDataStack: CoreDataStackProtocol, @unchecked Sendable {
// - The coreDataQueue serializes any direct container operations // - The coreDataQueue serializes any direct container operations
/// The persistent container managing the Core Data stack /// The persistent container managing the Core Data stack
private let persistentContainer: NSPersistentContainer private let persistentContainer: NSPersistentCloudKitContainer
/// Serial queue for thread-safe operations /// Serial queue for thread-safe operations
private let coreDataQueue = DispatchQueue(label: "com.plantguide.coredata", qos: .userInitiated) private let coreDataQueue = DispatchQueue(label: "com.plantguide.coredata", qos: .userInitiated)
@@ -142,8 +143,8 @@ final class CoreDataStack: CoreDataStackProtocol, @unchecked Sendable {
private static func createPersistentContainer( private static func createPersistentContainer(
modelName: String, modelName: String,
migrationConfig: MigrationConfiguration migrationConfig: MigrationConfiguration
) -> NSPersistentContainer { ) -> NSPersistentCloudKitContainer {
let container = NSPersistentContainer(name: modelName) let container = NSPersistentCloudKitContainer(name: modelName)
// Configure store description with migration options // Configure store description with migration options
let storeDescription = container.persistentStoreDescriptions.first let storeDescription = container.persistentStoreDescriptions.first
@@ -154,6 +155,11 @@ final class CoreDataStack: CoreDataStackProtocol, @unchecked Sendable {
storeDescription?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) storeDescription?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
storeDescription?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) storeDescription?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
// Configure CloudKit container
storeDescription?.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.t-t.PlantGuide"
)
container.loadPersistentStores { storeDescription, error in container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? { if let error = error as NSError? {
// Log the error - in production, consider recovery strategies // Log the error - in production, consider recovery strategies

View File

@@ -19,11 +19,6 @@
<relationship name="careSchedule" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CareScheduleMO" inverseName="plant" inverseEntity="CareScheduleMO"/> <relationship name="careSchedule" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CareScheduleMO" inverseName="plant" inverseEntity="CareScheduleMO"/>
<relationship name="identifications" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="IdentificationMO" inverseName="plant" inverseEntity="IdentificationMO"/> <relationship name="identifications" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="IdentificationMO" inverseName="plant" inverseEntity="IdentificationMO"/>
<relationship name="plantCareInfo" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="PlantCareInfoMO" inverseName="plant" inverseEntity="PlantCareInfoMO"/> <relationship name="plantCareInfo" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="PlantCareInfoMO" inverseName="plant" inverseEntity="PlantCareInfoMO"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity> </entity>
<entity name="IdentificationMO" representedClassName="IdentificationMO" syncable="YES"> <entity name="IdentificationMO" representedClassName="IdentificationMO" syncable="YES">
<attribute name="confidenceScore" attributeType="Double" defaultValueString="0.0" usesScalarType="YES"/> <attribute name="confidenceScore" attributeType="Double" defaultValueString="0.0" usesScalarType="YES"/>
@@ -34,11 +29,6 @@
<attribute name="longitude" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarType="NO"/> <attribute name="longitude" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarType="NO"/>
<attribute name="source" attributeType="String"/> <attribute name="source" attributeType="String"/>
<relationship name="plant" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PlantMO" inverseName="identifications" inverseEntity="PlantMO"/> <relationship name="plant" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PlantMO" inverseName="identifications" inverseEntity="PlantMO"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity> </entity>
<entity name="CareScheduleMO" representedClassName="CareScheduleMO" syncable="YES"> <entity name="CareScheduleMO" representedClassName="CareScheduleMO" syncable="YES">
<attribute name="fertilizerSchedule" attributeType="String"/> <attribute name="fertilizerSchedule" attributeType="String"/>
@@ -50,11 +40,6 @@
<attribute name="wateringSchedule" attributeType="String"/> <attribute name="wateringSchedule" attributeType="String"/>
<relationship name="plant" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PlantMO" inverseName="careSchedule" inverseEntity="PlantMO"/> <relationship name="plant" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PlantMO" inverseName="careSchedule" inverseEntity="PlantMO"/>
<relationship name="tasks" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="CareTaskMO" inverseName="careSchedule" inverseEntity="CareTaskMO"/> <relationship name="tasks" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="CareTaskMO" inverseName="careSchedule" inverseEntity="CareTaskMO"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity> </entity>
<entity name="CareTaskMO" representedClassName="CareTaskMO" syncable="YES"> <entity name="CareTaskMO" representedClassName="CareTaskMO" syncable="YES">
<attribute name="completedDate" optional="YES" attributeType="Date" usesScalarType="NO"/> <attribute name="completedDate" optional="YES" attributeType="Date" usesScalarType="NO"/>
@@ -64,11 +49,6 @@
<attribute name="scheduledDate" attributeType="Date" usesScalarType="NO"/> <attribute name="scheduledDate" attributeType="Date" usesScalarType="NO"/>
<attribute name="type" attributeType="String"/> <attribute name="type" attributeType="String"/>
<relationship name="careSchedule" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CareScheduleMO" inverseName="tasks" inverseEntity="CareScheduleMO"/> <relationship name="careSchedule" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CareScheduleMO" inverseName="tasks" inverseEntity="CareScheduleMO"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity> </entity>
<entity name="PlantCareInfoMO" representedClassName="PlantCareInfoMO" syncable="YES"> <entity name="PlantCareInfoMO" representedClassName="PlantCareInfoMO" syncable="YES">
<attribute name="additionalNotes" optional="YES" attributeType="String"/> <attribute name="additionalNotes" optional="YES" attributeType="String"/>
@@ -86,10 +66,5 @@
<attribute name="trefleID" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarType="YES"/> <attribute name="trefleID" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarType="YES"/>
<attribute name="wateringScheduleData" attributeType="Binary"/> <attribute name="wateringScheduleData" attributeType="Binary"/>
<relationship name="plant" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PlantMO" inverseName="plantCareInfo" inverseEntity="PlantMO"/> <relationship name="plant" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PlantMO" inverseName="plantCareInfo" inverseEntity="PlantMO"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity> </entity>
</model> </model>

View File

@@ -5,7 +5,9 @@
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key> <key>com.apple.developer.icloud-container-identifiers</key>
<array/> <array>
<string>iCloud.com.t-t.PlantGuide</string>
</array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
<string>CloudKit</string> <string>CloudKit</string>