From 5eef747b4e22e0d088fa9ffc94df5a720b189f29 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 17 May 2018 16:06:11 -0700 Subject: [PATCH 001/203] normalize on Push & Pull nomenclature --- CloudCore.xcodeproj/project.pbxproj | 42 +++++++++---------- Source/Classes/CloudCore.swift | 6 +-- .../FetchPublicSubscriptionsOperation.swift | 0 .../PublicDatabaseSubscriptions.swift | 0 .../PullOperation.swift} | 4 +- .../DeleteFromCoreDataOperation.swift | 0 .../FetchRecordZoneChangesOperation.swift | 0 .../PurgeLocalDatabaseOperation.swift | 0 .../RecordToCoreDataOperation.swift | 0 .../{Save => Push}/CoreDataListener.swift | 18 ++++---- .../Model/RecordIDWithDatabase.swift | 0 .../Model/RecordWithDatabase.swift | 0 .../ObjectToRecord/CoreDataAttribute.swift | 0 .../ObjectToRecord/CoreDataRelationship.swift | 0 .../ObjectToRecordConverter.swift | 0 .../ObjectToRecordOperation.swift | 0 .../PushOperationQueue.swift} | 2 +- .../CreateCloudCoreZoneOperation.swift | 0 .../PushAllLocalDataOperation.swift} | 12 +++--- .../SetupOperation.swift | 2 +- .../SubscribeOperation.swift | 0 21 files changed, 43 insertions(+), 43 deletions(-) rename Source/Classes/{Fetch => Pull}/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift (100%) rename Source/Classes/{Fetch => Pull}/PublicSubscriptions/PublicDatabaseSubscriptions.swift (100%) rename Source/Classes/{Fetch/FetchAndSaveOperation.swift => Pull/PullOperation.swift} (95%) rename Source/Classes/{Fetch => Pull}/SubOperations/DeleteFromCoreDataOperation.swift (100%) rename Source/Classes/{Fetch => Pull}/SubOperations/FetchRecordZoneChangesOperation.swift (100%) rename Source/Classes/{Fetch => Pull}/SubOperations/PurgeLocalDatabaseOperation.swift (100%) rename Source/Classes/{Fetch => Pull}/SubOperations/RecordToCoreDataOperation.swift (100%) rename Source/Classes/{Save => Push}/CoreDataListener.swift (83%) rename Source/Classes/{Save => Push}/Model/RecordIDWithDatabase.swift (100%) rename Source/Classes/{Save => Push}/Model/RecordWithDatabase.swift (100%) rename Source/Classes/{Save => Push}/ObjectToRecord/CoreDataAttribute.swift (100%) rename Source/Classes/{Save => Push}/ObjectToRecord/CoreDataRelationship.swift (100%) rename Source/Classes/{Save => Push}/ObjectToRecord/ObjectToRecordConverter.swift (100%) rename Source/Classes/{Save => Push}/ObjectToRecord/ObjectToRecordOperation.swift (100%) rename Source/Classes/{Save/CloudSaveOperationQueue.swift => Push/PushOperationQueue.swift} (98%) rename Source/Classes/{Setup Operation => Setup}/CreateCloudCoreZoneOperation.swift (100%) rename Source/Classes/{Setup Operation/UploadAllLocalDataOperation.swift => Setup/PushAllLocalDataOperation.swift} (81%) rename Source/Classes/{Setup Operation => Setup}/SetupOperation.swift (94%) rename Source/Classes/{Setup Operation => Setup}/SubscribeOperation.swift (100%) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 6a51003b..b1a1b3b3 100755 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ D985DE9D1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DE9C1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift */; }; D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */; }; D985DEA81FE0292000236870 /* SubscribeOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEA71FE0292000236870 /* SubscribeOperation.swift */; }; - D985DEAB1FE0335800236870 /* UploadAllLocalDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEAA1FE0335800236870 /* UploadAllLocalDataOperation.swift */; }; + D985DEAB1FE0335800236870 /* PushAllLocalDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEAA1FE0335800236870 /* PushAllLocalDataOperation.swift */; }; D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEAD1FE034A900236870 /* NSManagedObjectModel.swift */; }; D9B3C6F61FCEF38D00CDB7FF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C6F51FCEF38D00CDB7FF /* AppDelegate.swift */; }; D9B3C6F81FCEF38D00CDB7FF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C6F71FCEF38D00CDB7FF /* ViewController.swift */; }; @@ -38,7 +38,7 @@ E21FA03E1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21FA03D1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift */; }; E22A53DA1E4A8743009286C0 /* CloudKitAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */; }; E22C40461E42956C009469A1 /* CoreDataListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22C40451E42956C009469A1 /* CoreDataListener.swift */; }; - E23C478C1E48A404004310F9 /* CloudSaveOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478B1E48A404004310F9 /* CloudSaveOperationQueue.swift */; }; + E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478B1E48A404004310F9 /* PushOperationQueue.swift */; }; E247EF8D1E67775500EBD75E /* ErrorBlockProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */; }; E247EF971E67873E00EBD75E /* DeleteFromCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */; }; E247EF9A1E678EAC00EBD75E /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF981E678EA200EBD75E /* CustomFunctions.swift */; }; @@ -65,7 +65,7 @@ E2C3A6D11E4A8EAF009151F3 /* FetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */; }; E2D390081E4A49350019BBCD /* NSEntityDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D390071E4A49350019BBCD /* NSEntityDescription.swift */; }; E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E296C91E49DA0800E7D6ED /* Tokens.swift */; }; - E2E4D8411E76D5A600550CBE /* FetchAndSaveOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E4D83D1E76D4EF00550CBE /* FetchAndSaveOperation.swift */; }; + E2E4D8411E76D5A600550CBE /* PullOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E4D83D1E76D4EF00550CBE /* PullOperation.swift */; }; E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */; }; E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FA74431E769BF900C3489D /* RecordWithDatabase.swift */; }; E2FA74481E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FA74471E769D9400C3489D /* RecordIDWithDatabase.swift */; }; @@ -97,7 +97,7 @@ D985DE9C1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeLocalDatabaseOperation.swift; sourceTree = ""; }; D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateCloudCoreZoneOperation.swift; sourceTree = ""; }; D985DEA71FE0292000236870 /* SubscribeOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeOperation.swift; sourceTree = ""; }; - D985DEAA1FE0335800236870 /* UploadAllLocalDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadAllLocalDataOperation.swift; sourceTree = ""; }; + D985DEAA1FE0335800236870 /* PushAllLocalDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushAllLocalDataOperation.swift; sourceTree = ""; }; D985DEAD1FE034A900236870 /* NSManagedObjectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectModel.swift; sourceTree = ""; }; D9B3C6F31FCEF38D00CDB7FF /* TestableApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestableApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; D9B3C6F51FCEF38D00CDB7FF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -125,7 +125,7 @@ E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitAttribute.swift; sourceTree = ""; }; E22C40441E4291FB009469A1 /* CloudCore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CloudCore.podspec; sourceTree = ""; }; E22C40451E42956C009469A1 /* CoreDataListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataListener.swift; sourceTree = ""; }; - E23C478B1E48A404004310F9 /* CloudSaveOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudSaveOperationQueue.swift; sourceTree = ""; }; + E23C478B1E48A404004310F9 /* PushOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushOperationQueue.swift; sourceTree = ""; }; E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxyTests.swift; sourceTree = ""; }; E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteFromCoreDataOperationTests.swift; sourceTree = ""; }; E247EF981E678EA200EBD75E /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctions.swift; sourceTree = ""; }; @@ -155,7 +155,7 @@ E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchResult.swift; sourceTree = ""; }; E2D390071E4A49350019BBCD /* NSEntityDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEntityDescription.swift; sourceTree = ""; }; E2E296C91E49DA0800E7D6ED /* Tokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokens.swift; sourceTree = ""; }; - E2E4D83D1E76D4EF00550CBE /* FetchAndSaveOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchAndSaveOperation.swift; sourceTree = ""; }; + E2E4D83D1E76D4EF00550CBE /* PullOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullOperation.swift; sourceTree = ""; }; E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAttributeName.swift; sourceTree = ""; }; E2FA74431E769BF900C3489D /* RecordWithDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWithDatabase.swift; sourceTree = ""; }; E2FA74471E769D9400C3489D /* RecordIDWithDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordIDWithDatabase.swift; sourceTree = ""; }; @@ -244,15 +244,15 @@ path = CloudCoreTests; sourceTree = ""; }; - D9089D481FE14E4A000FC60C /* Setup Operation */ = { + D9089D481FE14E4A000FC60C /* Setup */ = { isa = PBXGroup; children = ( D9089D491FE14E57000FC60C /* SetupOperation.swift */, D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */, - D985DEAA1FE0335800236870 /* UploadAllLocalDataOperation.swift */, + D985DEAA1FE0335800236870 /* PushAllLocalDataOperation.swift */, D985DEA71FE0292000236870 /* SubscribeOperation.swift */, ); - path = "Setup Operation"; + path = Setup; sourceTree = ""; }; D9B3C6F41FCEF38D00CDB7FF /* App */ = { @@ -311,35 +311,35 @@ E2075FF11E4BB6EF00E31F1F /* Classes */ = { isa = PBXGroup; children = ( - D9089D481FE14E4A000FC60C /* Setup Operation */, E2075FF81E4BBEAC00E31F1F /* AsynchronousOperation.swift */, - E2075FF31E4BB70D00E31F1F /* Fetch */, - E2075FF21E4BB6F700E31F1F /* Save */, + D9089D481FE14E4A000FC60C /* Setup */, + E2075FF31E4BB70D00E31F1F /* Pull */, + E2075FF21E4BB6F700E31F1F /* Push */, E200D44C1E48E13200B707D4 /* CloudCore.swift */, E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */, ); path = Classes; sourceTree = ""; }; - E2075FF21E4BB6F700E31F1F /* Save */ = { + E2075FF21E4BB6F700E31F1F /* Push */ = { isa = PBXGroup; children = ( E2FA74461E769D8700C3489D /* Model */, E288C5751E4C9519002360A1 /* ObjectToRecord */, - E23C478B1E48A404004310F9 /* CloudSaveOperationQueue.swift */, + E23C478B1E48A404004310F9 /* PushOperationQueue.swift */, E22C40451E42956C009469A1 /* CoreDataListener.swift */, ); - path = Save; + path = Push; sourceTree = ""; }; - E2075FF31E4BB70D00E31F1F /* Fetch */ = { + E2075FF31E4BB70D00E31F1F /* Pull */ = { isa = PBXGroup; children = ( E277DB0F1E77FC9F00DC334A /* PublicSubscriptions */, E2C02A171E4CDEDA001B2871 /* SubOperations */, - E2E4D83D1E76D4EF00550CBE /* FetchAndSaveOperation.swift */, + E2E4D83D1E76D4EF00550CBE /* PullOperation.swift */, ); - path = Fetch; + path = Pull; sourceTree = ""; }; E23C47871E487CEA004310F9 /* Model */ = { @@ -691,14 +691,14 @@ files = ( E21FA03E1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift in Sources */, D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */, - E2E4D8411E76D5A600550CBE /* FetchAndSaveOperation.swift in Sources */, + E2E4D8411E76D5A600550CBE /* PullOperation.swift in Sources */, E2C02A141E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift in Sources */, E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */, E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */, E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */, E29BB21E1E433E050020F5B6 /* CKRecordID.swift in Sources */, D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */, - E23C478C1E48A404004310F9 /* CloudSaveOperationQueue.swift in Sources */, + E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */, E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */, E2C3A6D11E4A8EAF009151F3 /* FetchResult.swift in Sources */, E22C40461E42956C009469A1 /* CoreDataListener.swift in Sources */, @@ -711,7 +711,7 @@ E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */, E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */, D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */, - D985DEAB1FE0335800236870 /* UploadAllLocalDataOperation.swift in Sources */, + D985DEAB1FE0335800236870 /* PushAllLocalDataOperation.swift in Sources */, E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */, D985DE9D1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift in Sources */, E2FA74481E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */, diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 84421763..0576d91a 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -91,7 +91,7 @@ open class CloudCore { #endif // Fetch updated data (e.g. push notifications weren't received) - let updateFromCloudOperation = FetchAndSaveOperation(persistentContainer: container) + let updateFromCloudOperation = PullOperation(persistentContainer: container) updateFromCloudOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.fetchFromCloud)) } @@ -133,7 +133,7 @@ open class CloudCore { DispatchQueue.global(qos: .utility).async { let errorProxy = ErrorBlockProxy(destination: error) - let operation = FetchAndSaveOperation(from: [cloudDatabase], persistentContainer: container) + let operation = PullOperation(from: [cloudDatabase], persistentContainer: container) operation.errorBlock = { errorProxy.send(error: $0) } operation.start() @@ -153,7 +153,7 @@ open class CloudCore { - completion: `FetchResult` enumeration with results of operation */ public static func fetchAndSave(to container: NSPersistentContainer, error: ErrorBlock?, completion: (() -> Void)?) { - let operation = FetchAndSaveOperation(persistentContainer: container) + let operation = PullOperation(persistentContainer: container) operation.errorBlock = error operation.completionBlock = completion diff --git a/Source/Classes/Fetch/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift similarity index 100% rename from Source/Classes/Fetch/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift rename to Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift diff --git a/Source/Classes/Fetch/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift similarity index 100% rename from Source/Classes/Fetch/PublicSubscriptions/PublicDatabaseSubscriptions.swift rename to Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift diff --git a/Source/Classes/Fetch/FetchAndSaveOperation.swift b/Source/Classes/Pull/PullOperation.swift similarity index 95% rename from Source/Classes/Fetch/FetchAndSaveOperation.swift rename to Source/Classes/Pull/PullOperation.swift index 50995fc2..3e86be28 100644 --- a/Source/Classes/Fetch/FetchAndSaveOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -10,7 +10,7 @@ import CloudKit import CoreData /// An operation that fetches data from CloudKit and saves it to Core Data, you can use it without calling `CloudCore.fetchAndSave` methods if you application relies on `Operation` -public class FetchAndSaveOperation: Operation { +public class PullOperation: Operation { /// Private cloud database for the CKContainer specified by CloudCoreConfig public static let allDatabases = [ @@ -36,7 +36,7 @@ public class FetchAndSaveOperation: Operation { /// - databases: list of databases to fetch data from (only private is supported now) /// - persistentContainer: `NSPersistentContainer` that will be used to save data /// - tokens: previously saved `Tokens`, you can generate new ones if you want to fetch all data - public init(from databases: [CKDatabase] = FetchAndSaveOperation.allDatabases, persistentContainer: NSPersistentContainer, tokens: Tokens = CloudCore.tokens) { + public init(from databases: [CKDatabase] = PullOperation.allDatabases, persistentContainer: NSPersistentContainer, tokens: Tokens = CloudCore.tokens) { self.tokens = tokens self.databases = databases self.persistentContainer = persistentContainer diff --git a/Source/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift similarity index 100% rename from Source/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift rename to Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift diff --git a/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift similarity index 100% rename from Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift rename to Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift diff --git a/Source/Classes/Fetch/SubOperations/PurgeLocalDatabaseOperation.swift b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift similarity index 100% rename from Source/Classes/Fetch/SubOperations/PurgeLocalDatabaseOperation.swift rename to Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift diff --git a/Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift similarity index 100% rename from Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift rename to Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift diff --git a/Source/Classes/Save/CoreDataListener.swift b/Source/Classes/Push/CoreDataListener.swift similarity index 83% rename from Source/Classes/Save/CoreDataListener.swift rename to Source/Classes/Push/CoreDataListener.swift index 7410706f..d76fa8b9 100644 --- a/Source/Classes/Save/CoreDataListener.swift +++ b/Source/Classes/Push/CoreDataListener.swift @@ -15,7 +15,7 @@ class CoreDataListener { var container: NSPersistentContainer let converter = ObjectToRecordConverter() - let cloudSaveOperationQueue = CloudSaveOperationQueue() + let pushOperationQueue = PushOperationQueue() let cloudContextName = "CloudCoreSync" @@ -73,9 +73,9 @@ class CoreDataListener { backgroundContext.name = listener.cloudContextName let records = listener.converter.confirmConvertOperationsAndWait(in: backgroundContext) - listener.cloudSaveOperationQueue.errorBlock = { listener.handle(error: $0, parentContext: backgroundContext) } - listener.cloudSaveOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) - listener.cloudSaveOperationQueue.waitUntilAllOperationsAreFinished() + listener.pushOperationQueue.errorBlock = { listener.handle(error: $0, parentContext: backgroundContext) } + listener.pushOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) + listener.pushOperationQueue.waitUntilAllOperationsAreFinished() do { if backgroundContext.hasChanges { @@ -98,13 +98,13 @@ class CoreDataListener { switch cloudError.code { // Zone was accidentally deleted (NOT PURGED), we need to reupload all data accroding Apple Guidelines case .zoneNotFound: - cloudSaveOperationQueue.cancelAllOperations() + pushOperationQueue.cancelAllOperations() // Create CloudCore Zone let createZoneOperation = CreateCloudCoreZoneOperation() createZoneOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) - self.cloudSaveOperationQueue.cancelAllOperations() + self.pushOperationQueue.cancelAllOperations() } // Subscribe operation @@ -112,14 +112,14 @@ class CoreDataListener { let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) } subscribeOperation.addDependency(createZoneOperation) - cloudSaveOperationQueue.addOperation(subscribeOperation) + pushOperationQueue.addOperation(subscribeOperation) #endif // Upload all local data - let uploadOperation = UploadAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) + let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) } - cloudSaveOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) + pushOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) case .operationCancelled: return default: delegate?.error(error: cloudError, module: .some(.saveToCloud)) } diff --git a/Source/Classes/Save/Model/RecordIDWithDatabase.swift b/Source/Classes/Push/Model/RecordIDWithDatabase.swift similarity index 100% rename from Source/Classes/Save/Model/RecordIDWithDatabase.swift rename to Source/Classes/Push/Model/RecordIDWithDatabase.swift diff --git a/Source/Classes/Save/Model/RecordWithDatabase.swift b/Source/Classes/Push/Model/RecordWithDatabase.swift similarity index 100% rename from Source/Classes/Save/Model/RecordWithDatabase.swift rename to Source/Classes/Push/Model/RecordWithDatabase.swift diff --git a/Source/Classes/Save/ObjectToRecord/CoreDataAttribute.swift b/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift similarity index 100% rename from Source/Classes/Save/ObjectToRecord/CoreDataAttribute.swift rename to Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift diff --git a/Source/Classes/Save/ObjectToRecord/CoreDataRelationship.swift b/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift similarity index 100% rename from Source/Classes/Save/ObjectToRecord/CoreDataRelationship.swift rename to Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift diff --git a/Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift similarity index 100% rename from Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift rename to Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift diff --git a/Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift similarity index 100% rename from Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift rename to Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift diff --git a/Source/Classes/Save/CloudSaveOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift similarity index 98% rename from Source/Classes/Save/CloudSaveOperationQueue.swift rename to Source/Classes/Push/PushOperationQueue.swift index 583cde4d..be70acbe 100644 --- a/Source/Classes/Save/CloudSaveOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -9,7 +9,7 @@ import CloudKit import CoreData -class CloudSaveOperationQueue: OperationQueue { +class PushOperationQueue: OperationQueue { var errorBlock: ErrorBlock? /// Modify CloudKit database, operations will be created and added to operation queue. diff --git a/Source/Classes/Setup Operation/CreateCloudCoreZoneOperation.swift b/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift similarity index 100% rename from Source/Classes/Setup Operation/CreateCloudCoreZoneOperation.swift rename to Source/Classes/Setup/CreateCloudCoreZoneOperation.swift diff --git a/Source/Classes/Setup Operation/UploadAllLocalDataOperation.swift b/Source/Classes/Setup/PushAllLocalDataOperation.swift similarity index 81% rename from Source/Classes/Setup Operation/UploadAllLocalDataOperation.swift rename to Source/Classes/Setup/PushAllLocalDataOperation.swift index fe45d28b..40c52dd7 100644 --- a/Source/Classes/Setup Operation/UploadAllLocalDataOperation.swift +++ b/Source/Classes/Setup/PushAllLocalDataOperation.swift @@ -9,7 +9,7 @@ import Foundation import CoreData -class UploadAllLocalDataOperation: Operation { +class PushAllLocalDataOperation: Operation { let managedObjectModel: NSManagedObjectModel let parentContext: NSManagedObjectContext @@ -17,12 +17,12 @@ class UploadAllLocalDataOperation: Operation { var errorBlock: ErrorBlock? { didSet { converter.errorBlock = errorBlock - cloudSaveOperationQueue.errorBlock = errorBlock + pushOperationQueue.errorBlock = errorBlock } } private let converter = ObjectToRecordConverter() - private let cloudSaveOperationQueue = CloudSaveOperationQueue() + private let pushOperationQueue = PushOperationQueue() init(parentContext: NSManagedObjectContext, managedObjectModel: NSManagedObjectModel) { self.parentContext = parentContext @@ -58,8 +58,8 @@ class UploadAllLocalDataOperation: Operation { converter.setUnconfirmedOperations(inserted: allManagedObjects, updated: Set(), deleted: Set()) let recordsToSave = converter.confirmConvertOperationsAndWait(in: childContext).recordsToSave - cloudSaveOperationQueue.addOperations(recordsToSave: recordsToSave, recordIDsToDelete: [RecordIDWithDatabase]()) - cloudSaveOperationQueue.waitUntilAllOperationsAreFinished() + pushOperationQueue.addOperations(recordsToSave: recordsToSave, recordIDsToDelete: [RecordIDWithDatabase]()) + pushOperationQueue.waitUntilAllOperationsAreFinished() do { try childContext.save() @@ -69,7 +69,7 @@ class UploadAllLocalDataOperation: Operation { } override func cancel() { - cloudSaveOperationQueue.cancelAllOperations() + pushOperationQueue.cancelAllOperations() super.cancel() } diff --git a/Source/Classes/Setup Operation/SetupOperation.swift b/Source/Classes/Setup/SetupOperation.swift similarity index 94% rename from Source/Classes/Setup Operation/SetupOperation.swift rename to Source/Classes/Setup/SetupOperation.swift index ceaddc8b..03215ceb 100644 --- a/Source/Classes/Setup Operation/SetupOperation.swift +++ b/Source/Classes/Setup/SetupOperation.swift @@ -60,7 +60,7 @@ class SetupOperation: Operation { #endif // Upload all local data - let uploadOperation = UploadAllLocalDataOperation(parentContext: childContext, managedObjectModel: container.managedObjectModel) + let uploadOperation = PushAllLocalDataOperation(parentContext: childContext, managedObjectModel: container.managedObjectModel) uploadOperation.errorBlock = errorBlock #if !os(watchOS) diff --git a/Source/Classes/Setup Operation/SubscribeOperation.swift b/Source/Classes/Setup/SubscribeOperation.swift similarity index 100% rename from Source/Classes/Setup Operation/SubscribeOperation.swift rename to Source/Classes/Setup/SubscribeOperation.swift From 47f0d09592d3952082822f28afd05c84d5d7d812 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 17 May 2018 16:06:32 -0700 Subject: [PATCH 002/203] ignore .DS_Store files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f1933103..67cbb590 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output +.DS_Store From 53c2f62ddacc8bd6f0cf64e9d1e17690d439068a Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 17 May 2018 16:55:54 -0700 Subject: [PATCH 003/203] more nomenclature changes --- CloudCore.xcodeproj/project.pbxproj | 8 ++++---- Source/Classes/CloudCore.swift | 16 ++++++++-------- ...DataListener.swift => CoreDataObserver.swift} | 14 +++++++------- .../ObjectToRecord/ObjectToRecordConverter.swift | 14 +++++++------- .../Setup/PushAllLocalDataOperation.swift | 4 ++-- 5 files changed, 28 insertions(+), 28 deletions(-) rename Source/Classes/Push/{CoreDataListener.swift => CoreDataObserver.swift} (92%) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index b1a1b3b3..757c8896 100755 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -37,7 +37,7 @@ E20A73CC1E68608100A6851A /* RecordToCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20A73CB1E68608100A6851A /* RecordToCoreDataOperationTests.swift */; }; E21FA03E1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21FA03D1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift */; }; E22A53DA1E4A8743009286C0 /* CloudKitAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */; }; - E22C40461E42956C009469A1 /* CoreDataListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22C40451E42956C009469A1 /* CoreDataListener.swift */; }; + E22C40461E42956C009469A1 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22C40451E42956C009469A1 /* CoreDataObserver.swift */; }; E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478B1E48A404004310F9 /* PushOperationQueue.swift */; }; E247EF8D1E67775500EBD75E /* ErrorBlockProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */; }; E247EF971E67873E00EBD75E /* DeleteFromCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */; }; @@ -124,7 +124,7 @@ E21FA03D1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordToCoreDataOperation.swift; sourceTree = ""; }; E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitAttribute.swift; sourceTree = ""; }; E22C40441E4291FB009469A1 /* CloudCore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CloudCore.podspec; sourceTree = ""; }; - E22C40451E42956C009469A1 /* CoreDataListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataListener.swift; sourceTree = ""; }; + E22C40451E42956C009469A1 /* CoreDataObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = ""; }; E23C478B1E48A404004310F9 /* PushOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushOperationQueue.swift; sourceTree = ""; }; E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxyTests.swift; sourceTree = ""; }; E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteFromCoreDataOperationTests.swift; sourceTree = ""; }; @@ -327,7 +327,7 @@ E2FA74461E769D8700C3489D /* Model */, E288C5751E4C9519002360A1 /* ObjectToRecord */, E23C478B1E48A404004310F9 /* PushOperationQueue.swift */, - E22C40451E42956C009469A1 /* CoreDataListener.swift */, + E22C40451E42956C009469A1 /* CoreDataObserver.swift */, ); path = Push; sourceTree = ""; @@ -701,7 +701,7 @@ E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */, E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */, E2C3A6D11E4A8EAF009151F3 /* FetchResult.swift in Sources */, - E22C40461E42956C009469A1 /* CoreDataListener.swift in Sources */, + E22C40461E42956C009469A1 /* CoreDataObserver.swift in Sources */, E2075FF91E4BBEAC00E31F1F /* AsynchronousOperation.swift in Sources */, E24F44A61E4595B900F78819 /* CoreDataRelationship.swift in Sources */, D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */, diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 0576d91a..fcd85da5 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -51,7 +51,7 @@ open class CloudCore { // MARK: - Properties - private(set) static var coreDataListener: CoreDataListener? + private(set) static var coreDataObserver: CoreDataObserver? /// CloudCore configuration, it's recommended to set up before calling any of CloudCore methods. You can read more at `CloudCoreConfig` struct description public static var config = CloudCoreConfig() @@ -62,7 +62,7 @@ open class CloudCore { /// Error and sync actions are reported to that delegate public static weak var delegate: CloudCoreDelegate? { didSet { - coreDataListener?.delegate = delegate + coreDataObserver?.delegate = delegate } } @@ -78,10 +78,10 @@ open class CloudCore { /// - container: `NSPersistentContainer` that will be used to save data public static func enable(persistentContainer container: NSPersistentContainer) { // Listen for local changes - let listener = CoreDataListener(container: container) - listener.delegate = self.delegate - listener.observe() - self.coreDataListener = listener + let observer = CoreDataObserver(container: container) + observer.delegate = self.delegate + observer.start() + self.coreDataObserver = observer // Subscribe (subscription may be outdated/removed) #if !os(watchOS) @@ -107,8 +107,8 @@ open class CloudCore { public static func disable() { queue.cancelAllOperations() - coreDataListener?.stopObserving() - coreDataListener = nil + coreDataObserver?.stop() + coreDataObserver = nil // FIXME: unsubscribe } diff --git a/Source/Classes/Push/CoreDataListener.swift b/Source/Classes/Push/CoreDataObserver.swift similarity index 92% rename from Source/Classes/Push/CoreDataListener.swift rename to Source/Classes/Push/CoreDataObserver.swift index d76fa8b9..7cf5b36a 100644 --- a/Source/Classes/Push/CoreDataListener.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -11,7 +11,7 @@ import CoreData import CloudKit /// Class responsible for taking action on Core Data save notifications -class CoreDataListener { +class CoreDataObserver { var container: NSPersistentContainer let converter = ObjectToRecordConverter() @@ -30,18 +30,18 @@ class CoreDataListener { } /// Observe Core Data willSave and didSave notifications - func observe() { + func start() { NotificationCenter.default.addObserver(self, selector: #selector(self.willSave(notification:)), name: .NSManagedObjectContextWillSave, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.didSave(notification:)), name: .NSManagedObjectContextDidSave, object: nil) } /// Remove Core Data observers - func stopObserving() { + func stop() { NotificationCenter.default.removeObserver(self) } deinit { - stopObserving() + stop() } @objc private func willSave(notification: Notification) { @@ -53,7 +53,7 @@ class CoreDataListener { // Upload only for changes in root context that will be saved to persistentStore if context.parent != nil { return } - converter.setUnconfirmedOperations(inserted: context.insertedObjects, + converter.prepareOperationsFor(inserted: context.insertedObjects, updated: context.updatedObjects, deleted: context.deletedObjects) } @@ -63,7 +63,7 @@ class CoreDataListener { if context.name == CloudCore.config.contextName { return } if context.parent != nil { return } - if converter.notConfirmedConvertOperations.isEmpty && converter.recordIDsToDelete.isEmpty { return } + if converter.pendingConvertOperations.isEmpty && converter.recordIDsToDelete.isEmpty { return } DispatchQueue.global(qos: .utility).async { [weak self] in guard let listener = self else { return } @@ -72,7 +72,7 @@ class CoreDataListener { let backgroundContext = listener.container.newBackgroundContext() backgroundContext.name = listener.cloudContextName - let records = listener.converter.confirmConvertOperationsAndWait(in: backgroundContext) + let records = listener.converter.processPendingOperations(in: backgroundContext) listener.pushOperationQueue.errorBlock = { listener.handle(error: $0, parentContext: backgroundContext) } listener.pushOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) listener.pushOperationQueue.waitUntilAllOperationsAreFinished() diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index aa3d9bf8..e4be266e 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -16,15 +16,15 @@ class ObjectToRecordConverter { var errorBlock: ErrorBlock? - private(set) var notConfirmedConvertOperations = [ObjectToRecordOperation]() + private(set) var pendingConvertOperations = [ObjectToRecordOperation]() private let operationQueue = OperationQueue() private var convertedRecords = [RecordWithDatabase]() private(set) var recordIDsToDelete = [RecordIDWithDatabase]() - func setUnconfirmedOperations(inserted: Set, updated: Set, deleted: Set) { - self.notConfirmedConvertOperations = self.convertOperations(from: inserted, changeType: .inserted) - self.notConfirmedConvertOperations += self.convertOperations(from: updated, changeType: .updated) + func prepareOperationsFor(inserted: Set, updated: Set, deleted: Set) { + self.pendingConvertOperations = self.convertOperations(from: inserted, changeType: .inserted) + self.pendingConvertOperations += self.convertOperations(from: updated, changeType: .updated) self.recordIDsToDelete = convert(deleted: deleted) } @@ -100,13 +100,13 @@ class ObjectToRecordConverter { /// Add all uncofirmed operations to operation queue /// - attention: Don't call this method from same context's `perfom`, that will cause deadlock - func confirmConvertOperationsAndWait(in context: NSManagedObjectContext) -> (recordsToSave: [RecordWithDatabase], recordIDsToDelete: [RecordIDWithDatabase]) { - for operation in notConfirmedConvertOperations { + func processPendingOperations(in context: NSManagedObjectContext) -> (recordsToSave: [RecordWithDatabase], recordIDsToDelete: [RecordIDWithDatabase]) { + for operation in pendingConvertOperations { operation.parentContext = context operationQueue.addOperation(operation) } - notConfirmedConvertOperations = [ObjectToRecordOperation]() + pendingConvertOperations = [ObjectToRecordOperation]() operationQueue.waitUntilAllOperationsAreFinished() let recordsToSave = self.convertedRecords diff --git a/Source/Classes/Setup/PushAllLocalDataOperation.swift b/Source/Classes/Setup/PushAllLocalDataOperation.swift index 40c52dd7..a755fa15 100644 --- a/Source/Classes/Setup/PushAllLocalDataOperation.swift +++ b/Source/Classes/Setup/PushAllLocalDataOperation.swift @@ -56,8 +56,8 @@ class PushAllLocalDataOperation: Operation { } } - converter.setUnconfirmedOperations(inserted: allManagedObjects, updated: Set(), deleted: Set()) - let recordsToSave = converter.confirmConvertOperationsAndWait(in: childContext).recordsToSave + converter.prepareOperationsFor(inserted: allManagedObjects, updated: Set(), deleted: Set()) + let recordsToSave = converter.processPendingOperations(in: childContext).recordsToSave pushOperationQueue.addOperations(recordsToSave: recordsToSave, recordIDsToDelete: [RecordIDWithDatabase]()) pushOperationQueue.waitUntilAllOperationsAreFinished() From c36d4bdb34807575330be3e8534a8ca765086bea Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 22 May 2018 16:24:07 -0700 Subject: [PATCH 004/203] still more nomenclature changes --- .../project.pbxproj | 48 ++++--------------- Source/Classes/Push/CoreDataObserver.swift | 43 +++++++++-------- .../ObjectToRecordConverter.swift | 8 +++- 3 files changed, 38 insertions(+), 61 deletions(-) diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index 8fc53242..fcccde60 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -46,13 +46,6 @@ remoteGlobalIDString = D5B2E89F1C3A780C00C0327D; remoteInfo = "CloudCore-iOS"; }; - E23BE6FC1EA4CC1C008F4F23 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = D5C629401C3A7FAA007F7B7C; - remoteInfo = "CloudCore-Mac"; - }; E23BE6FE1EA4CC1C008F4F23 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; @@ -60,13 +53,6 @@ remoteGlobalIDString = E29BB2281E436F310020F5B6; remoteInfo = "CloudCoreTests-iOS"; }; - E23BE7001EA4CC1C008F4F23 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = E29D11821E69B30C00E3DCBF; - remoteInfo = "CloudCoreTests-macOS"; - }; E23BE70E1EA4FD78008F4F23 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; @@ -166,9 +152,7 @@ isa = PBXGroup; children = ( E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */, - E23BE6FD1EA4CC1C008F4F23 /* CloudCore.framework */, - E23BE6FF1EA4CC1C008F4F23 /* CloudCoreTests-iOS.xctest */, - E23BE7011EA4CC1C008F4F23 /* CloudCoreTests-macOS.xctest */, + E23BE6FF1EA4CC1C008F4F23 /* CloudCoreTests.xctest */, D9DC6DC11FDFEFF100017652 /* TestableApp.app */, D9DC6DC31FDFEFF100017652 /* CloudKitTests.xctest */, ); @@ -265,7 +249,7 @@ TargetAttributes = { E2C3E34F1E53299800A733BF = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = 7X2PJ6H6YM; + DevelopmentTeam = 26Y8AQV29F; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -328,27 +312,13 @@ remoteRef = E23BE6FA1EA4CC1C008F4F23 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - E23BE6FD1EA4CC1C008F4F23 /* CloudCore.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CloudCore.framework; - remoteRef = E23BE6FC1EA4CC1C008F4F23 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - E23BE6FF1EA4CC1C008F4F23 /* CloudCoreTests-iOS.xctest */ = { + E23BE6FF1EA4CC1C008F4F23 /* CloudCoreTests.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; - path = "CloudCoreTests-iOS.xctest"; + path = CloudCoreTests.xctest; remoteRef = E23BE6FE1EA4CC1C008F4F23 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - E23BE7011EA4CC1C008F4F23 /* CloudCoreTests-macOS.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = "CloudCoreTests-macOS.xctest"; - remoteRef = E23BE7001EA4CC1C008F4F23 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -395,7 +365,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; D0F793BBB16236A031D0746F /* [CP] Embed Pods Frameworks */ = { @@ -576,11 +546,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; - DEVELOPMENT_TEAM = 7X2PJ6H6YM; + DEVELOPMENT_TEAM = 26Y8AQV29F; INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = changeMe.change2; + PRODUCT_BUNDLE_IDENTIFIER = com.deeje.example.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; @@ -594,11 +564,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; - DEVELOPMENT_TEAM = 7X2PJ6H6YM; + DEVELOPMENT_TEAM = 26Y8AQV29F; INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = changeMe.change2; + PRODUCT_BUNDLE_IDENTIFIER = com.deeje.example.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 7cf5b36a..6442f03a 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -44,15 +44,20 @@ class CoreDataObserver { stop() } + func shouldProcess(_ context: NSManagedObjectContext) -> Bool { + // Ignore saves that are generated by FetchAndSaveController + if context.name == CloudCore.config.contextName { return false } + + // Upload only for changes in root context that will be saved to persistentStore + if context.parent != nil { return false } + + return true + } + @objc private func willSave(notification: Notification) { guard let context = notification.object as? NSManagedObjectContext else { return } - - // Ignore saves that are generated by FetchAndSaveController - if context.name == CloudCore.config.contextName { return } - - // Upload only for changes in root context that will be saved to persistentStore - if context.parent != nil { return } - + guard shouldProcess(context) else { return } + converter.prepareOperationsFor(inserted: context.insertedObjects, updated: context.updatedObjects, deleted: context.deletedObjects) @@ -60,29 +65,27 @@ class CoreDataObserver { @objc private func didSave(notification: Notification) { guard let context = notification.object as? NSManagedObjectContext else { return } - if context.name == CloudCore.config.contextName { return } - if context.parent != nil { return } - - if converter.pendingConvertOperations.isEmpty && converter.recordIDsToDelete.isEmpty { return } - + guard shouldProcess(context) else { return } + guard converter.hasPendingOperations else { return } + DispatchQueue.global(qos: .utility).async { [weak self] in - guard let listener = self else { return } + guard let observer = self else { return } CloudCore.delegate?.willSyncToCloud() - let backgroundContext = listener.container.newBackgroundContext() - backgroundContext.name = listener.cloudContextName + let backgroundContext = observer.container.newBackgroundContext() + backgroundContext.name = observer.cloudContextName - let records = listener.converter.processPendingOperations(in: backgroundContext) - listener.pushOperationQueue.errorBlock = { listener.handle(error: $0, parentContext: backgroundContext) } - listener.pushOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) - listener.pushOperationQueue.waitUntilAllOperationsAreFinished() + let records = observer.converter.processPendingOperations(in: backgroundContext) + observer.pushOperationQueue.errorBlock = { observer.handle(error: $0, parentContext: backgroundContext) } + observer.pushOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) + observer.pushOperationQueue.waitUntilAllOperationsAreFinished() do { if backgroundContext.hasChanges { try backgroundContext.save() } } catch { - listener.delegate?.error(error: error, module: .some(.saveToCloud)) + observer.delegate?.error(error: error, module: .some(.saveToCloud)) } CloudCore.delegate?.didSyncToCloud() diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index e4be266e..ccba9b48 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -16,12 +16,16 @@ class ObjectToRecordConverter { var errorBlock: ErrorBlock? - private(set) var pendingConvertOperations = [ObjectToRecordOperation]() + private var pendingConvertOperations = [ObjectToRecordOperation]() private let operationQueue = OperationQueue() private var convertedRecords = [RecordWithDatabase]() - private(set) var recordIDsToDelete = [RecordIDWithDatabase]() + private var recordIDsToDelete = [RecordIDWithDatabase]() + var hasPendingOperations: Bool { + return !pendingConvertOperations.isEmpty || !recordIDsToDelete.isEmpty + } + func prepareOperationsFor(inserted: Set, updated: Set, deleted: Set) { self.pendingConvertOperations = self.convertOperations(from: inserted, changeType: .inserted) self.pendingConvertOperations += self.convertOperations(from: updated, changeType: .updated) From 75a1b3e429461712e87af0da4e66138ca40b6426 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 22 May 2018 16:31:14 -0700 Subject: [PATCH 005/203] =?UTF-8?q?don=E2=80=99t=20need=20to=20use=20self.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectToRecord/ObjectToRecordConverter.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index ccba9b48..21a1686d 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -27,10 +27,10 @@ class ObjectToRecordConverter { } func prepareOperationsFor(inserted: Set, updated: Set, deleted: Set) { - self.pendingConvertOperations = self.convertOperations(from: inserted, changeType: .inserted) - self.pendingConvertOperations += self.convertOperations(from: updated, changeType: .updated) + pendingConvertOperations = convertOperations(from: inserted, changeType: .inserted) + pendingConvertOperations += convertOperations(from: updated, changeType: .updated) - self.recordIDsToDelete = convert(deleted: deleted) + recordIDsToDelete = convert(deleted: deleted) } private func convertOperations(from objectSet: Set, changeType: ManagedObjectChangeType) -> [ObjectToRecordOperation] { @@ -102,17 +102,18 @@ class ObjectToRecordConverter { return recordIDs } - /// Add all uncofirmed operations to operation queue + /// Add all unconfirmed operations to operation queue /// - attention: Don't call this method from same context's `perfom`, that will cause deadlock func processPendingOperations(in context: NSManagedObjectContext) -> (recordsToSave: [RecordWithDatabase], recordIDsToDelete: [RecordIDWithDatabase]) { for operation in pendingConvertOperations { operation.parentContext = context operationQueue.addOperation(operation) } - + pendingConvertOperations = [ObjectToRecordOperation]() + operationQueue.waitUntilAllOperationsAreFinished() - + let recordsToSave = self.convertedRecords let recordIDsToDelete = self.recordIDsToDelete From 4142fc3dfd62e8893ebe9d2fde825b56f7e4a956 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 23 May 2018 16:01:21 -0700 Subject: [PATCH 006/203] enable persistent history in Core Data --- Example/Sources/AppDelegate.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 7251cea8..37358e04 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -64,6 +64,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele error conditions that could cause the creation of the store to fail. */ let container = NSPersistentContainer(name: "Model") + + if #available(iOS 11.0, *) { + let storeDescription = container.persistentStoreDescriptions.first + storeDescription?.setOption(true as NSNumber, forKey:NSPersistentHistoryTrackingKey) + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { // Replace this implementation with code to handle the error appropriately. From ae8fdb22539f53458cbd3c461ed15dd21cbcc798 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 23 May 2018 16:01:45 -0700 Subject: [PATCH 007/203] move the push code outside of the didSave handler --- Source/Classes/Push/CoreDataObserver.swift | 56 +++++++++++++--------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 6442f03a..c50fddc2 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -10,7 +10,7 @@ import Foundation import CoreData import CloudKit -/// Class responsible for taking action on Core Data save notifications +/// Class responsible for taking action on Core Data changes class CoreDataObserver { var container: NSPersistentContainer @@ -31,8 +31,13 @@ class CoreDataObserver { /// Observe Core Data willSave and didSave notifications func start() { - NotificationCenter.default.addObserver(self, selector: #selector(self.willSave(notification:)), name: .NSManagedObjectContextWillSave, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didSave(notification:)), name: .NSManagedObjectContextDidSave, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(self.willSave(notification:)), name: .NSManagedObjectContextWillSave, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(self.didSave(notification:)), + name: .NSManagedObjectContextDidSave, + object: nil) } /// Remove Core Data observers @@ -54,6 +59,30 @@ class CoreDataObserver { return true } + func processChanges() { + CloudCore.delegate?.willSyncToCloud() + + let backgroundContext = container.newBackgroundContext() + backgroundContext.name = cloudContextName + + let records = converter.processPendingOperations(in: backgroundContext) + pushOperationQueue.errorBlock = { + self.handle(error: $0, parentContext: backgroundContext) + } + pushOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) + pushOperationQueue.waitUntilAllOperationsAreFinished() + + do { + if backgroundContext.hasChanges { + try backgroundContext.save() + } + } catch { + delegate?.error(error: error, module: .some(.saveToCloud)) + } + + CloudCore.delegate?.didSyncToCloud() + } + @objc private func willSave(notification: Notification) { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } @@ -66,29 +95,12 @@ class CoreDataObserver { @objc private func didSave(notification: Notification) { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } + guard converter.hasPendingOperations else { return } DispatchQueue.global(qos: .utility).async { [weak self] in guard let observer = self else { return } - CloudCore.delegate?.willSyncToCloud() - - let backgroundContext = observer.container.newBackgroundContext() - backgroundContext.name = observer.cloudContextName - - let records = observer.converter.processPendingOperations(in: backgroundContext) - observer.pushOperationQueue.errorBlock = { observer.handle(error: $0, parentContext: backgroundContext) } - observer.pushOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) - observer.pushOperationQueue.waitUntilAllOperationsAreFinished() - - do { - if backgroundContext.hasChanges { - try backgroundContext.save() - } - } catch { - observer.delegate?.error(error: error, module: .some(.saveToCloud)) - } - - CloudCore.delegate?.didSyncToCloud() + observer.processChanges() } } From 51913b25a6fe6a42948ca1d5c2a99c7ceef338d3 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 23 Sep 2018 16:09:56 -0700 Subject: [PATCH 008/203] update to Swift 4.2 --- Source/Classes/Pull/PullOperation.swift | 4 ++-- .../DeleteFromCoreDataOperation.swift | 4 ++-- .../FetchRecordZoneChangesOperation.swift | 16 ++++++++-------- .../Push/Model/RecordIDWithDatabase.swift | 4 ++-- .../ObjectToRecord/CoreDataRelationship.swift | 8 ++++---- .../ObjectToRecord/ObjectToRecordConverter.swift | 2 +- Source/Classes/Push/PushOperationQueue.swift | 4 ++-- Source/Classes/Setup/SubscribeOperation.swift | 2 +- Source/Extensions/CKRecordID.swift | 8 ++++---- Source/Model/CloudCoreConfig.swift | 2 +- Source/Model/CloudKitAttribute.swift | 6 +++--- Source/Model/Tokens.swift | 6 +++--- 12 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 3e86be28..2763ef3f 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -68,7 +68,7 @@ public class PullOperation: Operation { CloudCore.delegate?.didSyncFromCloud() } - private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZoneID], database: CKDatabase, context: NSManagedObjectContext) { + private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZone.ID], database: CKDatabase, context: NSManagedObjectContext) { if recordZoneIDs.isEmpty { return } let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) @@ -94,7 +94,7 @@ public class PullOperation: Operation { queue.addOperation(recordZoneChangesOperation) } - private func handle(recordZoneChangesError: Error, in zoneId: CKRecordZoneID, database: CKDatabase, context: NSManagedObjectContext) { + private func handle(recordZoneChangesError: Error, in zoneId: CKRecordZone.ID, database: CKDatabase, context: NSManagedObjectContext) { guard let cloudError = recordZoneChangesError as? CKError else { errorBlock?(recordZoneChangesError) return diff --git a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift index 9e2155ad..9c61d765 100644 --- a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift @@ -11,10 +11,10 @@ import CloudKit class DeleteFromCoreDataOperation: Operation { let parentContext: NSManagedObjectContext - let recordID: CKRecordID + let recordID: CKRecord.ID var errorBlock: ErrorBlock? - init(parentContext: NSManagedObjectContext, recordID: CKRecordID) { + init(parentContext: NSManagedObjectContext, recordID: CKRecord.ID) { self.parentContext = parentContext self.recordID = recordID diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index 67f0a407..4d97d152 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -11,25 +11,25 @@ import CloudKit class FetchRecordZoneChangesOperation: Operation { // Set on init let tokens: Tokens - let recordZoneIDs: [CKRecordZoneID] + let recordZoneIDs: [CKRecordZone.ID] let database: CKDatabase // - var errorBlock: ((CKRecordZoneID, Error) -> Void)? + var errorBlock: ((CKRecordZone.ID, Error) -> Void)? var recordChangedBlock: ((CKRecord) -> Void)? - var recordWithIDWasDeletedBlock: ((CKRecordID) -> Void)? + var recordWithIDWasDeletedBlock: ((CKRecord.ID) -> Void)? - private let optionsByRecordZoneID: [CKRecordZoneID: CKFetchRecordZoneChangesOptions] + private let optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions] private let fetchQueue = OperationQueue() - init(from database: CKDatabase, recordZoneIDs: [CKRecordZoneID], tokens: Tokens) { + init(from database: CKDatabase, recordZoneIDs: [CKRecordZone.ID], tokens: Tokens) { self.tokens = tokens self.database = database self.recordZoneIDs = recordZoneIDs - var optionsByRecordZoneID = [CKRecordZoneID: CKFetchRecordZoneChangesOptions]() + var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]() for zoneID in recordZoneIDs { - let options = CKFetchRecordZoneChangesOptions() + let options = CKFetchRecordZoneChangesOperation.ZoneOptions() options.previousServerChangeToken = self.tokens.tokensByRecordZoneID[zoneID] optionsByRecordZoneID[zoneID] = options } @@ -49,7 +49,7 @@ class FetchRecordZoneChangesOperation: Operation { fetchQueue.waitUntilAllOperationsAreFinished() } - private func makeFetchOperation(optionsByRecordZoneID: [CKRecordZoneID: CKFetchRecordZoneChangesOptions]) -> CKFetchRecordZoneChangesOperation { + private func makeFetchOperation(optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]) -> CKFetchRecordZoneChangesOperation { // Init Fetch Operation let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, optionsByRecordZoneID: optionsByRecordZoneID) diff --git a/Source/Classes/Push/Model/RecordIDWithDatabase.swift b/Source/Classes/Push/Model/RecordIDWithDatabase.swift index b802e464..e133a3ea 100644 --- a/Source/Classes/Push/Model/RecordIDWithDatabase.swift +++ b/Source/Classes/Push/Model/RecordIDWithDatabase.swift @@ -9,10 +9,10 @@ import CloudKit class RecordIDWithDatabase { - let recordID: CKRecordID + let recordID: CKRecord.ID let database: CKDatabase - init(_ recordID: CKRecordID, _ database: CKDatabase) { + init(_ recordID: CKRecord.ID, _ database: CKDatabase) { self.recordID = recordID self.database = database } diff --git a/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift b/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift index d2087cc4..069c2862 100644 --- a/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift +++ b/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift @@ -46,7 +46,7 @@ class CoreDataRelationship { guard let objectsSet = value as? NSSet else { return nil } - var referenceList = [CKReference]() + var referenceList = [CKRecord.Reference]() for (_, managedObject) in objectsSet.enumerated() { guard let managedObject = managedObject as? NSManagedObject, let reference = try makeReference(from: managedObject) else { continue } @@ -64,8 +64,8 @@ class CoreDataRelationship { } } - private func makeReference(from managedObject: NSManagedObject) throws -> CKReference? { - let action: CKReferenceAction + private func makeReference(from managedObject: NSManagedObject) throws -> CKRecord.Reference? { + let action: CKRecord_Reference_Action if case .some(NSDeleteRule.cascadeDeleteRule) = description.inverseRelationship?.deleteRule { action = .deleteSelf } else { @@ -79,7 +79,7 @@ class CoreDataRelationship { return nil } - return CKReference(record: record, action: action) + return CKRecord.Reference(record: record, action: action) } } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 21a1686d..94305338 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -124,7 +124,7 @@ class ObjectToRecordConverter { } /// Get appropriate database for modify operations - private func database(for recordID: CKRecordID, serviceAttributes: ServiceAttributeNames) -> CKDatabase { + private func database(for recordID: CKRecord.ID, serviceAttributes: ServiceAttributeNames) -> CKDatabase { let container = CloudCore.config.container if serviceAttributes.isPublic { return container.publicCloudDatabase } diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index be70acbe..fd2ad308 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -44,7 +44,7 @@ class PushOperationQueue: OperationQueue { } } - private func addOperation(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecordID], database: CKDatabase) { + private func addOperation(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], database: CKDatabase) { // Modify CKRecord Operation let modifyOperation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) modifyOperation.savePolicy = .changedKeys @@ -81,7 +81,7 @@ class PushOperationQueue: OperationQueue { fileprivate class DatabaseModifyDataSource { let database: CKDatabase var save = [CKRecord]() - var delete = [CKRecordID]() + var delete = [CKRecord.ID]() init(database: CKDatabase) { self.database = database diff --git a/Source/Classes/Setup/SubscribeOperation.swift b/Source/Classes/Setup/SubscribeOperation.swift index e7b01277..f87cef4c 100644 --- a/Source/Classes/Setup/SubscribeOperation.swift +++ b/Source/Classes/Setup/SubscribeOperation.swift @@ -43,7 +43,7 @@ class SubscribeOperation: AsynchronousOperation { } private func makeRecordZoneSubscriptionOperation(for database: CKDatabase, id: String) -> CKModifySubscriptionsOperation { - let notificationInfo = CKNotificationInfo() + let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.shouldSendContentAvailable = true let subscription = CKRecordZoneSubscription(zoneID: CloudCore.config.zoneID, subscriptionID: id) diff --git a/Source/Extensions/CKRecordID.swift b/Source/Extensions/CKRecordID.swift index da63c85a..0fbac032 100644 --- a/Source/Extensions/CKRecordID.swift +++ b/Source/Extensions/CKRecordID.swift @@ -8,17 +8,17 @@ import CloudKit -extension CKRecordID { +extension CKRecord.ID { private static let separator = "|" /// Init from encoded string /// /// - Parameter encodedString: format: `recordName|ownerName` convenience init?(encodedString: String) { - let separated = encodedString.components(separatedBy: CKRecordID.separator) + let separated = encodedString.components(separatedBy: CKRecord.ID.separator) if separated.count == 2 { - let zoneID = CKRecordZoneID(zoneName: CloudCore.config.zoneID.zoneName, ownerName: separated[1]) + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneID.zoneName, ownerName: separated[1]) self.init(recordName: separated[0], zoneID: zoneID) } else { return nil @@ -27,6 +27,6 @@ extension CKRecordID { /// Encoded string in format: `recordName|ownerName` var encodedString: String { - return recordName + CKRecordID.separator + zoneID.ownerName + return recordName + CKRecord.ID.separator + zoneID.ownerName } } diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 582fc48e..2dd943b3 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -37,7 +37,7 @@ public struct CloudCoreConfig { /// RecordZone inside private database to store CoreData. /// /// Default value is `CloudCore` - public var zoneID = CKRecordZoneID(zoneName: "CloudCore", ownerName: CKCurrentUserDefaultName) + public var zoneID = CKRecordZone.ID(zoneName: "CloudCore", ownerName: CKCurrentUserDefaultName) let subscriptionIDForPrivateDB = "CloudCorePrivate" let subscriptionIDForSharedDB = "CloudCoreShared" diff --git a/Source/Model/CloudKitAttribute.swift b/Source/Model/CloudKitAttribute.swift index 805b0dc1..da26c45b 100644 --- a/Source/Model/CloudKitAttribute.swift +++ b/Source/Model/CloudKitAttribute.swift @@ -30,8 +30,8 @@ class CloudKitAttribute { func makeCoreDataValue() throws -> Any? { switch value { - case let reference as CKReference: return try findManagedObject(for: reference.recordID) - case let references as [CKReference]: + case let reference as CKRecord.Reference: return try findManagedObject(for: reference.recordID) + case let references as [CKRecord.Reference]: let managedObjects = NSMutableSet() for ref in references { guard let foundObject = try findManagedObject(for: ref.recordID) else { continue } @@ -45,7 +45,7 @@ class CloudKitAttribute { } } - private func findManagedObject(for recordID: CKRecordID) throws -> NSManagedObject? { + private func findManagedObject(for recordID: CKRecord.ID) throws -> NSManagedObject? { let targetEntityName = try findTargetEntityName() let fetchRequest = NSFetchRequest(entityName: targetEntityName) diff --git a/Source/Model/Tokens.swift b/Source/Model/Tokens.swift index a91ab8a8..029cc574 100644 --- a/Source/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -29,7 +29,7 @@ import CloudKit */ open class Tokens: NSObject, NSCoding { - var tokensByRecordZoneID = [CKRecordZoneID: CKServerChangeToken]() + var tokensByRecordZoneID = [CKRecordZone.ID: CKServerChangeToken]() private struct ArchiverKey { static let tokensByRecordZoneID = "tokensByRecordZoneID" @@ -45,7 +45,7 @@ open class Tokens: NSObject, NSCoding { /// Load saved Tokens from UserDefaults. Key is used from `CloudCoreConfig.userDefaultsKeyTokens` /// /// - Returns: previously saved `Token` object, if tokens weren't saved before newly initialized `Tokens` object will be returned - open static func loadFromUserDefaults() -> Tokens { + public static func loadFromUserDefaults() -> Tokens { guard let tokensData = UserDefaults.standard.data(forKey: CloudCore.config.userDefaultsKeyTokens), let tokens = NSKeyedUnarchiver.unarchiveObject(with: tokensData) as? Tokens else { return Tokens() @@ -65,7 +65,7 @@ open class Tokens: NSObject, NSCoding { /// Returns an object initialized from data in a given unarchiver. public required init?(coder aDecoder: NSCoder) { - if let decodedTokens = aDecoder.decodeObject(forKey: ArchiverKey.tokensByRecordZoneID) as? [CKRecordZoneID: CKServerChangeToken] { + if let decodedTokens = aDecoder.decodeObject(forKey: ArchiverKey.tokensByRecordZoneID) as? [CKRecordZone.ID: CKServerChangeToken] { self.tokensByRecordZoneID = decodedTokens } else { return nil From 2c7ce03f942e3c671cb87d412bca6588d4e6f4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oleg=20Mu=CC=88ller?= Date: Tue, 3 Apr 2018 20:44:22 +0200 Subject: [PATCH 009/203] supporting transformable attributes --- .../RecordToCoreDataOperation.swift | 15 ++++++++++++++- .../ObjectToRecord/CoreDataAttribute.swift | 18 +++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index bbc99627..f537c65e 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -78,7 +78,20 @@ class RecordToCoreDataOperation: AsynchronousOperation { let attribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) let coreDataValue = try attribute.makeCoreDataValue() - object.setValue(coreDataValue, forKey: key) + + if let attribute = object.entity.attributesByName[key], attribute.attributeType == .transformableAttributeType, + let data = coreDataValue as? Data { + if let name = attribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { + let value = transformer.transformedValue(coreDataValue) + object.setValue(value, forKey: key) + } else if let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: data) { + object.setValue(unarchivedObject, forKey: key) + } else { + object.setValue(coreDataValue, forKey: key) + } + } else { + object.setValue(coreDataValue, forKey: key) + } } // Set system headers diff --git a/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift b/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift index cf7dd05f..2c7aa848 100644 --- a/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift +++ b/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift @@ -23,14 +23,22 @@ class CoreDataAttribute { // it is not an attribute return nil } - - self.description = description + + self.description = description if value is NSNull { self.value = nil - } else { - self.value = value - } + } else if let attribute = entity.attributesByName[attributeName], + attribute.attributeType == .transformableAttributeType { + if let transformerName = attribute.valueTransformerName, + let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: transformerName)) { + self.value = transformer.reverseTransformedValue(value) + } else { + self.value = NSKeyedArchiver.archivedData(withRootObject: value) + } + } else { + self.value = value + } self.name = attributeName } From c908d21332670dd1963203aceefa0ab1baa60518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oleg=20Mu=CC=88ller?= Date: Wed, 4 Apr 2018 22:53:51 +0200 Subject: [PATCH 010/203] adding fixes for fixing relationships # Conflicts: # Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift --- Source/Classes/Pull/PullOperation.swift | 45 +++++++++++++++++++ .../RecordToCoreDataOperation.swift | 6 +++ Source/Model/CloudKitAttribute.swift | 9 +++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 2763ef3f..f1d00640 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -73,10 +73,14 @@ public class PullOperation: Operation { let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) + var objectsWithMissingReferences = [MissingReferences]() recordZoneChangesOperation.recordChangedBlock = { // Convert and write CKRecord To NSManagedObject Operation let convertOperation = RecordToCoreDataOperation(parentContext: context, record: $0) convertOperation.errorBlock = { self.errorBlock?($0) } + convertOperation.completionBlock = { + objectsWithMissingReferences.append(convertOperation.missingObjectsPerEntities) + } self.queue.addOperation(convertOperation) } @@ -90,6 +94,47 @@ public class PullOperation: Operation { recordZoneChangesOperation.errorBlock = { zoneID, error in self.handle(recordZoneChangesError: error, in: zoneID, database: database, context: context) } + + recordZoneChangesOperation.completionBlock = { + // iterate over all missing references and fix them, now are all NSManagedObjects created + for missingReferences in objectsWithMissingReferences { + for (object, references) in missingReferences { + guard let serviceAttributes = object.entity.serviceAttributeNames else { continue } + + for (attributeName, recordIDs) in references { + for recordId in recordIDs { + guard let relationship = object.entity.relationshipsByName[attributeName], let targetEntityName = relationship.destinationEntity?.name else { continue } + + // TODO: move to extension + let fetchRequest = NSFetchRequest(entityName: targetEntityName) + fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordID + " == %@" , recordId) + fetchRequest.fetchLimit = 1 + fetchRequest.includesPropertyValues = false + + do { + let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject + + if let foundObject = foundObject { + if relationship.isToMany { + let set = object.value(forKey: attributeName) as? NSMutableSet ?? NSMutableSet() + set.add(foundObject) + object.setValue(set, forKey: attributeName) + } else { + object.setValue(foundObject, forKey: attributeName) + } + } else { + // TODO: show warning? + print("NOT FOUND " + recordId) + } + } catch { + // TODO: handle error + print("ERROR: \(error)") + } + } + } + } + } + } queue.addOperation(recordZoneChangesOperation) } diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index f537c65e..c9ec09a6 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -9,11 +9,16 @@ import CoreData import CloudKit +typealias AttributeName = String +typealias RecordID = String +typealias MissingReferences = [NSManagedObject: [AttributeName: [RecordID]]] + /// Convert CKRecord to NSManagedObject and save it to parent context, thread-safe class RecordToCoreDataOperation: AsynchronousOperation { let parentContext: NSManagedObjectContext let record: CKRecord var errorBlock: ErrorBlock? + var missingObjectsPerEntities = MissingReferences() /// - Parameters: /// - parentContext: operation will be safely performed in that context, **operation doesn't save that context** you need to do it manually @@ -91,6 +96,7 @@ class RecordToCoreDataOperation: AsynchronousOperation { } } else { object.setValue(coreDataValue, forKey: key) + missingObjectsPerEntities[object] = attribute.notFoundRecordIDsForAttribute } } diff --git a/Source/Model/CloudKitAttribute.swift b/Source/Model/CloudKitAttribute.swift index da26c45b..6688813d 100644 --- a/Source/Model/CloudKitAttribute.swift +++ b/Source/Model/CloudKitAttribute.swift @@ -19,6 +19,7 @@ class CloudKitAttribute { let entityName: String let serviceAttributes: ServiceAttributeNames let context: NSManagedObjectContext + var notFoundRecordIDsForAttribute = [AttributeName: [RecordID]]() init(value: Any?, fieldName: String, entityName: String, serviceAttributes: ServiceAttributeNames, context: NSManagedObjectContext) { self.value = value @@ -56,7 +57,13 @@ class CloudKitAttribute { fetchRequest.includesPropertyValues = false let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject - + + if foundObject == nil { + var values = notFoundRecordIDsForAttribute[fieldName] ?? [] + values.append(recordID.encodedString) + notFoundRecordIDsForAttribute[fieldName] = values + } + return foundObject } From c3a7dabe61bb44fd17f2c65045261613c5b6e711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oleg=20Mu=CC=88ller?= Date: Thu, 5 Apr 2018 19:12:49 +0200 Subject: [PATCH 011/203] error handling --- Source/Classes/Pull/PullOperation.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index f1d00640..07604f00 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -123,12 +123,10 @@ public class PullOperation: Operation { object.setValue(foundObject, forKey: attributeName) } } else { - // TODO: show warning? - print("NOT FOUND " + recordId) + print("warning: object not found " + recordId) } } catch { - // TODO: handle error - print("ERROR: \(error)") + self.errorBlock?(error) } } } From 1dcee9e893f9bb2a7471109f9bcbc8be16d5188d Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 18 Nov 2018 12:37:52 -0800 Subject: [PATCH 012/203] update CloudCore to Swift 4.2 --- Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift | 2 +- Source/Extensions/NSManagedObject.swift | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift b/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift index 2c7aa848..0b6167fd 100644 --- a/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift +++ b/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift @@ -34,7 +34,7 @@ class CoreDataAttribute { let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: transformerName)) { self.value = transformer.reverseTransformedValue(value) } else { - self.value = NSKeyedArchiver.archivedData(withRootObject: value) + self.value = NSKeyedArchiver.archivedData(withRootObject: value!) } } else { self.value = value diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index 757781ed..85c7b6b4 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -35,8 +35,10 @@ extension NSManagedObject { guard let serviceAttributeNames = self.entity.serviceAttributeNames else { throw CloudCoreError.missingServiceAttributes(entityName: self.entity.name) } - - let record = CKRecord(recordType: entityName, zoneID: CloudCore.config.zoneID) + + let recordID = CKRecord.ID(recordName: UUID().uuidString, zoneID: CloudCore.config.zoneID) + let record = CKRecord(recordType: entityName, recordID: recordID) + self.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.recordData) self.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.recordID) From 77380595052bfe26413ab1f5d0238ef68c5d0736 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 21 Nov 2018 00:17:51 -0800 Subject: [PATCH 013/203] support for push to CloudKit via PersistentHistory --- Source/Classes/Pull/PullOperation.swift | 2 +- Source/Classes/Push/CoreDataObserver.swift | 131 +++++++++++++++--- .../ObjectToRecordConverter.swift | 12 +- Source/Model/CloudCoreConfig.swift | 5 +- 4 files changed, 126 insertions(+), 24 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 07604f00..603d9eec 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -51,7 +51,7 @@ public class PullOperation: Operation { CloudCore.delegate?.willSyncFromCloud() let backgroundContext = persistentContainer.newBackgroundContext() - backgroundContext.name = CloudCore.config.contextName + backgroundContext.name = CloudCore.config.pullContextName for database in self.databases { self.addRecordZoneChangesOperation(recordZoneIDs: [CloudCore.config.zoneID], database: database, context: backgroundContext) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index c50fddc2..5cea79d5 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -13,6 +13,7 @@ import CloudKit /// Class responsible for taking action on Core Data changes class CoreDataObserver { var container: NSPersistentContainer + var usePersistentHistory = false let converter = ObjectToRecordConverter() let pushOperationQueue = PushOperationQueue() @@ -27,12 +28,15 @@ class CoreDataObserver { converter.errorBlock = { [weak self] in self?.delegate?.error(error: $0, module: .some(.saveToCloud)) } + + //usePersistentHistory = true } /// Observe Core Data willSave and didSave notifications func start() { NotificationCenter.default.addObserver(self, - selector: #selector(self.willSave(notification:)), name: .NSManagedObjectContextWillSave, + selector: #selector(self.willSave(notification:)), + name: .NSManagedObjectContextWillSave, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.didSave(notification:)), @@ -51,7 +55,7 @@ class CoreDataObserver { func shouldProcess(_ context: NSManagedObjectContext) -> Bool { // Ignore saves that are generated by FetchAndSaveController - if context.name == CloudCore.config.contextName { return false } + if context.name != CloudCore.config.pushContextName { return false } // Upload only for changes in root context that will be saved to persistentStore if context.parent != nil { return false } @@ -59,7 +63,9 @@ class CoreDataObserver { return true } - func processChanges() { + func processChanges() -> Bool { + var success = true + CloudCore.delegate?.willSyncToCloud() let backgroundContext = container.newBackgroundContext() @@ -68,42 +74,131 @@ class CoreDataObserver { let records = converter.processPendingOperations(in: backgroundContext) pushOperationQueue.errorBlock = { self.handle(error: $0, parentContext: backgroundContext) + success = false } pushOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) pushOperationQueue.waitUntilAllOperationsAreFinished() - do { - if backgroundContext.hasChanges { - try backgroundContext.save() + if success { + do { + if backgroundContext.hasChanges { + try backgroundContext.save() + } + } catch { + delegate?.error(error: error, module: .some(.saveToCloud)) + success = false } - } catch { - delegate?.error(error: error, module: .some(.saveToCloud)) } CloudCore.delegate?.didSyncToCloud() + + return success } @objc private func willSave(notification: Notification) { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } - converter.prepareOperationsFor(inserted: context.insertedObjects, - updated: context.updatedObjects, - deleted: context.deletedObjects) + if usePersistentHistory { + context.insertedObjects.forEach { (inserted) in + _ = try? inserted.setRecordInformation() + } + } else { + converter.prepareOperationsFor(inserted: context.insertedObjects, + updated: context.updatedObjects, + deleted: context.deletedObjects) + } } @objc private func didSave(notification: Notification) { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } - guard converter.hasPendingOperations else { return } - - DispatchQueue.global(qos: .utility).async { [weak self] in - guard let observer = self else { return } - observer.processChanges() - } + if usePersistentHistory == true { + DispatchQueue.main.async { [weak self] in + guard let observer = self else { return } + observer.processPersistentHistory() + } + } else { + guard converter.hasPendingOperations else { return } + + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let observer = self else { return } + _ = observer.processChanges() + } + } } - + + func processPersistentHistory() { + if #available(iOS 11.0, watchOSApplicationExtension 4.0, *) { + container.performBackgroundTask { (moc) in + let settings = UserDefaults.standard + let key = "lastPersistentHistoryTokenKey" + var token: NSPersistentHistoryToken? = nil + if let data = settings.object(forKey: key) as? Data { + token = NSKeyedUnarchiver.unarchiveObject(with: data) as? NSPersistentHistoryToken + } + let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) + do { + let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult + + if let history = historyResult.result as? [NSPersistentHistoryTransaction] { + for transaction in history { + if transaction.contextName != CloudCore.config.pushContextName { continue } + + if let changes = transaction.changes { + var insertedObjects = Set() + var updatedObject = Set() + var deletedRecordIDs: [RecordIDWithDatabase] = [] + + for change in changes { + switch change.changeType { + case .insert: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + insertedObjects.insert(inserted) + } + + case .update: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + updatedObject.insert(inserted) + } + + case .delete: + if change.tombstone != nil, let recordID = change.tombstone!["recordID"] as? String { + let ckRecordID = CKRecord.ID(recordName: recordID, zoneID: CloudCore.config.zoneID) + let recordIDWithDatabase = RecordIDWithDatabase(ckRecordID, CloudCore.config.container.privateCloudDatabase) + deletedRecordIDs.append(recordIDWithDatabase) + } + } + } + + self.converter.prepareOperationsFor(inserted: insertedObjects, + updated: updatedObject, + deleted: deletedRecordIDs) + + try? moc.save() + + if self.converter.hasPendingOperations { + if self.processChanges() == true { + let data = NSKeyedArchiver.archivedData(withRootObject: transaction.token) + settings.set(data, forKey: key) + + NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) + } else { + break + } + } + } + } + } + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } + } + private func handle(error: Error, parentContext: NSManagedObjectContext) { guard let cloudError = error as? CKError else { delegate?.error(error: error, module: .some(.saveToCloud)) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 94305338..c8914c6f 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -27,11 +27,17 @@ class ObjectToRecordConverter { } func prepareOperationsFor(inserted: Set, updated: Set, deleted: Set) { - pendingConvertOperations = convertOperations(from: inserted, changeType: .inserted) - pendingConvertOperations += convertOperations(from: updated, changeType: .updated) - recordIDsToDelete = convert(deleted: deleted) + + prepareOperationsFor(inserted: inserted, updated: updated, deleted: recordIDsToDelete) } + + func prepareOperationsFor(inserted: Set, updated: Set, deleted deletedIDs: [RecordIDWithDatabase]) { + pendingConvertOperations = convertOperations(from: inserted, changeType: .inserted) + pendingConvertOperations += convertOperations(from: updated, changeType: .updated) + + recordIDsToDelete = deletedIDs + } private func convertOperations(from objectSet: Set, changeType: ManagedObjectChangeType) -> [ObjectToRecordOperation] { var operations = [ObjectToRecordOperation]() diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 2dd943b3..2a54b5ac 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -45,8 +45,9 @@ public struct CloudCoreConfig { var publicSubscriptionIDPrefix = "CloudCore-" // MARK: Core Data - let contextName = "CloudCoreFetchAndSave" - + public let pushContextName = "CloudCorePushContext" + let pullContextName = "CloudCorePullContext" + /// Default entity's attribute name for *Record ID* if User Info is not specified. /// /// Default value is `recordID` From a7bd3b63fdb29a41f7ab4523f6f14d887e52ed04 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 21 Nov 2018 15:51:44 -0800 Subject: [PATCH 014/203] CloudCore.usePersistenHistoryForPush --- Source/Classes/Push/CoreDataObserver.swift | 111 ++++++++++++--------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 5cea79d5..6689ae23 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -13,7 +13,7 @@ import CloudKit /// Class responsible for taking action on Core Data changes class CoreDataObserver { var container: NSPersistentContainer - var usePersistentHistory = false + var usePersistentHistoryForPush = false let converter = ObjectToRecordConverter() let pushOperationQueue = PushOperationQueue() @@ -29,7 +29,13 @@ class CoreDataObserver { self?.delegate?.error(error: $0, module: .some(.saveToCloud)) } - //usePersistentHistory = true + if #available(iOS 11.0, watchOS 4.0, *) { + let storeDescription = container.persistentStoreDescriptions.first + if let persistentHistoryNumber = storeDescription?.options[NSPersistentHistoryTrackingKey] as? NSNumber + { + usePersistentHistoryForPush = persistentHistoryNumber.boolValue + } + } } /// Observe Core Data willSave and didSave notifications @@ -99,7 +105,7 @@ class CoreDataObserver { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } - if usePersistentHistory { + if usePersistentHistoryForPush { context.insertedObjects.forEach { (inserted) in _ = try? inserted.setRecordInformation() } @@ -114,7 +120,7 @@ class CoreDataObserver { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } - if usePersistentHistory == true { + if usePersistentHistoryForPush == true { DispatchQueue.main.async { [weak self] in guard let observer = self else { return } observer.processPersistentHistory() @@ -131,9 +137,55 @@ class CoreDataObserver { func processPersistentHistory() { if #available(iOS 11.0, watchOSApplicationExtension 4.0, *) { + + func process(_ transaction: NSPersistentHistoryTransaction, in moc: NSManagedObjectContext) -> Bool { + var success = true + + if transaction.contextName != CloudCore.config.pushContextName { return success } + + if let changes = transaction.changes { + var insertedObjects = Set() + var updatedObject = Set() + var deletedRecordIDs: [RecordIDWithDatabase] = [] + + for change in changes { + switch change.changeType { + case .insert: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + insertedObjects.insert(inserted) + } + + case .update: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + updatedObject.insert(inserted) + } + + case .delete: + if change.tombstone != nil, let recordID = change.tombstone!["recordID"] as? String { + let ckRecordID = CKRecord.ID(recordName: recordID, zoneID: CloudCore.config.zoneID) + let recordIDWithDatabase = RecordIDWithDatabase(ckRecordID, CloudCore.config.container.privateCloudDatabase) + deletedRecordIDs.append(recordIDWithDatabase) + } + } + } + + self.converter.prepareOperationsFor(inserted: insertedObjects, + updated: updatedObject, + deleted: deletedRecordIDs) + + try? moc.save() + + if self.converter.hasPendingOperations { + success = self.processChanges() + } + } + + return success + } + container.performBackgroundTask { (moc) in - let settings = UserDefaults.standard let key = "lastPersistentHistoryTokenKey" + let settings = UserDefaults.standard var token: NSPersistentHistoryToken? = nil if let data = settings.object(forKey: key) as? Data { token = NSKeyedUnarchiver.unarchiveObject(with: data) as? NSPersistentHistoryToken @@ -144,50 +196,13 @@ class CoreDataObserver { if let history = historyResult.result as? [NSPersistentHistoryTransaction] { for transaction in history { - if transaction.contextName != CloudCore.config.pushContextName { continue } - - if let changes = transaction.changes { - var insertedObjects = Set() - var updatedObject = Set() - var deletedRecordIDs: [RecordIDWithDatabase] = [] + if process(transaction, in: moc) { + let data = NSKeyedArchiver.archivedData(withRootObject: transaction.token) + settings.set(data, forKey: key) - for change in changes { - switch change.changeType { - case .insert: - if let inserted = try? moc.existingObject(with: change.changedObjectID) { - insertedObjects.insert(inserted) - } - - case .update: - if let inserted = try? moc.existingObject(with: change.changedObjectID) { - updatedObject.insert(inserted) - } - - case .delete: - if change.tombstone != nil, let recordID = change.tombstone!["recordID"] as? String { - let ckRecordID = CKRecord.ID(recordName: recordID, zoneID: CloudCore.config.zoneID) - let recordIDWithDatabase = RecordIDWithDatabase(ckRecordID, CloudCore.config.container.privateCloudDatabase) - deletedRecordIDs.append(recordIDWithDatabase) - } - } - } - - self.converter.prepareOperationsFor(inserted: insertedObjects, - updated: updatedObject, - deleted: deletedRecordIDs) - - try? moc.save() - - if self.converter.hasPendingOperations { - if self.processChanges() == true { - let data = NSKeyedArchiver.archivedData(withRootObject: transaction.token) - settings.set(data, forKey: key) - - NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) - } else { - break - } - } + NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) + } else { + break } } } From 30d73c9e54fe53f792c9b25a2487336d288d1b11 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 21 Nov 2018 16:14:34 -0800 Subject: [PATCH 015/203] fix delete --- .../Push/ObjectToRecord/ObjectToRecordConverter.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index c8914c6f..0e47045a 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -26,10 +26,8 @@ class ObjectToRecordConverter { return !pendingConvertOperations.isEmpty || !recordIDsToDelete.isEmpty } - func prepareOperationsFor(inserted: Set, updated: Set, deleted: Set) { - recordIDsToDelete = convert(deleted: deleted) - - prepareOperationsFor(inserted: inserted, updated: updated, deleted: recordIDsToDelete) + func prepareOperationsFor(inserted: Set, updated: Set, deleted: Set) { + prepareOperationsFor(inserted: inserted, updated: updated, deleted: convert(deleted: deleted)) } func prepareOperationsFor(inserted: Set, updated: Set, deleted deletedIDs: [RecordIDWithDatabase]) { From f71a5a1548c2cc184eca1814e1603b1191d2247c Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 21 Nov 2018 17:16:28 -0800 Subject: [PATCH 016/203] processPersistentHistory when reachability changes --- CloudCore.podspec | 2 + CloudCore.xcodeproj/project.pbxproj | 44 +++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++ .../contents.xcworkspacedata | 10 +++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++ Podfile | 8 ++++ Podfile.lock | 16 +++++++ Source/Classes/Push/CoreDataObserver.swift | 44 ++++++++++++++++++- 9 files changed, 147 insertions(+), 1 deletion(-) mode change 100755 => 100644 CloudCore.xcodeproj/project.pbxproj create mode 100644 CloudCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 CloudCore.xcworkspace/contents.xcworkspacedata create mode 100644 CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/CloudCoreExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Podfile create mode 100644 Podfile.lock diff --git a/CloudCore.podspec b/CloudCore.podspec index 342335bd..0717296f 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -20,6 +20,8 @@ Pod::Spec.new do |s| s.ios.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.osx.frameworks = 'Foundation', 'CloudKit', 'CoreData' + s.ios.dependency 'ReachabilitySwift' + s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.0' } s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' end diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj old mode 100755 new mode 100644 index 757c8896..9f08c102 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 501C5822F10154ED6A7BD2E8 /* Pods_CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; @@ -89,6 +90,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCore.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCore/Pods-CloudCore.debug.xcconfig"; sourceTree = ""; }; + 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCore.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCore/Pods-CloudCore.release.xcconfig"; sourceTree = ""; }; D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5C6298B1C3A8BBD007F7B7C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D9089D491FE14E57000FC60C /* SetupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupOperation.swift; sourceTree = ""; }; @@ -166,6 +170,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 501C5822F10154ED6A7BD2E8 /* Pods_CloudCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -196,6 +201,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4D5D43E1DB83FFB79D4B36AE /* Pods */ = { + isa = PBXGroup; + children = ( + 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */, + AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; D5B2E8951C3A780C00C0327D = { isa = PBXGroup; children = ( @@ -204,6 +218,7 @@ E29BB2291E436F310020F5B6 /* Tests */, E22C40441E4291FB009469A1 /* CloudCore.podspec */, D9B3C7301FCEFC9C00CDB7FF /* Frameworks */, + 4D5D43E1DB83FFB79D4B36AE /* Pods */, ); sourceTree = ""; }; @@ -295,6 +310,7 @@ isa = PBXGroup; children = ( D9B3C7331FCEFD9100CDB7FF /* CloudKit.framework */, + 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */, ); name = Frameworks; sourceTree = ""; @@ -521,6 +537,7 @@ isa = PBXNativeTarget; buildConfigurationList = D5B2E8B31C3A780C00C0327D /* Build configuration list for PBXNativeTarget "CloudCore" */; buildPhases = ( + E293C6A0E8D94DD87C14B67D /* [CP] Check Pods Manifest.lock */, D5B2E89A1C3A780C00C0327D /* Sources */, D5B2E89B1C3A780C00C0327D /* Frameworks */, D5B2E89C1C3A780C00C0327D /* Headers */, @@ -684,6 +701,31 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + E293C6A0E8D94DD87C14B67D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CloudCore-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ D5B2E89A1C3A780C00C0327D /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -916,6 +958,7 @@ }; D5B2E8B41C3A780C00C0327D /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -941,6 +984,7 @@ }; D5B2E8B51C3A780C00C0327D /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; diff --git a/CloudCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/CloudCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/CloudCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/CloudCore.xcworkspace/contents.xcworkspacedata b/CloudCore.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..8f7e5e87 --- /dev/null +++ b/CloudCore.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/CloudCoreExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/CloudCoreExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Example/CloudCoreExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Podfile b/Podfile new file mode 100644 index 00000000..8eeeac9e --- /dev/null +++ b/Podfile @@ -0,0 +1,8 @@ + +target 'CloudCore' do + platform :ios, '10.0' + use_frameworks! + + pod 'ReachabilitySwift' +end + diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 00000000..c6baf367 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - ReachabilitySwift (4.3.0) + +DEPENDENCIES: + - ReachabilitySwift + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - ReachabilitySwift + +SPEC CHECKSUMS: + ReachabilitySwift: 408477d1b6ed9779dba301953171e017c31241f3 + +PODFILE CHECKSUM: 0a37dd21daace4e9491496f51280ef2259635294 + +COCOAPODS: 1.5.3 diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 6689ae23..7a52d30d 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -9,11 +9,13 @@ import Foundation import CoreData import CloudKit +#if os(iOS) || os(tvOS) || os(macOS) +import Reachability +#endif /// Class responsible for taking action on Core Data changes class CoreDataObserver { var container: NSPersistentContainer - var usePersistentHistoryForPush = false let converter = ObjectToRecordConverter() let pushOperationQueue = PushOperationQueue() @@ -23,6 +25,18 @@ class CoreDataObserver { // Used for errors delegation weak var delegate: CloudCoreDelegate? + var usePersistentHistoryForPush = false + #if os(iOS) || os(tvOS) || os(macOS) + var reachability: Reachability? + var isOnline = true { + didSet { + if isOnline != oldValue && isOnline == true { + processPersistentHistory() + } + } + } + #endif + public init(container: NSPersistentContainer) { self.container = container converter.errorBlock = { [weak self] in @@ -35,6 +49,9 @@ class CoreDataObserver { { usePersistentHistoryForPush = persistentHistoryNumber.boolValue } + #if os(iOS) || os(tvOS) || os(macOS) + reachability = Reachability(hostname: "icloud.com") + #endif } } @@ -48,11 +65,24 @@ class CoreDataObserver { selector: #selector(self.didSave(notification:)), name: .NSManagedObjectContextDidSave, object: nil) + + #if os(iOS) || os(tvOS) || os(macOS) + NotificationCenter.default.addObserver(self, + selector: #selector(reachabilityChanged(notification:)), + name: .reachabilityChanged, + object: reachability) + + try? reachability?.startNotifier() + #endif } /// Remove Core Data observers func stop() { NotificationCenter.default.removeObserver(self) + + #if os(iOS) || os(tvOS) || os(macOS) + reachability?.stopNotifier() + #endif } deinit { @@ -135,7 +165,19 @@ class CoreDataObserver { } } + #if os(iOS) || os(tvOS) || os(macOS) + @objc private func reachabilityChanged(notification: Notification) { + let reachability = notification.object as! Reachability + + isOnline = reachability.connection != .none + } + #endif + func processPersistentHistory() { + #if os(iOS) || os(tvOS) || os(macOS) + guard isOnline else { return } + #endif + if #available(iOS 11.0, watchOSApplicationExtension 4.0, *) { func process(_ transaction: NSPersistentHistoryTransaction, in moc: NSManagedObjectContext) -> Bool { From 1636053e033cf26a7e3c71708a6372de6eebe75c Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 22 Nov 2018 11:11:18 -0800 Subject: [PATCH 017/203] catch overall CKModify record operation errors --- Source/Classes/Push/PushOperationQueue.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index fd2ad308..92e032b1 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -63,6 +63,12 @@ class PushOperationQueue: OperationQueue { } } + modifyOperation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, operationError) in + if let error = operationError { + self.errorBlock?(error) + } + } + modifyOperation.database = database self.addOperation(modifyOperation) From ac0ada31ade36f9fc64fafdb72281f5b6d7ab21a Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 22 Nov 2018 11:12:07 -0800 Subject: [PATCH 018/203] delete CK records from tombstone recordData --- Source/Classes/Push/CoreDataObserver.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 7a52d30d..2c70c1f2 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -50,7 +50,9 @@ class CoreDataObserver { usePersistentHistoryForPush = persistentHistoryNumber.boolValue } #if os(iOS) || os(tvOS) || os(macOS) - reachability = Reachability(hostname: "icloud.com") + if usePersistentHistoryForPush { + reachability = Reachability(hostname: "icloud.com") + } #endif } } @@ -203,9 +205,9 @@ class CoreDataObserver { } case .delete: - if change.tombstone != nil, let recordID = change.tombstone!["recordID"] as? String { - let ckRecordID = CKRecord.ID(recordName: recordID, zoneID: CloudCore.config.zoneID) - let recordIDWithDatabase = RecordIDWithDatabase(ckRecordID, CloudCore.config.container.privateCloudDatabase) + if change.tombstone != nil, let recordData = change.tombstone!["recordData"] as? Data { + let ckRecord = CKRecord(archivedData: recordData) + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.privateCloudDatabase) deletedRecordIDs.append(recordIDWithDatabase) } } From f644ce854fee93541ac8181711f835efd4883deb Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 22 Nov 2018 13:11:34 -0800 Subject: [PATCH 019/203] =?UTF-8?q?don=E2=80=99t=20push=20all=20if=20using?= =?UTF-8?q?=20persistent=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit and clean up private MOCs --- Source/Classes/CloudCore.swift | 2 +- .../DeleteFromCoreDataOperation.swift | 52 +++++++-------- .../PurgeLocalDatabaseOperation.swift | 52 +++++++-------- .../ObjectToRecordOperation.swift | 20 +++--- Source/Classes/Push/PushOperationQueue.swift | 8 +-- .../Setup/PushAllLocalDataOperation.swift | 58 ++++++++--------- Source/Classes/Setup/SetupOperation.swift | 63 +++++++++---------- 7 files changed, 128 insertions(+), 127 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index fcd85da5..b55f50db 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -196,7 +196,7 @@ open class CloudCore { if case .zoneNotFound = subError.code { // Zone wasn't found, we need to create it self.queue.cancelAllOperations() - let setupOperation = SetupOperation(container: container, parentContext: nil) + let setupOperation = SetupOperation(container: container, uploadAllData: !(coreDataObserver?.usePersistentHistoryForPush)!) self.queue.addOperation(setupOperation) return diff --git a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift index 9c61d765..a6b836e8 100644 --- a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift @@ -27,31 +27,33 @@ class DeleteFromCoreDataOperation: Operation { if self.isCancelled { return } let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.parent = parentContext - - // Iterate through each entity to fetch and delete object with our recordData - guard let entities = childContext.persistentStoreCoordinator?.managedObjectModel.entities else { return } - for entity in entities { - guard let serviceAttributeNames = entity.serviceAttributeNames else { continue } - - do { - let deleted = try self.delete(entityName: serviceAttributeNames.entityName, - attributeNames: serviceAttributeNames, - in: childContext) - - // only 1 record with such recordData may exists, if delete we don't need to fetch other entities - if deleted { break } - } catch { - self.errorBlock?(error) - continue - } - } - - do { - try childContext.save() - } catch { - self.errorBlock?(error) - } + childContext.performAndWait { + childContext.parent = parentContext + + // Iterate through each entity to fetch and delete object with our recordData + guard let entities = childContext.persistentStoreCoordinator?.managedObjectModel.entities else { return } + for entity in entities { + guard let serviceAttributeNames = entity.serviceAttributeNames else { continue } + + do { + let deleted = try self.delete(entityName: serviceAttributeNames.entityName, + attributeNames: serviceAttributeNames, + in: childContext) + + // only 1 record with such recordData may exists, if delete we don't need to fetch other entities + if deleted { break } + } catch { + self.errorBlock?(error) + continue + } + } + + do { + try childContext.save() + } catch { + self.errorBlock?(error) + } + } } /// Delete NSManagedObject with specified recordData from entity diff --git a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift index 6c1b5b94..219ab273 100644 --- a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift +++ b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift @@ -25,31 +25,33 @@ class PurgeLocalDatabaseOperation: Operation { super.main() let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.parent = parentContext - - for entity in managedObjectModel.cloudCoreEnabledEntities { - guard let entityName = entity.name else { continue } - - let fetchRequest = NSFetchRequest(entityName: entityName) - fetchRequest.includesPropertyValues = false - - do { - // I don't user `NSBatchDeleteRequest` because we can't notify viewContextes about changes - guard let objects = try childContext.fetch(fetchRequest) as? [NSManagedObject] else { continue } - - for object in objects { - childContext.delete(object) - } - } catch { - errorBlock?(error) - } - } - - do { - try childContext.save() - } catch { - errorBlock?(error) - } + childContext.performAndWait { + childContext.parent = parentContext + + for entity in managedObjectModel.cloudCoreEnabledEntities { + guard let entityName = entity.name else { continue } + + let fetchRequest = NSFetchRequest(entityName: entityName) + fetchRequest.includesPropertyValues = false + + do { + // I don't user `NSBatchDeleteRequest` because we can't notify viewContextes about changes + guard let objects = try childContext.fetch(fetchRequest) as? [NSManagedObject] else { continue } + + for object in objects { + childContext.delete(object) + } + } catch { + errorBlock?(error) + } + } + + do { + try childContext.save() + } catch { + errorBlock?(error) + } + } } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 917200bd..8a0d9fb3 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -40,15 +40,17 @@ class ObjectToRecordOperation: Operation { } let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.parent = parentContext - - do { - try self.fillRecordWithData(using: childContext) - try childContext.save() - self.conversionCompletionBlock?(self.record) - } catch { - self.errorCompletionBlock?(error) - } + childContext.performAndWait { + childContext.parent = parentContext + + do { + try self.fillRecordWithData(using: childContext) + try childContext.save() + self.conversionCompletionBlock?(self.record) + } catch { + self.errorCompletionBlock?(error) + } + } } private func fillRecordWithData(using context: NSManagedObjectContext) throws { diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index 92e032b1..f8825b5c 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -62,13 +62,7 @@ class PushOperationQueue: OperationQueue { self.errorBlock?(error) } } - - modifyOperation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, operationError) in - if let error = operationError { - self.errorBlock?(error) - } - } - + modifyOperation.database = database self.addOperation(modifyOperation) diff --git a/Source/Classes/Setup/PushAllLocalDataOperation.swift b/Source/Classes/Setup/PushAllLocalDataOperation.swift index a755fa15..b7f4f42f 100644 --- a/Source/Classes/Setup/PushAllLocalDataOperation.swift +++ b/Source/Classes/Setup/PushAllLocalDataOperation.swift @@ -38,34 +38,36 @@ class PushAllLocalDataOperation: Operation { } let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.parent = parentContext - - var allManagedObjects = Set() - for entity in managedObjectModel.cloudCoreEnabledEntities { - guard let entityName = entity.name else { continue } - let fetchRequest = NSFetchRequest(entityName: entityName) - - do { - guard let fetchedObjects = try childContext.fetch(fetchRequest) as? [NSManagedObject] else { - continue - } - - allManagedObjects.formUnion(fetchedObjects) - } catch { - errorBlock?(error) - } - } - - converter.prepareOperationsFor(inserted: allManagedObjects, updated: Set(), deleted: Set()) - let recordsToSave = converter.processPendingOperations(in: childContext).recordsToSave - pushOperationQueue.addOperations(recordsToSave: recordsToSave, recordIDsToDelete: [RecordIDWithDatabase]()) - pushOperationQueue.waitUntilAllOperationsAreFinished() - - do { - try childContext.save() - } catch { - errorBlock?(error) - } + childContext.performAndWait { + childContext.parent = parentContext + + var allManagedObjects = Set() + for entity in managedObjectModel.cloudCoreEnabledEntities { + guard let entityName = entity.name else { continue } + let fetchRequest = NSFetchRequest(entityName: entityName) + + do { + guard let fetchedObjects = try childContext.fetch(fetchRequest) as? [NSManagedObject] else { + continue + } + + allManagedObjects.formUnion(fetchedObjects) + } catch { + errorBlock?(error) + } + } + + converter.prepareOperationsFor(inserted: allManagedObjects, updated: Set(), deleted: Set()) + let recordsToSave = converter.processPendingOperations(in: childContext).recordsToSave + pushOperationQueue.addOperations(recordsToSave: recordsToSave, recordIDsToDelete: [RecordIDWithDatabase]()) + pushOperationQueue.waitUntilAllOperationsAreFinished() + + do { + try childContext.save() + } catch { + errorBlock?(error) + } + } } override func cancel() { diff --git a/Source/Classes/Setup/SetupOperation.swift b/Source/Classes/Setup/SetupOperation.swift index 03215ceb..35fd4003 100644 --- a/Source/Classes/Setup/SetupOperation.swift +++ b/Source/Classes/Setup/SetupOperation.swift @@ -20,14 +20,14 @@ class SetupOperation: Operation { var errorBlock: ErrorBlock? let container: NSPersistentContainer - let parentContext: NSManagedObjectContext? + let uploadAllData: Bool /// - Parameters: /// - container: persistent container to get managedObject model from /// - parentContext: context where changed data will be save (recordID's). If it is `nil`, new context will be created from `container` and saved - init(container: NSPersistentContainer, parentContext: NSManagedObjectContext?) { + init(container: NSPersistentContainer, uploadAllData: Bool) { self.container = container - self.parentContext = parentContext + self.uploadAllData = uploadAllData } private let queue = OperationQueue() @@ -35,48 +35,47 @@ class SetupOperation: Operation { override func main() { super.main() - let childContext: NSManagedObjectContext - - if let parentContext = self.parentContext { - childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.parent = parentContext - } else { - childContext = container.newBackgroundContext() - } - + let childContext = container.newBackgroundContext() + var operations: [Operation] = [] + // Create CloudCore Zone let createZoneOperation = CreateCloudCoreZoneOperation() createZoneOperation.errorBlock = { self.errorBlock?($0) self.queue.cancelAllOperations() } - + operations.append(createZoneOperation) + // Subscribe operation #if !os(watchOS) let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = errorBlock subscribeOperation.addDependency(createZoneOperation) - queue.addOperation(subscribeOperation) + operations.append(subscribeOperation) #endif - - // Upload all local data - let uploadOperation = PushAllLocalDataOperation(parentContext: childContext, managedObjectModel: container.managedObjectModel) - uploadOperation.errorBlock = errorBlock + + if uploadAllData { + // Upload all local data + let uploadOperation = PushAllLocalDataOperation(parentContext: childContext, managedObjectModel: container.managedObjectModel) + uploadOperation.errorBlock = errorBlock + + #if !os(watchOS) + uploadOperation.addDependency(subscribeOperation) + #endif + operations.append(uploadOperation) + } + + queue.maxConcurrentOperationCount = 1 + queue.addOperations(operations, waitUntilFinished: true) - #if !os(watchOS) - uploadOperation.addDependency(subscribeOperation) - #endif - - queue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) - - if self.parentContext == nil { - do { - // It's safe to save because we instatinated that context in current thread - try childContext.save() - } catch { - errorBlock?(error) - } - } + childContext.performAndWait { + do { + // It's safe to save because we instatinated that context in current thread + try childContext.save() + } catch { + errorBlock?(error) + } + } } } From 000a82d6d95e5109110693a252490e68b95b14eb Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 22 Nov 2018 13:33:58 -0800 Subject: [PATCH 020/203] rename to PullResult for consistency --- README.md | 6 +++--- Source/Classes/CloudCore.swift | 20 +++++++++---------- Source/Classes/Pull/PullOperation.swift | 6 +++--- Source/Classes/Push/CoreDataObserver.swift | 2 +- .../{FetchResult.swift => PullResult.swift} | 10 +++++----- 5 files changed, 22 insertions(+), 22 deletions(-) rename Source/Enum/{FetchResult.swift => PullResult.swift} (70%) diff --git a/README.md b/README.md index 6e039588..da48b07d 100755 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ CloudCore is built using "black box" architecture, so it works invisibly for you 1. CloudCore stores *change tokens* from CloudKit, so only changed data is downloaded. 2. When CloudCore is enabled (`CloudCore.enable`) it fetches changed data from CloudKit and subscribes to CloudKit push notifications about new changes. -3. When `CloudCore.fetchAndSave` is called manually or by push notification, CloudCore fetches and saves changed data to Core Data. +3. When `CloudCore.pull` is called manually or by push notification, CloudCore fetches and saves changed data to Core Data. 4. When data is written to persistent container (parent context is saved) CloudCore founds locally changed data and uploads it to CloudKit. ## Installation @@ -68,7 +68,7 @@ func application(_ application: UIApplication, didReceiveRemoteNotification user // Check if it CloudKit's and CloudCore notification if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { // Fetch changed data from iCloud - CloudCore.fetchAndSave(using: userInfo, to: persistentContainer, error: nil, completion: { (fetchResult) in + CloudCore.pull(using: userInfo, to: persistentContainer, error: nil, completion: { (fetchResult) in completionHandler(fetchResult.uiBackgroundFetchResult) }) } @@ -116,7 +116,7 @@ You can find example application at [Example](/Example/) directory. **How to use it:** * **+** button adds new object to local storage (that will be automatically synced to Cloud) -* **refresh** button calls `fetchAndSave` to fetch data from Cloud. That is useful button for simulators because Simulator unable to receive push notifications +* **refresh** button calls `pull` to fetch data from Cloud. That is useful button for simulators because Simulator unable to receive push notifications * Use [CloudKit dashboard](https://icloud.developer.apple.com/dashboard/) to make changes and see it at application, and make change in application and see ones in dashboard. Don't forget to refresh dashboard's page because it doesn't update data on-the-fly. ## Tests diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index b55f50db..0232bdb1 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -30,7 +30,7 @@ import CloudKit ``` ## Fetch from cloud - When CloudKit data is changed **push notification** is posted to an application. You need to handle it and fetch changed data from CloudKit with `CloudCore.fetchAndSave(using:to:error:completion:)` method. + When CloudKit data is changed **push notification** is posted to an application. You need to handle it and fetch changed data from CloudKit with `CloudCore.pull(using:to:error:completion:)` method. ### Example ```swift @@ -38,14 +38,14 @@ import CloudKit // Check if it CloudKit's and CloudCore notification if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { // Fetch changed data from iCloud - CloudCore.fetchAndSave(using: userInfo, to: persistentContainer, error: nil, completion: { (fetchResult) in + CloudCore.pull(using: userInfo, to: persistentContainer, error: nil, completion: { (fetchResult) in completionHandler(fetchResult.uiBackgroundFetchResult) }) } } ``` - You can also check for updated data at CloudKit **manually** (e.g. push notifications are not working). Use for that `CloudCore.fetchAndSave(to:error:completion:)` + You can also check for updated data at CloudKit **manually** (e.g. push notifications are not working). Use for that `CloudCore.pull(to:error:completion:)` */ open class CloudCore { @@ -117,15 +117,15 @@ open class CloudCore { /** Fetch changes from one CloudKit database and save it to CoreData from `didReceiveRemoteNotification` method. - Don't forget to check notification's UserInfo by calling `isCloudCoreNotification(withUserInfo:)`. If incorrect user info is provided `FetchResult.noData` will be returned at completion block. + Don't forget to check notification's UserInfo by calling `isCloudCoreNotification(withUserInfo:)`. If incorrect user info is provided `PullResult.noData` will be returned at completion block. - Parameters: - userInfo: notification's user info, CloudKit database will be extraced from that notification - container: `NSPersistentContainer` that will be used to save fetched data - error: block will be called every time when error occurs during process - - completion: `FetchResult` enumeration with results of operation + - completion: `PullResult` enumeration with results of operation */ - public static func fetchAndSave(using userInfo: NotificationUserInfo, to container: NSPersistentContainer, error: ErrorBlock?, completion: @escaping (_ fetchResult: FetchResult) -> Void) { + public static func pull(using userInfo: NotificationUserInfo, to container: NSPersistentContainer, error: ErrorBlock?, completion: @escaping (_ fetchResult: PullResult) -> Void) { guard let cloudDatabase = self.database(for: userInfo) else { completion(.noData) return @@ -138,9 +138,9 @@ open class CloudCore { operation.start() if errorProxy.wasError { - completion(FetchResult.failed) + completion(PullResult.failed) } else { - completion(FetchResult.newData) + completion(PullResult.newData) } } } @@ -150,9 +150,9 @@ open class CloudCore { - Parameters: - container: `NSPersistentContainer` that will be used to save fetched data - error: block will be called every time when error occurs during process - - completion: `FetchResult` enumeration with results of operation + - completion: `PullResult` enumeration with results of operation */ - public static func fetchAndSave(to container: NSPersistentContainer, error: ErrorBlock?, completion: (() -> Void)?) { + public static func pull(to container: NSPersistentContainer, error: ErrorBlock?, completion: (() -> Void)?) { let operation = PullOperation(persistentContainer: container) operation.errorBlock = error operation.completionBlock = completion diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 603d9eec..01e1f213 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -1,5 +1,5 @@ // -// FetchAndSaveOperation.swift +// PullOperation.swift // CloudCore // // Created by Vasily Ulianov on 13/03/2017. @@ -9,7 +9,7 @@ import CloudKit import CoreData -/// An operation that fetches data from CloudKit and saves it to Core Data, you can use it without calling `CloudCore.fetchAndSave` methods if you application relies on `Operation` +/// An operation that fetches data from CloudKit and saves it to Core Data, you can use it without calling `CloudCore.pull` methods if you application relies on `Operation` public class PullOperation: Operation { /// Private cloud database for the CKContainer specified by CloudCoreConfig @@ -41,7 +41,7 @@ public class PullOperation: Operation { self.databases = databases self.persistentContainer = persistentContainer - queue.name = "FetchAndSaveQueue" + queue.name = "PullQueue" } /// Performs the receiver’s non-concurrent task. diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 2c70c1f2..ff65c1c2 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -92,7 +92,7 @@ class CoreDataObserver { } func shouldProcess(_ context: NSManagedObjectContext) -> Bool { - // Ignore saves that are generated by FetchAndSaveController + // Ignore saves that are generated by PullController if context.name != CloudCore.config.pushContextName { return false } // Upload only for changes in root context that will be saved to persistentStore diff --git a/Source/Enum/FetchResult.swift b/Source/Enum/PullResult.swift similarity index 70% rename from Source/Enum/FetchResult.swift rename to Source/Enum/PullResult.swift index 816eb629..a947bee5 100644 --- a/Source/Enum/FetchResult.swift +++ b/Source/Enum/PullResult.swift @@ -1,5 +1,5 @@ // -// FetchResult.swift +// PullResult.swift // CloudCore // // Created by Vasily Ulianov on 08.02.17. @@ -9,12 +9,12 @@ import Foundation -/// Enumeration with results of `FetchAndSaveOperation`. -public enum FetchResult: UInt { +/// Enumeration with results of `PullOperation`. +public enum PullResult: UInt { /// Fetching has successfully completed without any errors case newData = 0 - /// No fetching was done, maybe fired with `FetchAndSaveOperation` was called with incorrect UserInfo without CloudCore's data + /// No fetching was done, maybe fired with `PullOperation` was called with incorrect UserInfo without CloudCore's data case noData = 1 /// There were some errors during operation @@ -24,7 +24,7 @@ public enum FetchResult: UInt { #if os(iOS) import UIKit - public extension FetchResult { + public extension PullResult { /// Convert `self` to `UIBackgroundFetchResult` /// From 98ef980fc73e3aaa0327298f5cfd1a2169c5f708 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 22 Nov 2018 13:44:35 -0800 Subject: [PATCH 021/203] more normalizing on push/pull terminology --- CloudCore.xcodeproj/project.pbxproj | 64 ++++++++++++++++++++-- Podfile | 6 ++ Podfile.lock | 2 +- Source/Classes/CloudCore.swift | 2 +- Source/Classes/Push/CoreDataObserver.swift | 14 ++--- Source/Enum/Module.swift | 4 +- 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 9f08c102..fb10e6de 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 501C5822F10154ED6A7BD2E8 /* Pods_CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */; }; + 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57505AAF21A7591500D9CF8F /* PullResult.swift */; }; + B34DB05E7C4B442CDBC5475B /* Pods_CloudCoreTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; @@ -63,7 +65,6 @@ E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A0D1E4C99AD001B2871 /* ObjectToRecordConverter.swift */; }; E2C02A141E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A131E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift */; }; E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A181E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift */; }; - E2C3A6D11E4A8EAF009151F3 /* FetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */; }; E2D390081E4A49350019BBCD /* NSEntityDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D390071E4A49350019BBCD /* NSEntityDescription.swift */; }; E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E296C91E49DA0800E7D6ED /* Tokens.swift */; }; E2E4D8411E76D5A600550CBE /* PullOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E4D83D1E76D4EF00550CBE /* PullOperation.swift */; }; @@ -90,7 +91,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0AB2CA993B2DDA9672D6AB2D /* Pods-CloudCoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests.debug.xcconfig"; sourceTree = ""; }; + 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCore.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCore/Pods-CloudCore.debug.xcconfig"; sourceTree = ""; }; + 57505AAF21A7591500D9CF8F /* PullResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullResult.swift; sourceTree = ""; }; 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCore.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCore/Pods-CloudCore.release.xcconfig"; sourceTree = ""; }; D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -156,13 +160,13 @@ E2C02A0D1E4C99AD001B2871 /* ObjectToRecordConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectToRecordConverter.swift; sourceTree = ""; }; E2C02A131E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRecordZoneChangesOperation.swift; sourceTree = ""; }; E2C02A181E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteFromCoreDataOperation.swift; sourceTree = ""; }; - E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchResult.swift; sourceTree = ""; }; E2D390071E4A49350019BBCD /* NSEntityDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEntityDescription.swift; sourceTree = ""; }; E2E296C91E49DA0800E7D6ED /* Tokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokens.swift; sourceTree = ""; }; E2E4D83D1E76D4EF00550CBE /* PullOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullOperation.swift; sourceTree = ""; }; E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAttributeName.swift; sourceTree = ""; }; E2FA74431E769BF900C3489D /* RecordWithDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWithDatabase.swift; sourceTree = ""; }; E2FA74471E769D9400C3489D /* RecordIDWithDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordIDWithDatabase.swift; sourceTree = ""; }; + FD3EED3090CAFFEB7FCD9CC7 /* Pods-CloudCoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -195,6 +199,7 @@ buildActionMask = 2147483647; files = ( E29BB22D1E436F310020F5B6 /* CloudCore.framework in Frameworks */, + B34DB05E7C4B442CDBC5475B /* Pods_CloudCoreTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -206,6 +211,8 @@ children = ( 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */, AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */, + 0AB2CA993B2DDA9672D6AB2D /* Pods-CloudCoreTests.debug.xcconfig */, + FD3EED3090CAFFEB7FCD9CC7 /* Pods-CloudCoreTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -311,6 +318,7 @@ children = ( D9B3C7331FCEFD9100CDB7FF /* CloudKit.framework */, 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */, + 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -485,7 +493,7 @@ isa = PBXGroup; children = ( E29BB21B1E43381D0020F5B6 /* CloudCoreError.swift */, - E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */, + 57505AAF21A7591500D9CF8F /* PullResult.swift */, D97465F91FE31A650060EA66 /* Module.swift */, ); path = Enum; @@ -591,9 +599,11 @@ isa = PBXNativeTarget; buildConfigurationList = E29BB2301E436F310020F5B6 /* Build configuration list for PBXNativeTarget "CloudCoreTests" */; buildPhases = ( + CA0ECDA8B5B1B5A7FFF86F6A /* [CP] Check Pods Manifest.lock */, E29BB2241E436F310020F5B6 /* Sources */, E29BB2251E436F310020F5B6 /* Frameworks */, E29BB2261E436F310020F5B6 /* Resources */, + 5DDB158E70ED5BCED209F262 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -702,6 +712,50 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 5DDB158E70ED5BCED209F262 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + ); + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + CA0ECDA8B5B1B5A7FFF86F6A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CloudCoreTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; E293C6A0E8D94DD87C14B67D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -742,12 +796,12 @@ D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */, E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */, E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */, - E2C3A6D11E4A8EAF009151F3 /* FetchResult.swift in Sources */, E22C40461E42956C009469A1 /* CoreDataObserver.swift in Sources */, E2075FF91E4BBEAC00E31F1F /* AsynchronousOperation.swift in Sources */, E24F44A61E4595B900F78819 /* CoreDataRelationship.swift in Sources */, D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */, D97465FA1FE31A650060EA66 /* Module.swift in Sources */, + 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */, E29BB2371E4377F80020F5B6 /* CoreDataAttribute.swift in Sources */, E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */, E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */, @@ -1109,6 +1163,7 @@ }; E29BB2311E436F310020F5B6 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0AB2CA993B2DDA9672D6AB2D /* Pods-CloudCoreTests.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -1130,6 +1185,7 @@ }; E29BB2321E436F310020F5B6 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = FD3EED3090CAFFEB7FCD9CC7 /* Pods-CloudCoreTests.release.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; diff --git a/Podfile b/Podfile index 8eeeac9e..46ce2ed5 100644 --- a/Podfile +++ b/Podfile @@ -6,3 +6,9 @@ target 'CloudCore' do pod 'ReachabilitySwift' end +target 'CloudCoreTests' do + platform :ios, '10.0' + use_frameworks! + + pod 'ReachabilitySwift' +end diff --git a/Podfile.lock b/Podfile.lock index c6baf367..e7cb9608 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -11,6 +11,6 @@ SPEC REPOS: SPEC CHECKSUMS: ReachabilitySwift: 408477d1b6ed9779dba301953171e017c31241f3 -PODFILE CHECKSUM: 0a37dd21daace4e9491496f51280ef2259635294 +PODFILE CHECKSUM: f59ea1aa3b1dbf873da9c79c15ed99a9ef75f238 COCOAPODS: 1.5.3 diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 0232bdb1..02ee9d4e 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -93,7 +93,7 @@ open class CloudCore { // Fetch updated data (e.g. push notifications weren't received) let updateFromCloudOperation = PullOperation(persistentContainer: container) updateFromCloudOperation.errorBlock = { - self.delegate?.error(error: $0, module: .some(.fetchFromCloud)) + self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } #if !os(watchOS) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index ff65c1c2..a3c477d6 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -40,7 +40,7 @@ class CoreDataObserver { public init(container: NSPersistentContainer) { self.container = container converter.errorBlock = { [weak self] in - self?.delegate?.error(error: $0, module: .some(.saveToCloud)) + self?.delegate?.error(error: $0, module: .some(.pushToCloud)) } if #available(iOS 11.0, watchOS 4.0, *) { @@ -123,7 +123,7 @@ class CoreDataObserver { try backgroundContext.save() } } catch { - delegate?.error(error: error, module: .some(.saveToCloud)) + delegate?.error(error: error, module: .some(.pushToCloud)) success = false } } @@ -260,7 +260,7 @@ class CoreDataObserver { private func handle(error: Error, parentContext: NSManagedObjectContext) { guard let cloudError = error as? CKError else { - delegate?.error(error: error, module: .some(.saveToCloud)) + delegate?.error(error: error, module: .some(.pushToCloud)) return } @@ -272,25 +272,25 @@ class CoreDataObserver { // Create CloudCore Zone let createZoneOperation = CreateCloudCoreZoneOperation() createZoneOperation.errorBlock = { - self.delegate?.error(error: $0, module: .some(.saveToCloud)) + self.delegate?.error(error: $0, module: .some(.pushToCloud)) self.pushOperationQueue.cancelAllOperations() } // Subscribe operation #if !os(watchOS) let subscribeOperation = SubscribeOperation() - subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) } + subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } subscribeOperation.addDependency(createZoneOperation) pushOperationQueue.addOperation(subscribeOperation) #endif // Upload all local data let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) - uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) } + uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } pushOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) case .operationCancelled: return - default: delegate?.error(error: cloudError, module: .some(.saveToCloud)) + default: delegate?.error(error: cloudError, module: .some(.pushToCloud)) } } diff --git a/Source/Enum/Module.swift b/Source/Enum/Module.swift index 2ff7525b..cf956d54 100644 --- a/Source/Enum/Module.swift +++ b/Source/Enum/Module.swift @@ -12,9 +12,9 @@ import Foundation public enum Module { /// Save to CloudKit module - case saveToCloud + case pushToCloud /// Fetch from CloudKit module - case fetchFromCloud + case pullFromCloud } From f01a249497f8d762e0e9aa396e9e442f4b56c15c Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 22 Nov 2018 14:01:53 -0800 Subject: [PATCH 022/203] TODO: optimize by using change.updatedProperties --- Source/Classes/Push/CoreDataObserver.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index a3c477d6..4c34a635 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -201,6 +201,7 @@ class CoreDataObserver { case .update: if let inserted = try? moc.existingObject(with: change.changedObjectID) { + // TODO: optimize by using change.updatedProperties updatedObject.insert(inserted) } From eb252812711a7ae703107fea18be9a6bbd57b022 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 26 Nov 2018 16:21:10 -0800 Subject: [PATCH 023/203] actually delete history as we push --- Source/Classes/Push/CoreDataObserver.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 4c34a635..1fba4ae5 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -245,7 +245,8 @@ class CoreDataObserver { let data = NSKeyedArchiver.archivedData(withRootObject: transaction.token) settings.set(data, forKey: key) - NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) + let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) + try moc.execute(deleteRequest) } else { break } From 3bfa66af2550ea693dee9d42ca1c6dedfac3e668 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 26 Nov 2018 17:22:36 -0800 Subject: [PATCH 024/203] use recordNames to match CK and CD objects --- Source/Classes/Pull/PullOperation.swift | 8 +-- .../DeleteFromCoreDataOperation.swift | 2 +- .../RecordToCoreDataOperation.swift | 9 +-- .../ObjectToRecordOperation.swift | 6 +- Source/Extensions/NSEntityDescription.swift | 61 ++++++++++++------- Source/Extensions/NSManagedObject.swift | 8 ++- Source/Model/CloudCoreConfig.swift | 15 +++-- Source/Model/CloudKitAttribute.swift | 10 +-- Source/Model/ServiceAttributeName.swift | 10 ++- 9 files changed, 79 insertions(+), 50 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 01e1f213..f5f05000 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -101,13 +101,13 @@ public class PullOperation: Operation { for (object, references) in missingReferences { guard let serviceAttributes = object.entity.serviceAttributeNames else { continue } - for (attributeName, recordIDs) in references { - for recordId in recordIDs { + for (attributeName, recordNames) in references { + for recordName in recordNames { guard let relationship = object.entity.relationshipsByName[attributeName], let targetEntityName = relationship.destinationEntity?.name else { continue } // TODO: move to extension let fetchRequest = NSFetchRequest(entityName: targetEntityName) - fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordID + " == %@" , recordId) + fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordName + " == %@" , recordName) fetchRequest.fetchLimit = 1 fetchRequest.includesPropertyValues = false @@ -123,7 +123,7 @@ public class PullOperation: Operation { object.setValue(foundObject, forKey: attributeName) } } else { - print("warning: object not found " + recordId) + print("warning: object not found " + recordName) } } catch { self.errorBlock?(error) diff --git a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift index a6b836e8..bc84b338 100644 --- a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift @@ -62,7 +62,7 @@ class DeleteFromCoreDataOperation: Operation { private func delete(entityName: String, attributeNames: ServiceAttributeNames, in context: NSManagedObjectContext) throws -> Bool { let fetchRequest = NSFetchRequest(entityName: entityName) fetchRequest.includesPropertyValues = false - fetchRequest.predicate = NSPredicate(format: attributeNames.recordID + " = %@", recordID.encodedString) + fetchRequest.predicate = NSPredicate(format: attributeNames.recordName + " = %@", recordID.recordName) guard let objects = try context.fetch(fetchRequest) as? [NSManagedObject] else { return false } if objects.isEmpty { return false } diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index c9ec09a6..1e0cb286 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -10,8 +10,8 @@ import CoreData import CloudKit typealias AttributeName = String -typealias RecordID = String -typealias MissingReferences = [NSManagedObject: [AttributeName: [RecordID]]] +typealias RecordName = String +typealias MissingReferences = [NSManagedObject: [AttributeName: [RecordName]]] /// Convert CKRecord to NSManagedObject and save it to parent context, thread-safe class RecordToCoreDataOperation: AsynchronousOperation { @@ -61,7 +61,7 @@ class RecordToCoreDataOperation: AsynchronousOperation { // Try to find existing objects let fetchRequest = NSFetchRequest(entityName: entityName) - fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordID + " == %@", record.recordID.encodedString) + fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordName + " == %@", record.recordID.recordName) if let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject { try fill(object: foundObject, entityName: entityName, serviceAttributeNames: serviceAttributes, context: context) @@ -96,11 +96,12 @@ class RecordToCoreDataOperation: AsynchronousOperation { } } else { object.setValue(coreDataValue, forKey: key) - missingObjectsPerEntities[object] = attribute.notFoundRecordIDsForAttribute + missingObjectsPerEntities[object] = attribute.notFoundRecordNamesForAttribute } } // Set system headers + object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName) object.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.recordID) object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.recordData) } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 8a0d9fb3..4325370c 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -61,7 +61,9 @@ class ObjectToRecordOperation: Operation { let changedValues = managedObject.committedValues(forKeys: changedAttributes) for (attributeName, value) in changedValues { - if attributeName == serviceAttributeNames.recordData || attributeName == serviceAttributeNames.recordID { continue } + if attributeName == serviceAttributeNames.recordData + || attributeName == serviceAttributeNames.recordID + || attributeName == serviceAttributeNames.recordName { continue } if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) { let recordValue = try attribute.makeRecordValue() @@ -77,7 +79,7 @@ class ObjectToRecordOperation: Operation { let entityName = record.recordType let fetchRequest = NSFetchRequest(entityName: entityName) - fetchRequest.predicate = NSPredicate(format: serviceAttributeNames.recordID + " == %@", record.recordID.encodedString) + fetchRequest.predicate = NSPredicate(format: serviceAttributeNames.recordName + " == %@", record.recordID.recordName) return try context.fetch(fetchRequest).first as? NSManagedObject } diff --git a/Source/Extensions/NSEntityDescription.swift b/Source/Extensions/NSEntityDescription.swift index 776b2b5b..d6e46fbc 100644 --- a/Source/Extensions/NSEntityDescription.swift +++ b/Source/Extensions/NSEntityDescription.swift @@ -17,38 +17,52 @@ extension NSEntityDescription { // Get required attributes // Record Data - let recordDataName: String + let recordDataAttribute: String if let recordDataUserInfoName = attributeNamesFromUserInfo.recordData { - recordDataName = recordDataUserInfoName + recordDataAttribute = recordDataUserInfoName } else { // Last chance: try to find default attribute name in entity if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordData) { - recordDataName = CloudCore.config.defaultAttributeNameRecordData + recordDataAttribute = CloudCore.config.defaultAttributeNameRecordData } else { return nil } } - // Record ID - let recordIDName: String - if let recordIDUserInfoName = attributeNamesFromUserInfo.recordID { - recordIDName = recordIDUserInfoName - } else { - // Last chance: try to find default attribute name in entity - if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordID) { - recordIDName = CloudCore.config.defaultAttributeNameRecordID - } else { - return nil - } - } - - return ServiceAttributeNames(entityName: entityName, recordData: recordDataName, recordID: recordIDName, isPublic: attributeNamesFromUserInfo.isPublic) + // Record ID + let recordIDAttribute: String + if let recordIDUserInfoName = attributeNamesFromUserInfo.recordID { + recordIDAttribute = recordIDUserInfoName + } else { + // Last chance: try to find default attribute name in entity + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordID) { + recordIDAttribute = CloudCore.config.defaultAttributeNameRecordID + } else { + return nil + } + } + + // Record Name + let recordNameAttribute: String + if let recordNameUserInfoName = attributeNamesFromUserInfo.recordName { + recordNameAttribute = recordNameUserInfoName + } else { + // Last chance: try to find default attribute name in entity + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordName) { + recordNameAttribute = CloudCore.config.defaultAttributeNameRecordName + } else { + return nil + } + } + + return ServiceAttributeNames(entityName: entityName, recordName: recordNameAttribute, recordID: recordIDAttribute, recordData: recordDataAttribute, isPublic: attributeNamesFromUserInfo.isPublic) } /// Parse data from User Info dictionary - private func parseAttributeNamesFromUserInfo() -> (isPublic: Bool, recordData: String?, recordID: String?) { - var recordDataName: String? - var recordIDName: String? + private func parseAttributeNamesFromUserInfo() -> (isPublic: Bool, recordData: String?, recordID: String?, recordName: String?) { + var recordDataAttribute: String? + var recordIDAttribute: String? + var recordNameAttribute: String? var isPublic = false // In attribute @@ -62,8 +76,9 @@ extension NSEntityDescription { if key == ServiceAttributeNames.keyType { switch value { - case ServiceAttributeNames.valueRecordID: recordIDName = attributeName - case ServiceAttributeNames.valueRecordData: recordDataName = attributeName + case ServiceAttributeNames.valueRecordData: recordDataAttribute = attributeName + case ServiceAttributeNames.valueRecordID: recordIDAttribute = attributeName + case ServiceAttributeNames.valueRecordName: recordNameAttribute = attributeName default: continue } } else if key == ServiceAttributeNames.keyIsPublic { @@ -72,7 +87,7 @@ extension NSEntityDescription { } } - return (isPublic, recordDataName, recordIDName) + return (isPublic, recordDataAttribute, recordIDAttribute, recordNameAttribute) } } diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index 85c7b6b4..b754cf44 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -36,12 +36,14 @@ extension NSManagedObject { throw CloudCoreError.missingServiceAttributes(entityName: self.entity.name) } - let recordID = CKRecord.ID(recordName: UUID().uuidString, zoneID: CloudCore.config.zoneID) + let recordName = UUID().uuidString + let recordID = CKRecord.ID(recordName: recordName, zoneID: CloudCore.config.zoneID) let record = CKRecord(recordType: entityName, recordID: recordID) - self.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.recordData) + self.setValue(recordName, forKey: serviceAttributeNames.recordName) self.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.recordID) - + self.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.recordData) + return record } } diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 2a54b5ac..d7688827 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -53,11 +53,16 @@ public struct CloudCoreConfig { /// Default value is `recordID` public var defaultAttributeNameRecordID = "recordID" - /// Default entity's attribute name for *Record Data* if User Info is not specified - /// - /// Default value is `recordData` - public var defaultAttributeNameRecordData = "recordData" - + /// Default entity's attribute name for *Record Data* if User Info is not specified + /// + /// Default value is `recordData` + public var defaultAttributeNameRecordData = "recordData" + + /// Default entity's attribute name for *Record Name* if User Info is not specified + /// + /// Default value is `recordName` + public var defaultAttributeNameRecordName = "recordName" + // MARK: User Defaults /// UserDefault's key to store `Tokens` object diff --git a/Source/Model/CloudKitAttribute.swift b/Source/Model/CloudKitAttribute.swift index 6688813d..1f769748 100644 --- a/Source/Model/CloudKitAttribute.swift +++ b/Source/Model/CloudKitAttribute.swift @@ -19,7 +19,7 @@ class CloudKitAttribute { let entityName: String let serviceAttributes: ServiceAttributeNames let context: NSManagedObjectContext - var notFoundRecordIDsForAttribute = [AttributeName: [RecordID]]() + var notFoundRecordNamesForAttribute = [AttributeName: [RecordName]]() init(value: Any?, fieldName: String, entityName: String, serviceAttributes: ServiceAttributeNames, context: NSManagedObjectContext) { self.value = value @@ -52,16 +52,16 @@ class CloudKitAttribute { // FIXME: user serviceAttributes.recordID from target entity (not from me) - fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordID + " == %@" , recordID.encodedString) + fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordName + " == %@" , recordID.recordName) fetchRequest.fetchLimit = 1 fetchRequest.includesPropertyValues = false let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject if foundObject == nil { - var values = notFoundRecordIDsForAttribute[fieldName] ?? [] - values.append(recordID.encodedString) - notFoundRecordIDsForAttribute[fieldName] = values + var values = notFoundRecordNamesForAttribute[fieldName] ?? [] + values.append(recordID.recordName) + notFoundRecordNamesForAttribute[fieldName] = values } return foundObject diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index 7174eaf4..7bfcfff5 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -13,11 +13,15 @@ struct ServiceAttributeNames { static let keyType = "CloudCoreType" static let keyIsPublic = "CloudCorePublicDatabase" - static let valueRecordData = "recordData" + static let valueRecordName = "recordName" static let valueRecordID = "recordID" - + static let valueRecordData = "recordData" + let entityName: String + + let recordName: String + let recordID: String let recordData: String - let recordID: String + let isPublic: Bool } From 411015497ddc5d9accf01e47fdf97c11daa22b81 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 26 Nov 2018 22:04:34 -0800 Subject: [PATCH 025/203] =?UTF-8?q?add=20ServiceAttributeNames.contains(?= =?UTF-8?q?=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Push/ObjectToRecord/ObjectToRecordOperation.swift | 4 +--- Source/Model/ServiceAttributeName.swift | 9 +++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 4325370c..c174cc54 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -61,9 +61,7 @@ class ObjectToRecordOperation: Operation { let changedValues = managedObject.committedValues(forKeys: changedAttributes) for (attributeName, value) in changedValues { - if attributeName == serviceAttributeNames.recordData - || attributeName == serviceAttributeNames.recordID - || attributeName == serviceAttributeNames.recordName { continue } + if serviceAttributeNames.contains(attributeName) { continue } if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) { let recordValue = try attribute.makeRecordValue() diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index 7bfcfff5..a65da2c0 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -24,4 +24,13 @@ struct ServiceAttributeNames { let recordData: String let isPublic: Bool + + func contains(_ attributeName: String) -> Bool { + switch attributeName { + case recordName, recordID, recordData: + return true + default: + return false + } + } } From 1eca3dfa699503e25d54b6781fedbb8b2f63ea4d Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 26 Nov 2018 22:04:56 -0800 Subject: [PATCH 026/203] use existing recordName if it exists --- Source/Extensions/NSManagedObject.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index b754cf44..94f2ce4b 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -36,14 +36,14 @@ extension NSManagedObject { throw CloudCoreError.missingServiceAttributes(entityName: self.entity.name) } - let recordName = UUID().uuidString + let recordName = (self.value(forKey: serviceAttributeNames.recordName) as? String) ?? UUID().uuidString let recordID = CKRecord.ID(recordName: recordName, zoneID: CloudCore.config.zoneID) let record = CKRecord(recordType: entityName, recordID: recordID) self.setValue(recordName, forKey: serviceAttributeNames.recordName) self.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.recordID) self.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.recordData) - + return record } } From 0cc7f5af399ee5aecbddf0bf4b70770995440875 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 27 Nov 2018 01:03:23 -0800 Subject: [PATCH 027/203] support for public push --- .../RecordToCoreDataOperation.swift | 9 +- Source/Classes/Push/CoreDataObserver.swift | 18 ++- .../ObjectToRecord/CoreDataRelationship.swift | 12 +- .../ObjectToRecordConverter.swift | 11 +- .../ObjectToRecordOperation.swift | 8 +- Source/Extensions/NSEntityDescription.swift | 105 ++++++++++++------ Source/Extensions/NSManagedObject.swift | 36 ++++-- Source/Model/CloudCoreConfig.swift | 30 +++-- Source/Model/ServiceAttributeName.swift | 16 ++- 9 files changed, 164 insertions(+), 81 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 1e0cb286..676cef5b 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -102,7 +102,12 @@ class RecordToCoreDataOperation: AsynchronousOperation { // Set system headers object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName) - object.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.recordID) - object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.recordData) + if record.recordID.zoneID == CloudCore.config.zoneID { + object.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.privateRecordID) + object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) + } else { + object.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.publicRecordID) + object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.publicRecordData) + } } } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 1fba4ae5..1c4d3e27 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -139,7 +139,8 @@ class CoreDataObserver { if usePersistentHistoryForPush { context.insertedObjects.forEach { (inserted) in - _ = try? inserted.setRecordInformation() + let _ = try? inserted.setRecordInformation(for: .private) + let _ = try? inserted.setRecordInformation(for: .public) } } else { converter.prepareOperationsFor(inserted: context.insertedObjects, @@ -206,10 +207,17 @@ class CoreDataObserver { } case .delete: - if change.tombstone != nil, let recordData = change.tombstone!["recordData"] as? Data { - let ckRecord = CKRecord(archivedData: recordData) - let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.privateCloudDatabase) - deletedRecordIDs.append(recordIDWithDatabase) + if change.tombstone != nil { + if let privateRecordData = change.tombstone!["privateRecordData"] as? Data { + let ckRecord = CKRecord(archivedData: privateRecordData) + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.privateCloudDatabase) + deletedRecordIDs.append(recordIDWithDatabase) + } + if let publicRecordData = change.tombstone!["publicRecordData"] as? Data { + let ckRecord = CKRecord(archivedData: publicRecordData) + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.publicCloudDatabase) + deletedRecordIDs.append(recordIDWithDatabase) + } } } } diff --git a/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift b/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift index 069c2862..729a5bd8 100644 --- a/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift +++ b/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift @@ -10,19 +10,19 @@ import CoreData import CloudKit class CoreDataRelationship { - typealias Class = CoreDataRelationship - + let scope: CKDatabase.Scope let value: Any let description: NSRelationshipDescription /// Initialize Core Data Attribute with properties and value /// - Returns: `nil` if it is not an attribute (possible it is relationship?) - init?(value: Any, relationshipName: String, entity: NSEntityDescription) { - guard let description = Class.relationshipDescription(for: relationshipName, in: entity) else { + init?(scope: CKDatabase.Scope, value: Any, relationshipName: String, entity: NSEntityDescription) { + guard let description = CoreDataRelationship.relationshipDescription(for: relationshipName, in: entity) else { // it is not a relationship return nil } + self.scope = scope self.description = description self.value = value } @@ -71,8 +71,8 @@ class CoreDataRelationship { } else { action = .none } - - guard let record = try managedObject.restoreRecordWithSystemFields() else { + + guard let record = try managedObject.restoreRecordWithSystemFields(for: scope) else { // That is possible if method is called before all managed object were filled with recordData // That may cause possible reference corruption (Core Data -> iCloud), but it is not critical assertionFailure("Managed Object doesn't have stored record information, should be reported as a framework bug") diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 0e47045a..15a1776b 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -46,8 +46,10 @@ class ObjectToRecordConverter { do { let recordWithSystemFields: CKRecord + + let scope: CKDatabase.Scope = serviceAttributeNames.isPublic ? .public : .private - if let restoredRecord = try object.restoreRecordWithSystemFields() { + if let restoredRecord = try object.restoreRecordWithSystemFields(for: scope) { switch changeType { case .inserted: // Create record with same ID but wihout token data (that record was accidently deleted from CloudKit perhaps, recordID exists in CoreData, but record doesn't exist in CloudKit @@ -57,7 +59,7 @@ class ObjectToRecordConverter { recordWithSystemFields = restoredRecord } } else { - recordWithSystemFields = try object.setRecordInformation() + recordWithSystemFields = try object.setRecordInformation(for: scope) } var changedAttributes: [String]? @@ -65,7 +67,8 @@ class ObjectToRecordConverter { // Save changes keys only for updated object, for inserted objects full sync will be used if case .updated = changeType { changedAttributes = Array(object.changedValues().keys) } - let convertOperation = ObjectToRecordOperation(record: recordWithSystemFields, + let convertOperation = ObjectToRecordOperation(scope: scope, + record: recordWithSystemFields, changedAttributes: changedAttributes, serviceAttributeNames: serviceAttributeNames) @@ -94,7 +97,7 @@ class ObjectToRecordConverter { var recordIDs = [RecordIDWithDatabase]() for object in objectSet { - if let triedRestoredRecord = try? object.restoreRecordWithSystemFields(), + if let triedRestoredRecord = try? object.restoreRecordWithSystemFields(for: .private), let restoredRecord = triedRestoredRecord, let serviceAttributeNames = object.entity.serviceAttributeNames { let database = self.database(for: restoredRecord.recordID, serviceAttributes: serviceAttributeNames) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index c174cc54..783a559d 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -14,6 +14,7 @@ class ObjectToRecordOperation: Operation { var parentContext: NSManagedObjectContext? // Set on init + let scope: CKDatabase.Scope let record: CKRecord private let changedAttributes: [String]? private let serviceAttributeNames: ServiceAttributeNames @@ -22,8 +23,9 @@ class ObjectToRecordOperation: Operation { var errorCompletionBlock: ((Error) -> Void)? var conversionCompletionBlock: ((CKRecord) -> Void)? - init(record: CKRecord, changedAttributes: [String]?, serviceAttributeNames: ServiceAttributeNames) { - self.record = record + init(scope: CKDatabase.Scope, record: CKRecord, changedAttributes: [String]?, serviceAttributeNames: ServiceAttributeNames) { + self.scope = scope + self.record = record self.changedAttributes = changedAttributes self.serviceAttributeNames = serviceAttributeNames @@ -66,7 +68,7 @@ class ObjectToRecordOperation: Operation { if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) { let recordValue = try attribute.makeRecordValue() record.setValue(recordValue, forKey: attributeName) - } else if let relationship = CoreDataRelationship(value: value, relationshipName: attributeName, entity: managedObject.entity) { + } else if let relationship = CoreDataRelationship(scope: scope, value: value, relationshipName: attributeName, entity: managedObject.entity) { let references = try relationship.makeRecordValue() record.setValue(references, forKey: attributeName) } diff --git a/Source/Extensions/NSEntityDescription.swift b/Source/Extensions/NSEntityDescription.swift index d6e46fbc..fa62db45 100644 --- a/Source/Extensions/NSEntityDescription.swift +++ b/Source/Extensions/NSEntityDescription.swift @@ -15,56 +15,89 @@ extension NSEntityDescription { let attributeNamesFromUserInfo = self.parseAttributeNamesFromUserInfo() // Get required attributes - - // Record Data - let recordDataAttribute: String - if let recordDataUserInfoName = attributeNamesFromUserInfo.recordData { - recordDataAttribute = recordDataUserInfoName - } else { - // Last chance: try to find default attribute name in entity - if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordData) { - recordDataAttribute = CloudCore.config.defaultAttributeNameRecordData - } else { - return nil - } - } - - // Record ID - let recordIDAttribute: String - if let recordIDUserInfoName = attributeNamesFromUserInfo.recordID { - recordIDAttribute = recordIDUserInfoName + // Record Name + let recordNameAttribute: String + if let recordNameUserInfoName = attributeNamesFromUserInfo.recordName { + recordNameAttribute = recordNameUserInfoName } else { // Last chance: try to find default attribute name in entity - if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordID) { - recordIDAttribute = CloudCore.config.defaultAttributeNameRecordID + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordName) { + recordNameAttribute = CloudCore.config.defaultAttributeNameRecordName } else { return nil } } - // Record Name - let recordNameAttribute: String - if let recordNameUserInfoName = attributeNamesFromUserInfo.recordName { - recordNameAttribute = recordNameUserInfoName + // Private Record ID + let privateRecordIDAttribute: String + if let recordIDUserInfoName = attributeNamesFromUserInfo.privateRecordID { + privateRecordIDAttribute = recordIDUserInfoName } else { // Last chance: try to find default attribute name in entity - if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameRecordName) { - recordNameAttribute = CloudCore.config.defaultAttributeNameRecordName + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNamePrivateRecordID) { + privateRecordIDAttribute = CloudCore.config.defaultAttributeNamePrivateRecordID } else { return nil } } - return ServiceAttributeNames(entityName: entityName, recordName: recordNameAttribute, recordID: recordIDAttribute, recordData: recordDataAttribute, isPublic: attributeNamesFromUserInfo.isPublic) + // Private Record Data + let privateRecordDataAttribute: String + if let recordDataUserInfoName = attributeNamesFromUserInfo.privateRecordData { + privateRecordDataAttribute = recordDataUserInfoName + } else { + // Last chance: try to find default attribute name in entity + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNamePrivateRecordData) { + privateRecordDataAttribute = CloudCore.config.defaultAttributeNamePrivateRecordData + } else { + return nil + } + } + + // Pubic Record ID + let publicRecordIDAttribute: String + if let recordIDUserInfoName = attributeNamesFromUserInfo.publicRecordID { + publicRecordIDAttribute = recordIDUserInfoName + } else { + // Last chance: try to find default attribute name in entity + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNamePublicRecordID) { + publicRecordIDAttribute = CloudCore.config.defaultAttributeNamePublicRecordID + } else { + return nil + } + } + + // Public Record Data + let publicRecordDataAttribute: String + if let recordDataUserInfoName = attributeNamesFromUserInfo.publicRecordData { + publicRecordDataAttribute = recordDataUserInfoName + } else { + // Last chance: try to find default attribute name in entity + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNamePublicRecordData) { + publicRecordDataAttribute = CloudCore.config.defaultAttributeNamePublicRecordData + } else { + return nil + } + } + + return ServiceAttributeNames(entityName: entityName, + recordName: recordNameAttribute, + privateRecordID: privateRecordIDAttribute, + privateRecordData: privateRecordDataAttribute, + publicRecordID: publicRecordIDAttribute, + publicRecordData: publicRecordDataAttribute, + isPublic: attributeNamesFromUserInfo.isPublic) } /// Parse data from User Info dictionary - private func parseAttributeNamesFromUserInfo() -> (isPublic: Bool, recordData: String?, recordID: String?, recordName: String?) { - var recordDataAttribute: String? - var recordIDAttribute: String? + private func parseAttributeNamesFromUserInfo() -> (isPublic: Bool, recordName: String?, privateRecordID: String?, privateRecordData: String?, publicRecordID: String?, publicRecordData: String?) { + var isPublic = false var recordNameAttribute: String? - var isPublic = false - + var privateRecordIDAttribute: String? + var privateRecordDataAttribute: String? + var publicRecordIDAttribute: String? + var publicRecordDataAttribute: String? + // In attribute for (attributeName, attributeDescription) in self.attributesByName { guard let userInfo = attributeDescription.userInfo else { continue } @@ -76,9 +109,11 @@ extension NSEntityDescription { if key == ServiceAttributeNames.keyType { switch value { - case ServiceAttributeNames.valueRecordData: recordDataAttribute = attributeName - case ServiceAttributeNames.valueRecordID: recordIDAttribute = attributeName case ServiceAttributeNames.valueRecordName: recordNameAttribute = attributeName + case ServiceAttributeNames.valuePrivateRecordID: privateRecordIDAttribute = attributeName + case ServiceAttributeNames.valuePrivateRecordData: privateRecordDataAttribute = attributeName + case ServiceAttributeNames.valuePublicRecordID: publicRecordIDAttribute = attributeName + case ServiceAttributeNames.valuePublicRecordData: publicRecordDataAttribute = attributeName default: continue } } else if key == ServiceAttributeNames.keyIsPublic { @@ -87,7 +122,7 @@ extension NSEntityDescription { } } - return (isPublic, recordDataAttribute, recordIDAttribute, recordNameAttribute) + return (isPublic, recordNameAttribute, privateRecordIDAttribute, privateRecordDataAttribute, publicRecordIDAttribute, publicRecordDataAttribute) } } diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index 94f2ce4b..e5d7fa9d 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -14,11 +14,12 @@ extension NSManagedObject { /// /// - Returns: unacrhived `CKRecord` containing restored system fields (like RecordID, tokens, creationg date etc) /// - Throws: `CloudCoreError.missingServiceAttributes` if names of CloudCore attributes are not specified in User Info - func restoreRecordWithSystemFields() throws -> CKRecord? { + func restoreRecordWithSystemFields(for scope: CKDatabase.Scope) throws -> CKRecord? { guard let serviceAttributeNames = self.entity.serviceAttributeNames else { throw CloudCoreError.missingServiceAttributes(entityName: self.entity.name) } - guard let encodedRecordData = self.value(forKey: serviceAttributeNames.recordData) as? Data else { return nil } + let key = scope == .public ? serviceAttributeNames.publicRecordData : serviceAttributeNames.privateRecordData + guard let encodedRecordData = self.value(forKey: key) as? Data else { return nil } return CKRecord(archivedData: encodedRecordData) } @@ -28,7 +29,7 @@ extension NSManagedObject { /// - Postcondition: `self` is modified (recordData and recordID is written) /// - Throws: may throw exception if unable to find attributes marked by User Info as service attributes /// - Returns: new `CKRecord` - @discardableResult func setRecordInformation() throws -> CKRecord { + @discardableResult func setRecordInformation(for scope: CKDatabase.Scope) throws -> CKRecord { guard let entityName = self.entity.name else { throw CloudCoreError.coreData("No entity name for \(self.entity)") } @@ -36,14 +37,29 @@ extension NSManagedObject { throw CloudCoreError.missingServiceAttributes(entityName: self.entity.name) } - let recordName = (self.value(forKey: serviceAttributeNames.recordName) as? String) ?? UUID().uuidString - let recordID = CKRecord.ID(recordName: recordName, zoneID: CloudCore.config.zoneID) - let record = CKRecord(recordType: entityName, recordID: recordID) + var recordName = self.value(forKey: serviceAttributeNames.recordName) as? String + if recordName == nil { + recordName = UUID().uuidString + self.setValue(recordName, forKey: serviceAttributeNames.recordName) + } - self.setValue(recordName, forKey: serviceAttributeNames.recordName) - self.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.recordID) - self.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.recordData) + let aRecord: CKRecord + if scope == .public { + let publicRecordID = CKRecord.ID(recordName: recordName!) + let publicRecord = CKRecord(recordType: entityName, recordID:publicRecordID) + self.setValue(publicRecord.recordID.encodedString, forKey: serviceAttributeNames.publicRecordID) + self.setValue(publicRecord.encdodedSystemFields, forKey: serviceAttributeNames.publicRecordData) + + aRecord = publicRecord + } else { + let privateRecordID = CKRecord.ID(recordName: recordName!, zoneID: CloudCore.config.zoneID) + let privateRecord = CKRecord(recordType: entityName, recordID: privateRecordID) + self.setValue(privateRecord.recordID.encodedString, forKey: serviceAttributeNames.privateRecordID) + self.setValue(privateRecord.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) + + aRecord = privateRecord + } - return record + return aRecord } } diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index d7688827..8743509c 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -48,21 +48,31 @@ public struct CloudCoreConfig { public let pushContextName = "CloudCorePushContext" let pullContextName = "CloudCorePullContext" - /// Default entity's attribute name for *Record ID* if User Info is not specified. - /// - /// Default value is `recordID` - public var defaultAttributeNameRecordID = "recordID" - - /// Default entity's attribute name for *Record Data* if User Info is not specified - /// - /// Default value is `recordData` - public var defaultAttributeNameRecordData = "recordData" - /// Default entity's attribute name for *Record Name* if User Info is not specified /// /// Default value is `recordName` public var defaultAttributeNameRecordName = "recordName" + /// Default entity's attribute name for *Private Record ID* if User Info is not specified. + /// + /// Default value is `privateRecordID` + public var defaultAttributeNamePrivateRecordID = "privateRecordID" + + /// Default entity's attribute name for *Private Record Data* if User Info is not specified + /// + /// Default value is `privateRecordData` + public var defaultAttributeNamePrivateRecordData = "privateRecordData" + + /// Default entity's attribute name for *Public Record ID* if User Info is not specified. + /// + /// Default value is `publicRecordID` + public var defaultAttributeNamePublicRecordID = "publicRecordID" + + /// Default entity's attribute name for *Public Record Data* if User Info is not specified + /// + /// Default value is `publicRecordData` + public var defaultAttributeNamePublicRecordData = "publicRecordData" + // MARK: User Defaults /// UserDefault's key to store `Tokens` object diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index a65da2c0..eb09b588 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -14,20 +14,24 @@ struct ServiceAttributeNames { static let keyIsPublic = "CloudCorePublicDatabase" static let valueRecordName = "recordName" - static let valueRecordID = "recordID" - static let valueRecordData = "recordData" + static let valuePrivateRecordID = "privateRecordID" + static let valuePrivateRecordData = "privateRecordData" + static let valuePublicRecordID = "publicRecordID" + static let valuePublicRecordData = "publicRecordData" let entityName: String let recordName: String - let recordID: String - let recordData: String - + let privateRecordID: String + let privateRecordData: String + let publicRecordID: String + let publicRecordData: String + let isPublic: Bool func contains(_ attributeName: String) -> Bool { switch attributeName { - case recordName, recordID, recordData: + case recordName, privateRecordID, privateRecordData, publicRecordID, publicRecordData: return true default: return false From 5231870e26637c1c43a03ad2baf3ddf8761f50db Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 27 Nov 2018 12:07:55 -0800 Subject: [PATCH 028/203] replace .isPublic with ServiceAttributeNames.scopes allow Core Data schema to define which scopes to push --- .../ObjectToRecordConverter.swift | 125 +++++++++--------- Source/Extensions/NSEntityDescription.swift | 63 +++++---- Source/Model/ServiceAttributeName.swift | 7 +- 3 files changed, 107 insertions(+), 88 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 15a1776b..f3436084 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -43,51 +43,51 @@ class ObjectToRecordConverter { for object in objectSet { // Ignore entities that doesn't have required service attributes guard let serviceAttributeNames = object.entity.serviceAttributeNames else { continue } - - do { - let recordWithSystemFields: CKRecord - - let scope: CKDatabase.Scope = serviceAttributeNames.isPublic ? .public : .private - - if let restoredRecord = try object.restoreRecordWithSystemFields(for: scope) { - switch changeType { - case .inserted: - // Create record with same ID but wihout token data (that record was accidently deleted from CloudKit perhaps, recordID exists in CoreData, but record doesn't exist in CloudKit - let recordID = restoredRecord.recordID - recordWithSystemFields = CKRecord(recordType: restoredRecord.recordType, recordID: recordID) - case .updated: - recordWithSystemFields = restoredRecord - } - } else { - recordWithSystemFields = try object.setRecordInformation(for: scope) - } - - var changedAttributes: [String]? - - // Save changes keys only for updated object, for inserted objects full sync will be used - if case .updated = changeType { changedAttributes = Array(object.changedValues().keys) } - - let convertOperation = ObjectToRecordOperation(scope: scope, - record: recordWithSystemFields, - changedAttributes: changedAttributes, - serviceAttributeNames: serviceAttributeNames) - convertOperation.errorCompletionBlock = { [weak self] error in - self?.errorBlock?(error) - } - - convertOperation.conversionCompletionBlock = { [weak self] record in - guard let me = self else { return } - - let cloudDatabase = me.database(for: record.recordID, serviceAttributes: serviceAttributeNames) - let recordWithDB = RecordWithDatabase(record, cloudDatabase) - me.convertedRecords.append(recordWithDB) - } - - operations.append(convertOperation) - } catch { - errorBlock?(error) - } + for scope in serviceAttributeNames.scopes { + do { + let recordWithSystemFields: CKRecord + + if let restoredRecord = try object.restoreRecordWithSystemFields(for: scope) { + switch changeType { + case .inserted: + // Create record with same ID but wihout token data (that record was accidently deleted from CloudKit perhaps, recordID exists in CoreData, but record doesn't exist in CloudKit + let recordID = restoredRecord.recordID + recordWithSystemFields = CKRecord(recordType: restoredRecord.recordType, recordID: recordID) + case .updated: + recordWithSystemFields = restoredRecord + } + } else { + recordWithSystemFields = try object.setRecordInformation(for: scope) + } + + var changedAttributes: [String]? + + // Save changes keys only for updated object, for inserted objects full sync will be used + if case .updated = changeType { changedAttributes = Array(object.changedValues().keys) } + + let convertOperation = ObjectToRecordOperation(scope: scope, + record: recordWithSystemFields, + changedAttributes: changedAttributes, + serviceAttributeNames: serviceAttributeNames) + + convertOperation.errorCompletionBlock = { [weak self] error in + self?.errorBlock?(error) + } + + convertOperation.conversionCompletionBlock = { [weak self] record in + guard let me = self else { return } + + let cloudDatabase = me.database(for: scope) + let recordWithDB = RecordWithDatabase(record, cloudDatabase) + me.convertedRecords.append(recordWithDB) + } + + operations.append(convertOperation) + } catch { + errorBlock?(error) + } + } } return operations @@ -97,13 +97,16 @@ class ObjectToRecordConverter { var recordIDs = [RecordIDWithDatabase]() for object in objectSet { - if let triedRestoredRecord = try? object.restoreRecordWithSystemFields(for: .private), - let restoredRecord = triedRestoredRecord, - let serviceAttributeNames = object.entity.serviceAttributeNames { - let database = self.database(for: restoredRecord.recordID, serviceAttributes: serviceAttributeNames) - let recordIDWithDB = RecordIDWithDatabase(restoredRecord.recordID, database) - recordIDs.append(recordIDWithDB) - } + guard let serviceAttributeNames = object.entity.serviceAttributeNames else { continue } + + for scope in serviceAttributeNames.scopes { + if let triedRestoredRecord = try? object.restoreRecordWithSystemFields(for: scope), + let restoredRecord = triedRestoredRecord { + let database = self.database(for: scope) + let recordIDWithDB = RecordIDWithDatabase(restoredRecord.recordID, database) + recordIDs.append(recordIDWithDB) + } + } } return recordIDs @@ -131,17 +134,15 @@ class ObjectToRecordConverter { } /// Get appropriate database for modify operations - private func database(for recordID: CKRecord.ID, serviceAttributes: ServiceAttributeNames) -> CKDatabase { + private func database(for scope: CKDatabase.Scope) -> CKDatabase { let container = CloudCore.config.container - - if serviceAttributes.isPublic { return container.publicCloudDatabase } - - let ownerName = recordID.zoneID.ownerName - - if ownerName == CKCurrentUserDefaultName { - return container.privateCloudDatabase - } else { - return container.sharedCloudDatabase - } + switch scope { + case .private: + return container.privateCloudDatabase + case .shared: + return container.sharedCloudDatabase + case .public: + return container.publicCloudDatabase + } } } diff --git a/Source/Extensions/NSEntityDescription.swift b/Source/Extensions/NSEntityDescription.swift index fa62db45..a4c8f007 100644 --- a/Source/Extensions/NSEntityDescription.swift +++ b/Source/Extensions/NSEntityDescription.swift @@ -7,6 +7,7 @@ // import CoreData +import CloudKit extension NSEntityDescription { var serviceAttributeNames: ServiceAttributeNames? { @@ -81,48 +82,64 @@ extension NSEntityDescription { } return ServiceAttributeNames(entityName: entityName, + scopes: attributeNamesFromUserInfo.scopes, recordName: recordNameAttribute, privateRecordID: privateRecordIDAttribute, privateRecordData: privateRecordDataAttribute, publicRecordID: publicRecordIDAttribute, - publicRecordData: publicRecordDataAttribute, - isPublic: attributeNamesFromUserInfo.isPublic) + publicRecordData: publicRecordDataAttribute) } /// Parse data from User Info dictionary - private func parseAttributeNamesFromUserInfo() -> (isPublic: Bool, recordName: String?, privateRecordID: String?, privateRecordData: String?, publicRecordID: String?, publicRecordData: String?) { - var isPublic = false + private func parseAttributeNamesFromUserInfo() -> (scopes: [CKDatabase.Scope], recordName: String?, privateRecordID: String?, privateRecordData: String?, publicRecordID: String?, publicRecordData: String?) { + var scopes: [CKDatabase.Scope] = [] var recordNameAttribute: String? var privateRecordIDAttribute: String? var privateRecordDataAttribute: String? var publicRecordIDAttribute: String? var publicRecordDataAttribute: String? - - // In attribute - for (attributeName, attributeDescription) in self.attributesByName { - guard let userInfo = attributeDescription.userInfo else { continue } - - // In userInfo dictionary - for (key, value) in userInfo { - guard let key = key as? String, - let value = value as? String else { continue } - - if key == ServiceAttributeNames.keyType { - switch value { + + func parse(_ attributeName: String, _ userInfo: [AnyHashable: Any]) { + for (key, value) in userInfo { + guard let key = key as? String, + let value = value as? String else { continue } + + if key == ServiceAttributeNames.keyType { + switch value { case ServiceAttributeNames.valueRecordName: recordNameAttribute = attributeName case ServiceAttributeNames.valuePrivateRecordID: privateRecordIDAttribute = attributeName case ServiceAttributeNames.valuePrivateRecordData: privateRecordDataAttribute = attributeName case ServiceAttributeNames.valuePublicRecordID: publicRecordIDAttribute = attributeName case ServiceAttributeNames.valuePublicRecordData: publicRecordDataAttribute = attributeName - default: continue - } - } else if key == ServiceAttributeNames.keyIsPublic { - if value == "true" { isPublic = true } - } - } + default: continue + } + } else if key == ServiceAttributeNames.keyScopes { + let scopeStrings = value.components(separatedBy: ",") + for scopeString in scopeStrings { + switch scopeString { + case "public": + scopes.append(.public) + case "private": + scopes.append(.private) + default: + break + } + } + } + } + } + + if let userInfo = self.userInfo { + parse("", userInfo) + } + + // In attribute + for (attributeName, attributeDescription) in self.attributesByName { + guard let userInfo = attributeDescription.userInfo else { continue } + parse(attributeName, userInfo) } - return (isPublic, recordNameAttribute, privateRecordIDAttribute, privateRecordDataAttribute, publicRecordIDAttribute, publicRecordDataAttribute) + return (scopes, recordNameAttribute, privateRecordIDAttribute, privateRecordDataAttribute, publicRecordIDAttribute, publicRecordDataAttribute) } } diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index eb09b588..36c247fd 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -7,11 +7,12 @@ // import CoreData +import CloudKit struct ServiceAttributeNames { // User Info keys & values static let keyType = "CloudCoreType" - static let keyIsPublic = "CloudCorePublicDatabase" + static let keyScopes = "CloudCoreScopes" static let valueRecordName = "recordName" static let valuePrivateRecordID = "privateRecordID" @@ -21,13 +22,13 @@ struct ServiceAttributeNames { let entityName: String + let scopes: [CKDatabase.Scope] + let recordName: String let privateRecordID: String let privateRecordData: String let publicRecordID: String let publicRecordData: String - - let isPublic: Bool func contains(_ attributeName: String) -> Bool { switch attributeName { From 5175bd1bfbbb0d5e2af368468fbb3bbfdb5e8567 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 27 Nov 2018 12:25:11 -0800 Subject: [PATCH 029/203] only setRecordInfo for scopes --- Source/Classes/Push/CoreDataObserver.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 1c4d3e27..05341715 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -139,8 +139,11 @@ class CoreDataObserver { if usePersistentHistoryForPush { context.insertedObjects.forEach { (inserted) in - let _ = try? inserted.setRecordInformation(for: .private) - let _ = try? inserted.setRecordInformation(for: .public) + if let serviceAttributeNames = inserted.entity.serviceAttributeNames { + for scope in serviceAttributeNames.scopes { + let _ = try? inserted.setRecordInformation(for: scope) + } + } } } else { converter.prepareOperationsFor(inserted: context.insertedObjects, From b75027efdad3544bfbc66b7aac1c5fd07f24984d Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 27 Nov 2018 12:25:32 -0800 Subject: [PATCH 030/203] use container.databse(with: scope) --- .../Push/ObjectToRecord/ObjectToRecordConverter.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index f3436084..5fa1d3d0 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -135,14 +135,6 @@ class ObjectToRecordConverter { /// Get appropriate database for modify operations private func database(for scope: CKDatabase.Scope) -> CKDatabase { - let container = CloudCore.config.container - switch scope { - case .private: - return container.privateCloudDatabase - case .shared: - return container.sharedCloudDatabase - case .public: - return container.publicCloudDatabase - } + return CloudCore.config.container.database(with: scope) } } From 9f69b5291b130b471f49715df0f39b0f5e638312 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 27 Nov 2018 22:37:03 -0800 Subject: [PATCH 031/203] =?UTF-8?q?with=20recordName,=20we=20don=E2=80=99t?= =?UTF-8?q?=20need=20to=20store=20recordIDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecordToCoreDataOperation.swift | 2 -- Source/Extensions/NSEntityDescription.swift | 36 ++----------------- Source/Extensions/NSManagedObject.swift | 2 -- Source/Model/CloudCoreConfig.swift | 12 +------ Source/Model/ServiceAttributeName.swift | 6 +--- 5 files changed, 4 insertions(+), 54 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 676cef5b..e757172d 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -103,10 +103,8 @@ class RecordToCoreDataOperation: AsynchronousOperation { // Set system headers object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName) if record.recordID.zoneID == CloudCore.config.zoneID { - object.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.privateRecordID) object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) } else { - object.setValue(record.recordID.encodedString, forKey: serviceAttributeNames.publicRecordID) object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.publicRecordData) } } diff --git a/Source/Extensions/NSEntityDescription.swift b/Source/Extensions/NSEntityDescription.swift index a4c8f007..946680bc 100644 --- a/Source/Extensions/NSEntityDescription.swift +++ b/Source/Extensions/NSEntityDescription.swift @@ -29,19 +29,6 @@ extension NSEntityDescription { } } - // Private Record ID - let privateRecordIDAttribute: String - if let recordIDUserInfoName = attributeNamesFromUserInfo.privateRecordID { - privateRecordIDAttribute = recordIDUserInfoName - } else { - // Last chance: try to find default attribute name in entity - if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNamePrivateRecordID) { - privateRecordIDAttribute = CloudCore.config.defaultAttributeNamePrivateRecordID - } else { - return nil - } - } - // Private Record Data let privateRecordDataAttribute: String if let recordDataUserInfoName = attributeNamesFromUserInfo.privateRecordData { @@ -55,19 +42,6 @@ extension NSEntityDescription { } } - // Pubic Record ID - let publicRecordIDAttribute: String - if let recordIDUserInfoName = attributeNamesFromUserInfo.publicRecordID { - publicRecordIDAttribute = recordIDUserInfoName - } else { - // Last chance: try to find default attribute name in entity - if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNamePublicRecordID) { - publicRecordIDAttribute = CloudCore.config.defaultAttributeNamePublicRecordID - } else { - return nil - } - } - // Public Record Data let publicRecordDataAttribute: String if let recordDataUserInfoName = attributeNamesFromUserInfo.publicRecordData { @@ -84,19 +58,15 @@ extension NSEntityDescription { return ServiceAttributeNames(entityName: entityName, scopes: attributeNamesFromUserInfo.scopes, recordName: recordNameAttribute, - privateRecordID: privateRecordIDAttribute, privateRecordData: privateRecordDataAttribute, - publicRecordID: publicRecordIDAttribute, publicRecordData: publicRecordDataAttribute) } /// Parse data from User Info dictionary - private func parseAttributeNamesFromUserInfo() -> (scopes: [CKDatabase.Scope], recordName: String?, privateRecordID: String?, privateRecordData: String?, publicRecordID: String?, publicRecordData: String?) { + private func parseAttributeNamesFromUserInfo() -> (scopes: [CKDatabase.Scope], recordName: String?, privateRecordData: String?, publicRecordData: String?) { var scopes: [CKDatabase.Scope] = [] var recordNameAttribute: String? - var privateRecordIDAttribute: String? var privateRecordDataAttribute: String? - var publicRecordIDAttribute: String? var publicRecordDataAttribute: String? func parse(_ attributeName: String, _ userInfo: [AnyHashable: Any]) { @@ -107,9 +77,7 @@ extension NSEntityDescription { if key == ServiceAttributeNames.keyType { switch value { case ServiceAttributeNames.valueRecordName: recordNameAttribute = attributeName - case ServiceAttributeNames.valuePrivateRecordID: privateRecordIDAttribute = attributeName case ServiceAttributeNames.valuePrivateRecordData: privateRecordDataAttribute = attributeName - case ServiceAttributeNames.valuePublicRecordID: publicRecordIDAttribute = attributeName case ServiceAttributeNames.valuePublicRecordData: publicRecordDataAttribute = attributeName default: continue } @@ -139,7 +107,7 @@ extension NSEntityDescription { parse(attributeName, userInfo) } - return (scopes, recordNameAttribute, privateRecordIDAttribute, privateRecordDataAttribute, publicRecordIDAttribute, publicRecordDataAttribute) + return (scopes, recordNameAttribute, privateRecordDataAttribute, publicRecordDataAttribute) } } diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index e5d7fa9d..92cdbfb0 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -47,14 +47,12 @@ extension NSManagedObject { if scope == .public { let publicRecordID = CKRecord.ID(recordName: recordName!) let publicRecord = CKRecord(recordType: entityName, recordID:publicRecordID) - self.setValue(publicRecord.recordID.encodedString, forKey: serviceAttributeNames.publicRecordID) self.setValue(publicRecord.encdodedSystemFields, forKey: serviceAttributeNames.publicRecordData) aRecord = publicRecord } else { let privateRecordID = CKRecord.ID(recordName: recordName!, zoneID: CloudCore.config.zoneID) let privateRecord = CKRecord(recordType: entityName, recordID: privateRecordID) - self.setValue(privateRecord.recordID.encodedString, forKey: serviceAttributeNames.privateRecordID) self.setValue(privateRecord.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) aRecord = privateRecord diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 8743509c..93233ec0 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -53,21 +53,11 @@ public struct CloudCoreConfig { /// Default value is `recordName` public var defaultAttributeNameRecordName = "recordName" - /// Default entity's attribute name for *Private Record ID* if User Info is not specified. - /// - /// Default value is `privateRecordID` - public var defaultAttributeNamePrivateRecordID = "privateRecordID" - /// Default entity's attribute name for *Private Record Data* if User Info is not specified /// /// Default value is `privateRecordData` public var defaultAttributeNamePrivateRecordData = "privateRecordData" - - /// Default entity's attribute name for *Public Record ID* if User Info is not specified. - /// - /// Default value is `publicRecordID` - public var defaultAttributeNamePublicRecordID = "publicRecordID" - + /// Default entity's attribute name for *Public Record Data* if User Info is not specified /// /// Default value is `publicRecordData` diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index 36c247fd..f0cc803a 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -15,9 +15,7 @@ struct ServiceAttributeNames { static let keyScopes = "CloudCoreScopes" static let valueRecordName = "recordName" - static let valuePrivateRecordID = "privateRecordID" static let valuePrivateRecordData = "privateRecordData" - static let valuePublicRecordID = "publicRecordID" static let valuePublicRecordData = "publicRecordData" let entityName: String @@ -25,14 +23,12 @@ struct ServiceAttributeNames { let scopes: [CKDatabase.Scope] let recordName: String - let privateRecordID: String let privateRecordData: String - let publicRecordID: String let publicRecordData: String func contains(_ attributeName: String) -> Bool { switch attributeName { - case recordName, privateRecordID, privateRecordData, publicRecordID, publicRecordData: + case recordName, privateRecordData, publicRecordData: return true default: return false From e4a99637e66f89b244b155a20973a258edbb2d56 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 27 Nov 2018 22:41:41 -0800 Subject: [PATCH 032/203] remove unused CKRecordID extensions --- CloudCore.xcodeproj/project.pbxproj | 8 ----- Source/Extensions/CKRecordID.swift | 32 ------------------- .../Extensions/CKRecordIDTests.swift | 26 --------------- 3 files changed, 66 deletions(-) delete mode 100644 Source/Extensions/CKRecordID.swift delete mode 100644 Tests/CloudCoreTests/Extensions/CKRecordIDTests.swift diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index fb10e6de..1c5148f8 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -48,12 +48,10 @@ E24F44A61E4595B900F78819 /* CoreDataRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A51E4595B900F78819 /* CoreDataRelationship.swift */; }; E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */; }; E28F0B931E671E7400BF532A /* CKRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0B911E671E6500BF532A /* CKRecordTests.swift */; }; - E28F0B9F1E67245A00BF532A /* CKRecordIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0B9D1E67245600BF532A /* CKRecordIDTests.swift */; }; E28F0BA21E67260900BF532A /* NSEntityDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0BA01E6725E700BF532A /* NSEntityDescriptionTests.swift */; }; E28F0BA31E67280100BF532A /* NSManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */; }; E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2191E4334590020F5B6 /* CloudCoreConfig.swift */; }; E29BB21C1E43381D0020F5B6 /* CloudCoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB21B1E43381D0020F5B6 /* CloudCoreError.swift */; }; - E29BB21E1E433E050020F5B6 /* CKRecordID.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB21D1E433E050020F5B6 /* CKRecordID.swift */; }; E29BB2211E4344E80020F5B6 /* CKRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2201E4344E80020F5B6 /* CKRecord.swift */; }; E29BB2231E4346FF0020F5B6 /* NSManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2221E4346FF0020F5B6 /* NSManagedObject.swift */; }; E29BB22D1E436F310020F5B6 /* CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5B2E89F1C3A780C00C0327D /* CloudCore.framework */; }; @@ -143,11 +141,9 @@ E277DB061E7726FB00DC334A /* PublicDatabaseSubscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicDatabaseSubscriptions.swift; sourceTree = ""; }; E277DB0C1E77F96400DC334A /* FetchPublicSubscriptionsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPublicSubscriptionsOperation.swift; sourceTree = ""; }; E28F0B911E671E6500BF532A /* CKRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecordTests.swift; sourceTree = ""; }; - E28F0B9D1E67245600BF532A /* CKRecordIDTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecordIDTests.swift; sourceTree = ""; }; E28F0BA01E6725E700BF532A /* NSEntityDescriptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEntityDescriptionTests.swift; sourceTree = ""; }; E29BB2191E4334590020F5B6 /* CloudCoreConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreConfig.swift; sourceTree = ""; }; E29BB21B1E43381D0020F5B6 /* CloudCoreError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreError.swift; sourceTree = ""; }; - E29BB21D1E433E050020F5B6 /* CKRecordID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecordID.swift; sourceTree = ""; }; E29BB2201E4344E80020F5B6 /* CKRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecord.swift; sourceTree = ""; }; E29BB2221E4346FF0020F5B6 /* NSManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObject.swift; sourceTree = ""; }; E29BB2281E436F310020F5B6 /* CloudCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CloudCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -432,7 +428,6 @@ E28F0B9C1E67244A00BF532A /* Extensions */ = { isa = PBXGroup; children = ( - E28F0B9D1E67245600BF532A /* CKRecordIDTests.swift */, E28F0BA01E6725E700BF532A /* NSEntityDescriptionTests.swift */, E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */, ); @@ -442,7 +437,6 @@ E29BB21F1E433FDA0020F5B6 /* Extensions */ = { isa = PBXGroup; children = ( - E29BB21D1E433E050020F5B6 /* CKRecordID.swift */, D985DEAD1FE034A900236870 /* NSManagedObjectModel.swift */, E2D390071E4A49350019BBCD /* NSEntityDescription.swift */, E29BB2221E4346FF0020F5B6 /* NSManagedObject.swift */, @@ -792,7 +786,6 @@ E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */, E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */, E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */, - E29BB21E1E433E050020F5B6 /* CKRecordID.swift in Sources */, D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */, E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */, E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */, @@ -856,7 +849,6 @@ E29D117A1E69813F00E3DCBF /* CoreDataAttributeTests.swift in Sources */, E28F0B931E671E7400BF532A /* CKRecordTests.swift in Sources */, E29BB2351E436F720020F5B6 /* model.xcdatamodeld in Sources */, - E28F0B9F1E67245A00BF532A /* CKRecordIDTests.swift in Sources */, E247EF971E67873E00EBD75E /* DeleteFromCoreDataOperationTests.swift in Sources */, E29D117D1E69A47700E3DCBF /* CoreDataRelationshipTests.swift in Sources */, D9B3C7391FCF0C9E00CDB7FF /* CorrectObject.swift in Sources */, diff --git a/Source/Extensions/CKRecordID.swift b/Source/Extensions/CKRecordID.swift deleted file mode 100644 index 0fbac032..00000000 --- a/Source/Extensions/CKRecordID.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// CloudRecordID.swift -// CloudCore -// -// Created by Vasily Ulianov on 02.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import CloudKit - -extension CKRecord.ID { - private static let separator = "|" - - /// Init from encoded string - /// - /// - Parameter encodedString: format: `recordName|ownerName` - convenience init?(encodedString: String) { - let separated = encodedString.components(separatedBy: CKRecord.ID.separator) - - if separated.count == 2 { - let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneID.zoneName, ownerName: separated[1]) - self.init(recordName: separated[0], zoneID: zoneID) - } else { - return nil - } - } - - /// Encoded string in format: `recordName|ownerName` - var encodedString: String { - return recordName + CKRecord.ID.separator + zoneID.ownerName - } -} diff --git a/Tests/CloudCoreTests/Extensions/CKRecordIDTests.swift b/Tests/CloudCoreTests/Extensions/CKRecordIDTests.swift deleted file mode 100644 index 7ca18910..00000000 --- a/Tests/CloudCoreTests/Extensions/CKRecordIDTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// CKRecordID.swift -// CloudCore -// -// Created by Vasily Ulianov on 01.03.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import XCTest -import CloudKit - -@testable import CloudCore - -class CKRecordIDTests: XCTestCase { - func testRecordIDEncodeDecode() { - let zoneID = CKRecordZoneID(zoneName: CloudCore.config.zoneID.zoneName, ownerName: CKCurrentUserDefaultName) - let recordID = CKRecordID(recordName: "testName", zoneID: zoneID) - - let encodedString = recordID.encodedString - let restoredRecordID = CKRecordID(encodedString: encodedString) - - XCTAssertEqual(recordID.recordName, restoredRecordID?.recordName) - XCTAssertEqual(recordID.zoneID, restoredRecordID?.zoneID) - - } -} From 79e7f36e46d8fbe4f7b14f5d7acb4e4c3639139a Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 28 Nov 2018 12:11:33 -0800 Subject: [PATCH 033/203] add ServiceAttributeNames.ownerName --- .../RecordToCoreDataOperation.swift | 1 + Source/Extensions/NSEntityDescription.swift | 20 +++++++++++++++++-- Source/Extensions/NSManagedObject.swift | 5 +++++ Source/Model/CloudCoreConfig.swift | 5 +++++ Source/Model/ServiceAttributeName.swift | 4 +++- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index e757172d..6a25aadc 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -102,6 +102,7 @@ class RecordToCoreDataOperation: AsynchronousOperation { // Set system headers object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName) + object.setValue(record.recordID.zoneID.ownerName, forKey: serviceAttributeNames.ownerName) if record.recordID.zoneID == CloudCore.config.zoneID { object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) } else { diff --git a/Source/Extensions/NSEntityDescription.swift b/Source/Extensions/NSEntityDescription.swift index 946680bc..5dff9016 100644 --- a/Source/Extensions/NSEntityDescription.swift +++ b/Source/Extensions/NSEntityDescription.swift @@ -29,6 +29,19 @@ extension NSEntityDescription { } } + // Owner Name + let ownerNameAttribute: String + if let ownerNameUserInfoName = attributeNamesFromUserInfo.ownerName { + ownerNameAttribute = ownerNameUserInfoName + } else { + // Last chance: try to find default attribute name in entity + if self.attributesByName.keys.contains(CloudCore.config.defaultAttributeNameOwnerName) { + ownerNameAttribute = CloudCore.config.defaultAttributeNameOwnerName + } else { + return nil + } + } + // Private Record Data let privateRecordDataAttribute: String if let recordDataUserInfoName = attributeNamesFromUserInfo.privateRecordData { @@ -58,14 +71,16 @@ extension NSEntityDescription { return ServiceAttributeNames(entityName: entityName, scopes: attributeNamesFromUserInfo.scopes, recordName: recordNameAttribute, + ownerName: ownerNameAttribute, privateRecordData: privateRecordDataAttribute, publicRecordData: publicRecordDataAttribute) } /// Parse data from User Info dictionary - private func parseAttributeNamesFromUserInfo() -> (scopes: [CKDatabase.Scope], recordName: String?, privateRecordData: String?, publicRecordData: String?) { + private func parseAttributeNamesFromUserInfo() -> (scopes: [CKDatabase.Scope], recordName: String?, ownerName: String?, privateRecordData: String?, publicRecordData: String?) { var scopes: [CKDatabase.Scope] = [] var recordNameAttribute: String? + var ownerNameAttribute: String? var privateRecordDataAttribute: String? var publicRecordDataAttribute: String? @@ -77,6 +92,7 @@ extension NSEntityDescription { if key == ServiceAttributeNames.keyType { switch value { case ServiceAttributeNames.valueRecordName: recordNameAttribute = attributeName + case ServiceAttributeNames.valueOwnerName: ownerNameAttribute = attributeName case ServiceAttributeNames.valuePrivateRecordData: privateRecordDataAttribute = attributeName case ServiceAttributeNames.valuePublicRecordData: publicRecordDataAttribute = attributeName default: continue @@ -107,7 +123,7 @@ extension NSEntityDescription { parse(attributeName, userInfo) } - return (scopes, recordNameAttribute, privateRecordDataAttribute, publicRecordDataAttribute) + return (scopes, recordNameAttribute, ownerNameAttribute, privateRecordDataAttribute, publicRecordDataAttribute) } } diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index 92cdbfb0..364b0db9 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -58,6 +58,11 @@ extension NSManagedObject { aRecord = privateRecord } + let ownerName = self.value(forKey: serviceAttributeNames.ownerName) as? String + if ownerName == nil { + self.setValue(aRecord.recordID.zoneID.ownerName, forKey: serviceAttributeNames.ownerName) + } + return aRecord } } diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 93233ec0..837f56b4 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -53,6 +53,11 @@ public struct CloudCoreConfig { /// Default value is `recordName` public var defaultAttributeNameRecordName = "recordName" + /// Default entity's attribute name for *Owner Name* if User Info is not specified + /// + /// Default value is `recordName` + public var defaultAttributeNameOwnerName = "ownerName" + /// Default entity's attribute name for *Private Record Data* if User Info is not specified /// /// Default value is `privateRecordData` diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index f0cc803a..043bd1e4 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -15,6 +15,7 @@ struct ServiceAttributeNames { static let keyScopes = "CloudCoreScopes" static let valueRecordName = "recordName" + static let valueOwnerName = "ownerName" static let valuePrivateRecordData = "privateRecordData" static let valuePublicRecordData = "publicRecordData" @@ -23,12 +24,13 @@ struct ServiceAttributeNames { let scopes: [CKDatabase.Scope] let recordName: String + let ownerName: String let privateRecordData: String let publicRecordData: String func contains(_ attributeName: String) -> Bool { switch attributeName { - case recordName, privateRecordData, publicRecordData: + case recordName, ownerName, privateRecordData, publicRecordData: return true default: return false From d7fe3179087ab504b04ceaf328150b0ebe869b3e Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 29 Nov 2018 10:20:37 -0800 Subject: [PATCH 034/203] properly establish parent-child relationships --- .../ObjectToRecordOperation.swift | 8 +++++ Source/Classes/Setup/SubscribeOperation.swift | 35 ++++++++++++------- Source/Model/ServiceAttributeName.swift | 1 + 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 783a559d..eba91859 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -71,6 +71,14 @@ class ObjectToRecordOperation: Operation { } else if let relationship = CoreDataRelationship(scope: scope, value: value, relationshipName: attributeName, entity: managedObject.entity) { let references = try relationship.makeRecordValue() record.setValue(references, forKey: attributeName) + + if let parentRef = references as? CKRecord.Reference, + parentRef.recordID.zoneID == CloudCore.config.zoneID, + let parentAttributeName = managedObject.entity.userInfo?[ServiceAttributeNames.keyParent] as? String, + parentAttributeName == attributeName + { + record.setParent(parentRef.recordID) + } } } } diff --git a/Source/Classes/Setup/SubscribeOperation.swift b/Source/Classes/Setup/SubscribeOperation.swift index f87cef4c..81280de0 100644 --- a/Source/Classes/Setup/SubscribeOperation.swift +++ b/Source/Classes/Setup/SubscribeOperation.swift @@ -22,33 +22,42 @@ class SubscribeOperation: AsynchronousOperation { let container = CloudCore.config.container - // Subscribe operation let subcribeToPrivate = self.makeRecordZoneSubscriptionOperation(for: container.privateCloudDatabase, id: CloudCore.config.subscriptionIDForPrivateDB) - - // Fetch subscriptions and cancel subscription operation if subscription is already exists - let fetchPrivateSubscriptions = makeFetchSubscriptionOperation(for: container.privateCloudDatabase, + let fetchPrivateSubscription = makeFetchSubscriptionOperation(for: container.privateCloudDatabase, searchForSubscriptionID: CloudCore.config.subscriptionIDForPrivateDB, operationToCancelIfSubcriptionExists: subcribeToPrivate) + subcribeToPrivate.addDependency(fetchPrivateSubscription) - subcribeToPrivate.addDependency(fetchPrivateSubscriptions) - + let subscribeToShared = self.makeRecordZoneSubscriptionOperation(for: container.sharedCloudDatabase, id: CloudCore.config.subscriptionIDForSharedDB) + let fetchSharedSubscription = makeFetchSubscriptionOperation(for: container.sharedCloudDatabase, + searchForSubscriptionID: CloudCore.config.subscriptionIDForSharedDB, + operationToCancelIfSubcriptionExists: subscribeToShared) + subscribeToShared.addDependency(fetchSharedSubscription) + // Finish operation let finishOperation = BlockOperation { self.state = .finished } - finishOperation.addDependency(subcribeToPrivate) - finishOperation.addDependency(fetchPrivateSubscriptions) - - queue.addOperations([subcribeToPrivate, fetchPrivateSubscriptions, finishOperation], waitUntilFinished: false) + finishOperation.addDependency(subcribeToPrivate) + finishOperation.addDependency(fetchPrivateSubscription) + finishOperation.addDependency(subscribeToShared) + finishOperation.addDependency(fetchSharedSubscription) + + queue.addOperations([subcribeToPrivate, + fetchPrivateSubscription, + subscribeToShared, + fetchSharedSubscription, + finishOperation], waitUntilFinished: false) } private func makeRecordZoneSubscriptionOperation(for database: CKDatabase, id: String) -> CKModifySubscriptionsOperation { let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.shouldSendContentAvailable = true - let subscription = CKRecordZoneSubscription(zoneID: CloudCore.config.zoneID, subscriptionID: id) - subscription.notificationInfo = notificationInfo - + let subscription = (database == CloudCore.config.container.sharedCloudDatabase) ?CKDatabaseSubscription(subscriptionID: id) : + CKRecordZoneSubscription(zoneID: CloudCore.config.zoneID, subscriptionID: id) + subscription.notificationInfo = notificationInfo + let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) operation.modifySubscriptionsCompletionBlock = { if let error = $2 { diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index 043bd1e4..2792858f 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -13,6 +13,7 @@ struct ServiceAttributeNames { // User Info keys & values static let keyType = "CloudCoreType" static let keyScopes = "CloudCoreScopes" + static let keyParent = "CloudCoreParent" static let valueRecordName = "recordName" static let valueOwnerName = "ownerName" From ddabe380eb7cf3904f1e756b233133d8c21abb64 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 29 Nov 2018 16:03:14 -0800 Subject: [PATCH 035/203] making some extension funcs public, for sharing --- Source/Extensions/NSManagedObject.swift | 2 +- Source/Model/CKRecord.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index 364b0db9..86bb5abc 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -14,7 +14,7 @@ extension NSManagedObject { /// /// - Returns: unacrhived `CKRecord` containing restored system fields (like RecordID, tokens, creationg date etc) /// - Throws: `CloudCoreError.missingServiceAttributes` if names of CloudCore attributes are not specified in User Info - func restoreRecordWithSystemFields(for scope: CKDatabase.Scope) throws -> CKRecord? { + public func restoreRecordWithSystemFields(for scope: CKDatabase.Scope) throws -> CKRecord? { guard let serviceAttributeNames = self.entity.serviceAttributeNames else { throw CloudCoreError.missingServiceAttributes(entityName: self.entity.name) } diff --git a/Source/Model/CKRecord.swift b/Source/Model/CKRecord.swift index 5894f9d7..e4770137 100644 --- a/Source/Model/CKRecord.swift +++ b/Source/Model/CKRecord.swift @@ -8,7 +8,7 @@ import CloudKit -extension CKRecord { +public extension CKRecord { convenience init?(archivedData: Data) { let unarchiver = NSKeyedUnarchiver(forReadingWith: archivedData) unarchiver.requiresSecureCoding = true From 99d34af50fc9ec6cc06260b42a13b14bdc01eeff Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 29 Nov 2018 17:25:51 -0800 Subject: [PATCH 036/203] support for (shared) database changes --- Source/Classes/Pull/PullOperation.swift | 20 ++++++++++++++++--- .../FetchRecordZoneChangesOperation.swift | 4 ++-- .../RecordToCoreDataOperation.swift | 6 +++--- Source/Model/Tokens.swift | 16 +++++++++------ 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index f5f05000..92472585 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -15,8 +15,8 @@ public class PullOperation: Operation { /// Private cloud database for the CKContainer specified by CloudCoreConfig public static let allDatabases = [ // CloudCore.config.container.publicCloudDatabase, - CloudCore.config.container.privateCloudDatabase -// CloudCore.config.container.sharedCloudDatabase + CloudCore.config.container.privateCloudDatabase, + CloudCore.config.container.sharedCloudDatabase ] public typealias NotificationUserInfo = [AnyHashable : Any] @@ -54,7 +54,21 @@ public class PullOperation: Operation { backgroundContext.name = CloudCore.config.pullContextName for database in self.databases { - self.addRecordZoneChangesOperation(recordZoneIDs: [CloudCore.config.zoneID], database: database, context: backgroundContext) + var changedZoneIDs = [CKRecordZone.ID]() + let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope] + let databaseChangeOp = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) + databaseChangeOp.database = database + databaseChangeOp.recordZoneWithIDChangedBlock = { (recordZoneID) in + changedZoneIDs.append(recordZoneID) + } + databaseChangeOp.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in + // TODO: error handling? + + self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) + + self.tokens.tokensByDatabaseScope[database.databaseScope] = changeToken + } + self.queue.addOperation(databaseChangeOp) } self.queue.waitUntilAllOperationsAreFinished() diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index 4d97d152..d0923c32 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -44,7 +44,7 @@ class FetchRecordZoneChangesOperation: Operation { super.main() let fetchOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) - self.fetchQueue.addOperation(fetchOperation) + fetchQueue.addOperation(fetchOperation) fetchQueue.waitUntilAllOperationsAreFinished() } @@ -52,7 +52,7 @@ class FetchRecordZoneChangesOperation: Operation { private func makeFetchOperation(optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]) -> CKFetchRecordZoneChangesOperation { // Init Fetch Operation let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, optionsByRecordZoneID: optionsByRecordZoneID) - + fetchOperation.recordChangedBlock = { self.recordChangedBlock?($0) } diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 6a25aadc..c1767d85 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -103,10 +103,10 @@ class RecordToCoreDataOperation: AsynchronousOperation { // Set system headers object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName) object.setValue(record.recordID.zoneID.ownerName, forKey: serviceAttributeNames.ownerName) - if record.recordID.zoneID == CloudCore.config.zoneID { - object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) - } else { + if record.recordID.zoneID == CKRecordZone.default().zoneID { object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.publicRecordData) + } else { + object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) } } } diff --git a/Source/Model/Tokens.swift b/Source/Model/Tokens.swift index 029cc574..dcf650c9 100644 --- a/Source/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -30,9 +30,11 @@ import CloudKit open class Tokens: NSObject, NSCoding { var tokensByRecordZoneID = [CKRecordZone.ID: CKServerChangeToken]() + var tokensByDatabaseScope = [CKDatabase.Scope: CKServerChangeToken]() private struct ArchiverKey { - static let tokensByRecordZoneID = "tokensByRecordZoneID" + static let tokensByRecordZoneID = "tokensByRecordZoneID" + static let tokensByDatabaseScope = "tokensByDatabaseScope" } /// Create fresh object without any Tokens inside. Can be used to fetch full data. @@ -65,16 +67,18 @@ open class Tokens: NSObject, NSCoding { /// Returns an object initialized from data in a given unarchiver. public required init?(coder aDecoder: NSCoder) { - if let decodedTokens = aDecoder.decodeObject(forKey: ArchiverKey.tokensByRecordZoneID) as? [CKRecordZone.ID: CKServerChangeToken] { - self.tokensByRecordZoneID = decodedTokens - } else { - return nil + if let decodedTokensByZone = aDecoder.decodeObject(forKey: ArchiverKey.tokensByRecordZoneID) as? [CKRecordZone.ID: CKServerChangeToken] { + self.tokensByRecordZoneID = decodedTokensByZone } + if let decodedTokensByScope = aDecoder.decodeObject(forKey: ArchiverKey.tokensByDatabaseScope) as? [CKDatabase.Scope: CKServerChangeToken] { + self.tokensByDatabaseScope = decodedTokensByScope + } } /// Encodes the receiver using a given archiver. open func encode(with aCoder: NSCoder) { - aCoder.encode(tokensByRecordZoneID, forKey: ArchiverKey.tokensByRecordZoneID) + aCoder.encode(tokensByRecordZoneID, forKey: ArchiverKey.tokensByRecordZoneID) + aCoder.encode(tokensByDatabaseScope, forKey: ArchiverKey.tokensByDatabaseScope) } } From 4045c0268bbd16c8c6eeea2e0f12b6d3874b3085 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 2 Dec 2018 13:27:04 -0800 Subject: [PATCH 037/203] implement recordZoneWithIDWasDeletedBlock other user stopped sharing something, so clear it out locally --- Source/Classes/Pull/PullOperation.swift | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 92472585..1d9c2026 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -55,16 +55,21 @@ public class PullOperation: Operation { for database in self.databases { var changedZoneIDs = [CKRecordZone.ID]() + var deletedZoneIDs = [CKRecordZone.ID]() let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope] let databaseChangeOp = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) databaseChangeOp.database = database databaseChangeOp.recordZoneWithIDChangedBlock = { (recordZoneID) in changedZoneIDs.append(recordZoneID) } + databaseChangeOp.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in + deletedZoneIDs.append(recordZoneID) + } databaseChangeOp.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in // TODO: error handling? self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) + self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) self.tokens.tokensByDatabaseScope[database.databaseScope] = changeToken } @@ -150,6 +155,33 @@ public class PullOperation: Operation { queue.addOperation(recordZoneChangesOperation) } + + private func deleteRecordsFromDeletedZones(recordZoneIDs: [CKRecordZone.ID]) { + persistentContainer.performBackgroundTask { (moc) in + for entity in self.persistentContainer.managedObjectModel.entities { + if let serviceAttributes = entity.serviceAttributeNames { + for recordZoneID in recordZoneIDs { + do { + let request = NSFetchRequest(entityName: entity.name!) + request.predicate = NSPredicate(format: "%K == %@", serviceAttributes.ownerName, recordZoneID.ownerName) + let results = try moc.fetch(request) as! [NSManagedObject] + for object in results { + moc.delete(object) + } + } catch { + print("Unexpected error: \(error).") + } + } + } + } + + do { + try moc.save() + } catch { + print("Unexpected error: \(error).") + } + } + } private func handle(recordZoneChangesError: Error, in zoneId: CKRecordZone.ID, database: CKDatabase, context: NSManagedObjectContext) { guard let cloudError = recordZoneChangesError as? CKError else { From 3f465e35b3fc523ce7d721294bdf91abf39eeb6e Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 2 Dec 2018 13:27:52 -0800 Subject: [PATCH 038/203] more resilience when error during fetchHistory --- Source/Classes/Push/CoreDataObserver.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 05341715..407ac0a8 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -253,11 +253,11 @@ class CoreDataObserver { if let history = historyResult.result as? [NSPersistentHistoryTransaction] { for transaction in history { if process(transaction, in: moc) { - let data = NSKeyedArchiver.archivedData(withRootObject: transaction.token) - settings.set(data, forKey: key) - let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) try moc.execute(deleteRequest) + + let data = NSKeyedArchiver.archivedData(withRootObject: transaction.token) + settings.set(data, forKey: key) } else { break } @@ -265,7 +265,12 @@ class CoreDataObserver { } } catch { let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + switch nserror.code { + case NSPersistentHistoryTokenExpiredError: + settings.set(nil, forKey: key) + default: + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } } } } From aa97c371fd0b3b5cebf3db3b48814b5f8777dd87 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 2 Dec 2018 15:33:29 -0800 Subject: [PATCH 039/203] update test cases to newer APIs --- .../DeleteFromCoreDataOperationTests.swift | 6 +-- .../RecordToCoreDataOperationTests.swift | 2 +- .../CoreDataRelationshipTests.swift | 14 +++--- .../ObjectToRecordOperationTests.swift | 10 ++-- .../Extensions/NSEntityDescriptionTests.swift | 5 +- .../Extensions/NSManagedObjectTests.swift | 11 +++-- .../model.xcdatamodel/contents | 48 ++++++++++++++----- Tests/Shared/CorrectObject.swift | 4 +- 8 files changed, 64 insertions(+), 36 deletions(-) diff --git a/Tests/CloudCoreTests/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift b/Tests/CloudCoreTests/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift index bb4c2bb7..1a6ad026 100644 --- a/Tests/CloudCoreTests/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift +++ b/Tests/CloudCoreTests/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift @@ -19,10 +19,10 @@ class DeleteFromCoreDataOperationTests: CoreDataTestCase { func testOperation() { let remainingObject = TestEntity(context: context) do { - try remainingObject.setRecordInformation() + try remainingObject.setRecordInformation(for: .private) let objectToDelete = TestEntity(context: context) - let record = try objectToDelete.setRecordInformation() + let record = try objectToDelete.setRecordInformation(for: .private) try context.save() @@ -65,7 +65,7 @@ class DeleteFromCoreDataOperationTests: CoreDataTestCase { for _ in 1...300 { let objectToDelete = TestEntity(context: context) do { - let record = try objectToDelete.setRecordInformation() + let record = try objectToDelete.setRecordInformation(for: .private) recordsToDelete.append(record) } catch { XCTFail(error) diff --git a/Tests/CloudCoreTests/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift b/Tests/CloudCoreTests/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift index 38b4633a..252ea62e 100644 --- a/Tests/CloudCoreTests/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift +++ b/Tests/CloudCoreTests/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift @@ -64,7 +64,7 @@ class RecordToCoreDataOperationTests: CoreDataTestCase { context.performAndWait { // Check operation results let fetchRequest: NSFetchRequest = TestEntity.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "recordID = %@", record.recordID.encodedString) + fetchRequest.predicate = NSPredicate(format: "recordName = %@", record.recordID.recordName) do { guard let managedObject = try context.fetch(fetchRequest).first else { XCTFail("Couldn't find converted object") diff --git a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift index 230ee289..7f3e155c 100644 --- a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift +++ b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift @@ -14,23 +14,23 @@ import CloudKit class CoreDataRelationshipTests: CoreDataTestCase { func testInitWithAttribute() { - let relationship = CoreDataRelationship(value: "attribute", relationshipName: "string", entity: TestEntity.entity()) + let relationship = CoreDataRelationship(scope: .private, value: "attribute", relationshipName: "string", entity: TestEntity.entity()) XCTAssertNil(relationship, "Expected nil because it is attribute, not relationship") } func testMakeRecordValues() { // Generate test model let object = TestEntity(context: context) - try! object.setRecordInformation() - let filledObjectRecord = try! object.restoreRecordWithSystemFields()! + try! object.setRecordInformation(for: .private) + let filledObjectRecord = try! object.restoreRecordWithSystemFields(for: .private)! var manyUsers = [UserEntity]() var manyUsersRecordsIDs = [CKRecordID]() for _ in 0...2 { let user = UserEntity(context: context) - try! user.setRecordInformation() - let userRecord = try! user.restoreRecordWithSystemFields()! - user.recordData = userRecord.encdodedSystemFields + try! user.setRecordInformation(for: .private) + let userRecord = try! user.restoreRecordWithSystemFields(for: .private)! + user.privateRecordData = userRecord.encdodedSystemFields manyUsers.append(user) manyUsersRecordsIDs.append(userRecord.recordID) @@ -42,7 +42,7 @@ class CoreDataRelationshipTests: CoreDataTestCase { // Fill testable CKRecord for name in object.entity.relationshipsByName.keys { let managedObjectValue = object.value(forKey: name)! - guard let relationship = CoreDataRelationship(value: managedObjectValue, relationshipName: name, entity: object.entity) else { + guard let relationship = CoreDataRelationship(scope: .private, value: managedObjectValue, relationshipName: name, entity: object.entity) else { XCTFail("Failed to initialize CoreDataRelationship with attribute: \(name)") continue } diff --git a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift index d68ca432..39c0ef2c 100644 --- a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift +++ b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift @@ -16,7 +16,7 @@ class ObjectToRecordOperationTests: CoreDataTestCase { func createTestObject(in context: NSManagedObjectContext) -> (TestEntity, CKRecord) { let managedObject = CorrectObject().insert(in: context) - let record = try! managedObject.setRecordInformation() + let record = try! managedObject.setRecordInformation(for: .private) XCTAssertNil(record.value(forKey: "string")) return (managedObject, record) @@ -24,7 +24,7 @@ class ObjectToRecordOperationTests: CoreDataTestCase { func testGoodOperation() { let (managedObject, record) = createTestObject(in: context) - let operation = ObjectToRecordOperation(record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) + let operation = ObjectToRecordOperation(scope: .private, record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) let conversionExpectation = expectation(description: "ConversionCompleted") operation.errorCompletionBlock = { XCTFail($0) } @@ -40,7 +40,7 @@ class ObjectToRecordOperationTests: CoreDataTestCase { func testContextIsNotDefined() { let record = createTestObject(in: context).1 - let operation = ObjectToRecordOperation(record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) + let operation = ObjectToRecordOperation(scope: .private, record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) let errorExpectation = expectation(description: "ErrorCalled") operation.errorCompletionBlock = { error in @@ -62,7 +62,7 @@ class ObjectToRecordOperationTests: CoreDataTestCase { let record = CorrectObject().makeRecord() let _ = TestEntity(context: context) - let operation = ObjectToRecordOperation(record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) + let operation = ObjectToRecordOperation(scope: .private, record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) operation.parentContext = self.context let errorExpectation = expectation(description: "ErrorCalled") @@ -96,7 +96,7 @@ class ObjectToRecordOperationTests: CoreDataTestCase { let queue = OperationQueue() for record in records { - let operation = ObjectToRecordOperation(record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) + let operation = ObjectToRecordOperation(scope: .private, record: record, changedAttributes: nil, serviceAttributeNames: TestEntity.entity().serviceAttributeNames!) operation.errorCompletionBlock = { XCTFail($0) } operation.parentContext = backgroundContext queue.addOperation(operation) diff --git a/Tests/CloudCoreTests/Extensions/NSEntityDescriptionTests.swift b/Tests/CloudCoreTests/Extensions/NSEntityDescriptionTests.swift index 7264cc01..1e21947c 100644 --- a/Tests/CloudCoreTests/Extensions/NSEntityDescriptionTests.swift +++ b/Tests/CloudCoreTests/Extensions/NSEntityDescriptionTests.swift @@ -17,8 +17,9 @@ class NSEntityDescriptionTests: CoreDataTestCase { let attributeNames = correctObject.entity.serviceAttributeNames XCTAssertEqual(attributeNames?.entityName, "TestEntity") - XCTAssertEqual(attributeNames?.recordData, "recordData") - XCTAssertEqual(attributeNames?.recordID, "recordID") + XCTAssertEqual(attributeNames?.publicRecordData, "publicRecordData") + XCTAssertEqual(attributeNames?.privateRecordData, "privateRecordData") + XCTAssertEqual(attributeNames?.recordName, "recordName") let incorrectObject = IncorrectEntity(context: self.context) XCTAssertNil(incorrectObject.entity.serviceAttributeNames) diff --git a/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift b/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift index e972c018..dd5600d5 100644 --- a/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift +++ b/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift @@ -13,12 +13,13 @@ import CloudKit @testable import CloudCore class NSManagedObjectTests: CoreDataTestCase { + func testRestoreRecordWithSystemFields() { let object = TestEntity(context: context) do { - try object.setRecordInformation() + try object.setRecordInformation(for: .private) - let record = try object.restoreRecordWithSystemFields() + let record = try object.restoreRecordWithSystemFields(for: .private) XCTAssertEqual(record?.recordType, "TestEntity") XCTAssertEqual(record?.recordID.zoneID, CloudCore.config.zoneID) } catch { @@ -30,7 +31,7 @@ class NSManagedObjectTests: CoreDataTestCase { func testRestoreObjectWithoutData() { let object = TestEntity(context: context) do { - let record = try object.restoreRecordWithSystemFields() + let record = try object.restoreRecordWithSystemFields(for: .private) XCTAssertNil(record) } catch { XCTFail("\(error)") @@ -42,12 +43,12 @@ class NSManagedObjectTests: CoreDataTestCase { func testSetRecordInformationThrow() { let object = IncorrectEntity(context: context) - XCTAssertThrowsSpecific(try object.setRecordInformation(), CloudCoreError.missingServiceAttributes(entityName: "IncorrectEntity")) + XCTAssertThrowsSpecific(try object.setRecordInformation(for: .private), CloudCoreError.missingServiceAttributes(entityName: "IncorrectEntity")) } func testRestoreRecordThrow() { let object = IncorrectEntity(context: context) - XCTAssertThrowsSpecific(try object.restoreRecordWithSystemFields(), CloudCoreError.missingServiceAttributes(entityName: "IncorrectEntity")) + XCTAssertThrowsSpecific(try object.restoreRecordWithSystemFields(for: .private), CloudCoreError.missingServiceAttributes(entityName: "IncorrectEntity")) } } diff --git a/Tests/CloudCoreTests/model.xcdatamodeld/model.xcdatamodel/contents b/Tests/CloudCoreTests/model.xcdatamodeld/model.xcdatamodel/contents index 3b6591ce..8ead8015 100644 --- a/Tests/CloudCoreTests/model.xcdatamodeld/model.xcdatamodel/contents +++ b/Tests/CloudCoreTests/model.xcdatamodeld/model.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -13,39 +13,65 @@ - + - + - + - + + + + + + + + + + + + + + - + + + + + + + + + + + - + - + - + + + + - - + + \ No newline at end of file diff --git a/Tests/Shared/CorrectObject.swift b/Tests/Shared/CorrectObject.swift index 6694acd3..5c96ae15 100644 --- a/Tests/Shared/CorrectObject.swift +++ b/Tests/Shared/CorrectObject.swift @@ -38,7 +38,7 @@ struct CorrectObject { let managedObject = TestEntity(context: context) // Header - managedObject.recordData = self.recordData as Data + managedObject.privateRecordData = self.recordData as Data // Binary managedObject.binary = binary as Data @@ -100,7 +100,7 @@ struct CorrectObject { func assertEqualAttributes(_ managedObject: TestEntity, _ record: CKRecord) { // Headers - if let encodedRecordData = managedObject.recordData as Data? { + if let encodedRecordData = managedObject.privateRecordData as Data? { let recordFromObject = CKRecord(archivedData: encodedRecordData) XCTAssertEqual(recordFromObject?.recordID, record.recordID) From af07b490be8e2e04af2e854325280c5295145de1 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 2 Dec 2018 18:21:46 -0800 Subject: [PATCH 040/203] update Example to latest CloudCore --- .../project.pbxproj | 145 ++---------------- .../xcschemes/CloudCoreExample.xcscheme | 4 +- Example/Podfile | 4 +- .../Model.xcdatamodel/contents | 41 ++--- Example/Sources/AppDelegate.swift | 7 +- .../Class/FRCTableViewDataSource.swift | 1 - Example/Sources/Class/ModelFactory.swift | 2 +- .../DetailViewController.swift | 93 ++++++----- .../MasterViewController.swift | 64 ++++---- 9 files changed, 142 insertions(+), 219 deletions(-) diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index fcccde60..df9eee02 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -12,8 +12,6 @@ D974381D1FE16E6E00650541 /* ModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D974381C1FE16E6E00650541 /* ModelFactory.swift */; }; D974381F1FE18ED100650541 /* NotificationsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D974381E1FE18ED100650541 /* NotificationsObserver.swift */; }; D97438231FE199F500650541 /* EmployeeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97438221FE199F500650541 /* EmployeeTableViewCell.swift */; }; - E23BE70C1EA4FD78008F4F23 /* CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */; }; - E23BE70D1EA4FD78008F4F23 /* CloudCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E261E0581EAFEA8A00F1CA61 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E261E0571EAFEA8A00F1CA61 /* CloudKit.framework */; }; E2C3E3541E53299800A733BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C3E3531E53299800A733BF /* AppDelegate.swift */; }; E2C3E3571E53299800A733BF /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E2C3E3551E53299800A733BF /* Model.xcdatamodeld */; }; @@ -24,44 +22,6 @@ E2C3E3631E53299800A733BF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2C3E3611E53299800A733BF /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - D9DC6DC01FDFEFF100017652 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = D9B3C6F31FCEF38D00CDB7FF; - remoteInfo = TestableApp; - }; - D9DC6DC21FDFEFF100017652 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = D9B3C71C1FCEF96D00CDB7FF; - remoteInfo = CloudKitTests; - }; - E23BE6FA1EA4CC1C008F4F23 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = D5B2E89F1C3A780C00C0327D; - remoteInfo = "CloudCore-iOS"; - }; - E23BE6FE1EA4CC1C008F4F23 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = E29BB2281E436F310020F5B6; - remoteInfo = "CloudCoreTests-iOS"; - }; - E23BE70E1EA4FD78008F4F23 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = D5B2E89E1C3A780C00C0327D; - remoteInfo = "CloudCore-iOS"; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ E23BE7101EA4FD78008F4F23 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -69,7 +29,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - E23BE70D1EA4FD78008F4F23 /* CloudCore.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -83,7 +42,6 @@ D974381C1FE16E6E00650541 /* ModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelFactory.swift; sourceTree = ""; }; D974381E1FE18ED100650541 /* NotificationsObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsObserver.swift; sourceTree = ""; }; D97438221FE199F500650541 /* EmployeeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmployeeTableViewCell.swift; sourceTree = ""; }; - E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CloudCore.xcodeproj; path = ../CloudCore.xcodeproj; sourceTree = ""; }; E261E0571EAFEA8A00F1CA61 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; E2C3E3501E53299800A733BF /* CloudCoreExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CloudCoreExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; E2C3E3531E53299800A733BF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -103,7 +61,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E23BE70C1EA4FD78008F4F23 /* CloudCore.framework in Frameworks */, E261E0581EAFEA8A00F1CA61 /* CloudKit.framework in Frameworks */, B4532A37427BB629A3A47821 /* Pods_CloudCoreExample.framework in Frameworks */, ); @@ -148,17 +105,6 @@ path = View; sourceTree = ""; }; - E23BE6F41EA4CC1C008F4F23 /* Products */ = { - isa = PBXGroup; - children = ( - E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */, - E23BE6FF1EA4CC1C008F4F23 /* CloudCoreTests.xctest */, - D9DC6DC11FDFEFF100017652 /* TestableApp.app */, - D9DC6DC31FDFEFF100017652 /* CloudKitTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; E26BDD971E759D5E00994CE7 /* Resources */ = { isa = PBXGroup; children = ( @@ -179,7 +125,6 @@ E2C3E3511E53299800A733BF /* Products */, E26BDD971E759D5E00994CE7 /* Resources */, E2C3E3521E53299800A733BF /* Sources */, - E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */, BFBADA7DFB65C4DA1FA75BBE /* Pods */, ); sourceTree = ""; @@ -225,12 +170,10 @@ E2C3E34E1E53299800A733BF /* Resources */, E23BE7101EA4FD78008F4F23 /* Embed Frameworks */, D0F793BBB16236A031D0746F /* [CP] Embed Pods Frameworks */, - 141EBAF67A939CF3C08FF52E /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( - E23BE70F1EA4FD78008F4F23 /* PBXTargetDependency */, ); name = CloudCoreExample; productName = CloudTest2; @@ -244,13 +187,13 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = "Vasily Ulianov"; TargetAttributes = { E2C3E34F1E53299800A733BF = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = 26Y8AQV29F; - LastSwiftMigration = 0900; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -277,12 +220,6 @@ mainGroup = E2C3E3471E53299800A733BF; productRefGroup = E2C3E3511E53299800A733BF /* Products */; projectDirPath = ""; - projectReferences = ( - { - ProductGroup = E23BE6F41EA4CC1C008F4F23 /* Products */; - ProjectRef = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; - }, - ); projectRoot = ""; targets = ( E2C3E34F1E53299800A733BF /* CloudCoreExample */, @@ -290,37 +227,6 @@ }; /* End PBXProject section */ -/* Begin PBXReferenceProxy section */ - D9DC6DC11FDFEFF100017652 /* TestableApp.app */ = { - isa = PBXReferenceProxy; - fileType = wrapper.application; - path = TestableApp.app; - remoteRef = D9DC6DC01FDFEFF100017652 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - D9DC6DC31FDFEFF100017652 /* CloudKitTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = CloudKitTests.xctest; - remoteRef = D9DC6DC21FDFEFF100017652 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CloudCore.framework; - remoteRef = E23BE6FA1EA4CC1C008F4F23 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - E23BE6FF1EA4CC1C008F4F23 /* CloudCoreTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = CloudCoreTests.xctest; - remoteRef = E23BE6FE1EA4CC1C008F4F23 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - /* Begin PBXResourcesBuildPhase section */ E2C3E34E1E53299800A733BF /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -335,21 +241,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 141EBAF67A939CF3C08FF52E /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 74EEB05A875696C8E3F6398D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -365,7 +256,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; D0F793BBB16236A031D0746F /* [CP] Embed Pods Frameworks */ = { @@ -375,11 +266,15 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/CloudCore/CloudCore.framework", "${BUILT_PRODUCTS_DIR}/Fakery/Fakery.framework", + "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CloudCore.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Fakery.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -406,14 +301,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - E23BE70F1EA4FD78008F4F23 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "CloudCore-iOS"; - targetProxy = E23BE70E1EA4FD78008F4F23 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ E2C3E35C1E53299800A733BF /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -449,6 +336,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -456,6 +344,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -482,7 +371,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -504,6 +393,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -511,6 +401,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -531,7 +422,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -548,12 +439,11 @@ CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; DEVELOPMENT_TEAM = 26Y8AQV29F; INFOPLIST_FILE = Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.deeje.example.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Debug; }; @@ -566,12 +456,11 @@ CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; DEVELOPMENT_TEAM = 26Y8AQV29F; INFOPLIST_FILE = Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.deeje.example.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Release; }; diff --git a/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme b/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme index aae38032..fe665656 100644 --- a/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme +++ b/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme @@ -1,6 +1,6 @@ @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Example/Podfile b/Example/Podfile index f614da88..4292b45f 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -4,7 +4,9 @@ platform :ios, '10.0' target 'CloudCoreExample' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! - + + pod 'CloudCore', :path => '../' + # Pods for CloudCoreExample pod 'Fakery', '~> 3.3.0' end diff --git a/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents b/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents index 4bec3c35..2f4ba43a 100644 --- a/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,39 +1,42 @@ - + + - - + + + - - + + + + + + - - - - - - - - - - + + + + - - + + + + + - - + + \ No newline at end of file diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 37358e04..074452b6 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -17,7 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele let delegateHandler = CloudCoreDelegateHandler() - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Register for push notifications about changes application.registerForRemoteNotifications() @@ -33,7 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele // Check if it CloudKit's and CloudCore notification if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { // Fetch changed data from iCloud - CloudCore.fetchAndSave(using: userInfo, to: persistentContainer, error: { + CloudCore.pull(using: userInfo, to: persistentContainer, error: { print("fetchAndSave from didReceiveRemoteNotification error: \($0)") }, completion: { (fetchResult) in completionHandler(fetchResult.uiBackgroundFetchResult) @@ -50,7 +50,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } @@ -87,6 +87,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele } }) container.viewContext.automaticallyMergesChangesFromParent = true + return container }() diff --git a/Example/Sources/Class/FRCTableViewDataSource.swift b/Example/Sources/Class/FRCTableViewDataSource.swift index 3abfcd47..0b5f631f 100644 --- a/Example/Sources/Class/FRCTableViewDataSource.swift +++ b/Example/Sources/Class/FRCTableViewDataSource.swift @@ -95,7 +95,6 @@ class FRCTableViewDataSource: NSObject tableView?.endUpdates() } - func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {} func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return nil } } diff --git a/Example/Sources/Class/ModelFactory.swift b/Example/Sources/Class/ModelFactory.swift index 66a29dfd..2fd21c59 100644 --- a/Example/Sources/Class/ModelFactory.swift +++ b/Example/Sources/Class/ModelFactory.swift @@ -54,7 +54,7 @@ class ModelFactory { private static func randomAvatar() -> Data? { let randomNumber = String(faker.number.randomInt(min: 1, max: 9)) let image = UIImage(named: "avatar_" + randomNumber)! - return UIImagePNGRepresentation(image) + return image.pngData() } static func newCompanyName() -> String { diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift index d9614003..78d6f22a 100644 --- a/Example/Sources/View Controller/DetailViewController.swift +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -8,6 +8,7 @@ import UIKit import CoreData +import CloudCore class DetailViewController: UITableViewController { @@ -27,36 +28,35 @@ class DetailViewController: UITableViewController { tableDataSource = DetailTableDataSource(fetchRequest: fetchRequest, context: context, sectionNameKeyPath: nil, delegate: self, tableView: tableView) tableView.dataSource = tableDataSource try! tableDataSource.performFetch() - - navigationItem.rightBarButtonItem = editButtonItem - } - - override func setEditing(_ editing: Bool, animated: Bool) { - super.setEditing(editing, animated: animated) - - if editing { - let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(navAddButtonDidTap(_:))) - navigationItem.setLeftBarButton(addButton, animated: animated) - - let renameButton = UIBarButtonItem(title: "Rename", style: .plain, target: self, action: #selector(navRenameButtonDidTap(_:))) - navigationItem.setRightBarButtonItems([editButtonItem, renameButton], animated: animated) - } else { - navigationItem.setLeftBarButton(nil, animated: animated) - navigationItem.setRightBarButtonItems([editButtonItem], animated: animated) - try! context.save() - } + + let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add(_:))) + let renameButton = UIBarButtonItem(title: "Rename", style: .plain, target: self, action: #selector(rename(_:))) + navigationItem.setRightBarButtonItems([addButton, renameButton], animated: false) } - - @objc private func navAddButtonDidTap(_ sender: UIBarButtonItem) { - let employee = ModelFactory.insertEmployee(context: context) - let organization = context.object(with: organizationID) as! Organization - employee.organization = organization + + @objc private func add(_ sender: UIBarButtonItem) { + persistentContainer.performBackgroundTask { (moc) in + moc.name = CloudCore.config.pushContextName + + let employee = ModelFactory.insertEmployee(context: moc) + let organization = try? moc.existingObject(with: self.organizationID) as! Organization + employee.organization = organization + + try? moc.save() + } } - @objc private func navRenameButtonDidTap(_ sender: UIBarButtonItem) { - let organization = context.object(with: organizationID) as! Organization - organization.name = ModelFactory.newCompanyName() - self.title = organization.name + @objc private func rename(_ sender: UIBarButtonItem) { + let newTitle = ModelFactory.newCompanyName() + persistentContainer.performBackgroundTask { (moc) in + moc.name = CloudCore.config.pushContextName + + let organization = try? moc.existingObject(with: self.organizationID) as! Organization + organization?.name = newTitle + + try? moc.save() + } + self.title = newTitle } } @@ -93,15 +93,34 @@ extension DetailViewController: FRCTableViewDelegate { } +extension DetailViewController { + + @available(iOS 11.0, *) + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let deleteTitle = NSLocalizedString("Delete", comment: "Delete action") + let deleteAction = UIContextualAction(style: .destructive, title: deleteTitle, + handler: { [weak self] action, view, completionHandler in + + let anObject = self?.tableDataSource.object(at: indexPath) + let objectID = anObject?.objectID + + persistentContainer.performBackgroundTask { (moc) in + moc.name = CloudCore.config.pushContextName + if let objectToDelete = try? moc.existingObject(with: objectID!) { + moc.delete(objectToDelete) + try? moc.save() + } + } + + completionHandler(true) + }) + + let configuration = UISwipeActionsConfiguration(actions: [deleteAction]) + return configuration + } + +} + fileprivate class DetailTableDataSource: FRCTableViewDataSource { - - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { - let context = frc.managedObjectContext - - switch editingStyle { - case .delete: context.delete(object(at: indexPath)) - default: return - } - } - + } diff --git a/Example/Sources/View Controller/MasterViewController.swift b/Example/Sources/View Controller/MasterViewController.swift index e41fecb8..cc15299d 100644 --- a/Example/Sources/View Controller/MasterViewController.swift +++ b/Example/Sources/View Controller/MasterViewController.swift @@ -21,7 +21,7 @@ class MasterViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - + let fetchRequest: NSFetchRequest = Organization.fetchRequest() fetchRequest.sortDescriptors = [NSSortDescriptor(key: "sort", ascending: true)] tableDataSource = MasterTableViewDataSource(fetchRequest: fetchRequest, context: context, sectionNameKeyPath: nil, delegate: self, tableView: tableView) @@ -29,26 +29,18 @@ class MasterViewController: UITableViewController { try! tableDataSource.performFetch() self.clearsSelectionOnViewWillAppear = true - - self.navigationItem.rightBarButtonItem = editButtonItem - } - - override func setEditing(_ editing: Bool, animated: Bool) { - super.setEditing(editing, animated: animated) - - // Save on editing end - if !editing { - try! context.save() - } } - + @IBAction func addButtonClicked(_ sender: UIBarButtonItem) { - ModelFactory.insertOrganizationWithEmployees(context: context) - try! context.save() + persistentContainer.performBackgroundTask { (moc) in + moc.name = CloudCore.config.pushContextName + ModelFactory.insertOrganizationWithEmployees(context: moc) + try! moc.save() + } } @IBAction func refreshValueChanged(_ sender: UIRefreshControl) { - CloudCore.fetchAndSave(to: persistentContainer, error: { (error) in + CloudCore.pull(to: persistentContainer, error: { (error) in print("⚠️ FetchAndSave error: \(error)") DispatchQueue.main.async { sender.endRefreshing() @@ -70,7 +62,7 @@ class MasterViewController: UITableViewController { } } - override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle { + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { return .delete } @@ -92,15 +84,35 @@ extension MasterViewController: FRCTableViewDelegate { } +extension MasterViewController { + + @available(iOS 11.0, *) + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let deleteTitle = NSLocalizedString("Delete", comment: "Delete action") + let deleteAction = UIContextualAction(style: .destructive, title: deleteTitle, + handler: { [weak self] action, view, completionHandler in + + let anObject = self?.tableDataSource.object(at: indexPath) + let objectID = anObject?.objectID + + persistentContainer.performBackgroundTask { (moc) in + moc.name = CloudCore.config.pushContextName + if let objectToDelete = try? moc.existingObject(with: objectID!) { + moc.delete(objectToDelete) + try? moc.save() + } + } + + completionHandler(true) + }) + + let configuration = UISwipeActionsConfiguration(actions: [deleteAction]) + return configuration + } + +} + fileprivate class MasterTableViewDataSource: FRCTableViewDataSource { - - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { - let context = frc.managedObjectContext - switch editingStyle { - case .delete: context.delete(object(at: indexPath)) - default: return - } - } - } + From e11d9cfbcb365ea7d0cb86b9d55eab2cfe4d37b6 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 2 Dec 2018 18:22:06 -0800 Subject: [PATCH 041/203] update ReadMe to latest CloudCore --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index da48b07d..d38e3674 100755 --- a/README.md +++ b/README.md @@ -6,24 +6,29 @@ ![Status](https://img.shields.io/badge/status-beta-orange.svg) ![Swift](https://img.shields.io/badge/swift-4-orange.svg) -**CloudCore** is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. It maybe used are CloudKit caching. +**CloudCore** is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. #### Features -* Sync manually or on **push notifications**. -* **Differential sync**, only changed object and values are uploaded and downloaded. CloudCore even differs changed and not changed values inside objects. +* Leveraging **NSPersistentHistory**, local changes are pushed to CloudKit when online +* Pull manually or on CloudKit **remote notifications**. +* **Differential sync**, only changed object and values are uploaded and downloaded. +* Core Data relationships are preserved +* **private database** and **shared database** push and pull is supported. +* **public database** push is supported +* Parent-Child relationships can be defined for CloudKit Sharing * Respects of Core Data options (cascade deletions, external storage). * Knows and manages with CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. * Covered with Unit and CloudKit online **tests**. * All public methods are **[100% documented](https://sorix.github.io/CloudCore/)**. -* Currently only **private database** is supported. ## How it works? CloudCore is built using "black box" architecture, so it works invisibly for your application, you just need to add several lines to `AppDelegate` to enable it. Synchronization and error resolving is managed automatically. 1. CloudCore stores *change tokens* from CloudKit, so only changed data is downloaded. -2. When CloudCore is enabled (`CloudCore.enable`) it fetches changed data from CloudKit and subscribes to CloudKit push notifications about new changes. -3. When `CloudCore.pull` is called manually or by push notification, CloudCore fetches and saves changed data to Core Data. -4. When data is written to persistent container (parent context is saved) CloudCore founds locally changed data and uploads it to CloudKit. +2. When CloudCore is enabled (`CloudCore.enable`) it pulls changed data from CloudKit and subscribes to CloudKit push notifications about new changes. +3. When `CloudCore.pull` is called manually or by push notification, CloudCore pulls and saves changed data to Core Data. +4. When data is written to persistent container (parent context is saved) CloudCore founds locally changed data and pushed to CloudKit. +5. when leveraging NSPersistentHistory, changes are pushed only when online. ## Installation @@ -46,11 +51,21 @@ HTML-generated version of that documentation is [**available here**](https://sor 1. Enable CloudKit capability for you application: ![CloudKit capability](https://cloud.githubusercontent.com/assets/5610904/25092841/28305bc0-2398-11e7-9fbf-f94c619c264f.png) -2. Add 2 service attributes to each entity in CoreData model you want to sync: - * `recordData` attribute with `Binary` type - * `recordID` attribute with `String` type +2. For each entity type you want to sync, add this key: value pair to the UserInfo record of the entity: -3. Make changes in your **AppDelegate.swift** file: + * `CloudCoreScopes`: `private` + +3. Also add 4 attributes to each entity: + * `privateRecordData` attribute with `Binary` type + * `publicRecordData` attribute with `Binary` type + * `recordName` attribute with `String` type + * `ownerName` attribute with `String` type + +4. And enable 'Preserve After Deletion' for the following attributes + * `privateRecordData` + * `publicRecordData` + +4. Make changes in your **AppDelegate.swift** file: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { @@ -80,31 +95,67 @@ func applicationWillTerminate(_ application: UIApplication) { } ``` -4. Make first run of your application in a development environment, fill an example data in Core Data and wait until sync completes. CloudCore create needed CloudKit schemes automatically. +5. If you want to enable offline support, **enable NSPersistentHistoryTracking** when you initialize your Core Data stack + +```swift +lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "YourApp") + + let storeDescription = container.persistentStoreDescriptions.first + storeDescription?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + + container.loadPersistentStores { storeDescription, error in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + } + } + return container +}() +``` + +6. To identify changes from your app that should be pushed, **save** from a background ManagedObjectContexts named `CloudCorePushContext` + +```swift +persistentContainer.performBackgroundTask { moc in + moc.name = CloudCore.config.pushContextName + // make changes to objects, properties, and relationships you want pushed via CloudCore + try? context.save() +} +``` + +7. Make first run of your application in a development environment, fill an example data in Core Data and wait until sync completes. CloudKit will create needed schemas automatically. ## Service attributes -CloudCore stores service CloudKit information in managed objects, you need to add that attributes to your Core Data model. If required attributes are not found in entity that entity won't be synced. +CloudCore stores CloudKit information inside your managed objects, so you need to add attributes to your Core Data model for that. If required attributes are not found in an entity, that entity won't be synced. Required attributes for each synced entity: -1. *Record Data* attribute with `Binary` type -2. *Record ID* attribute with `String` type +1. *Private Record Data* attribute with `Binary` type +2. *Public Record Data* attribute with `Binary` type +3. *Record Name* attribute with `String` type +4. *Owner Name* attribute with `String` type -You may specify attributes' names in 2 ways (you may combine that ways in different entities). +You may specify attributes' names in one of two 2 ways (you may combine that ways in different entities). -### User Info -First off CloudCore try to search attributes by looking up User Info at your model, you may specify User Info key `CloudCoreType` for attribute to mark one as service one. Values are: -* *Record Data* value is `recordData`. -* *Record ID* value is `recordID`. +### Default names +The most simple way is to name attributes with default names because you don't need to map them in UserInfo. -![Model editor User Info](https://cloud.githubusercontent.com/assets/5610904/24004400/52e0ff94-0a77-11e7-9dd9-e1e24a86add5.png) +### Mapping via UserInfo +You can map your own attributes to the required service attributes. For each attribute you want to map, add an item to the attribute's UserInfo, using the key `CloudCoreType` and following values: +* *Private Record Data* value is `privateRecordData`. +* *Public Record Data* value is `publicRecordData`. +* *Record Name* value is `recordName`. +* *Owner Name* value is `ownerName`. -### Default names -The most simple way is to name attributes with default names because you don't need to specify any User Info. +![Model editor User Info](https://cloud.githubusercontent.com/assets/5610904/24004400/52e0ff94-0a77-11e7-9dd9-e1e24a86add5.png) ### 💡 Tips -* You can name attribute as you want, value of User Info is not changed (you can create attribute `myid` with User Info: `CloudCoreType: recordID`) -* I recommend to mark *Record ID* attribute as `Indexed`, that can speed up updates in big databases. -* *Record Data* attribute is used to store archived version of `CKRecord` with system fields only (like timestamps, tokens), so don't worry about size, no real data will be stored here. +* I recommend to set the *Record Name* attribute as `Indexed`, to speed up updates in big databases. +* *Record Data* attributes are used to store archived version of `CKRecord` with system fields only (like timestamps, tokens), so don't worry about size, no real data will be stored here. + +## CloudKit Sharing +To enable CloudKit Sharing when your entities have relationships, CloudCore will look for the following key:value pair in the UserInfo of your entities: + +`CloudCoreParent`: name of the to-one relationship property in your entity ## Example application You can find example application at [Example](/Example/) directory. @@ -141,7 +192,13 @@ CloudKit objects can't be mocked up, that's why I create 2 different types of te - [ ] Add methods to clear local cache and remote database - [ ] Add error resolving for `limitExceeded` error (split saves by relationships). -## Author +## Authors -Open for hire / relocation. Vasily Ulianov, [va...@me.com](http://www.google.com/recaptcha/mailhide/d?k=01eFEpy-HM-qd0Vf6QGABTjw==&c=JrKKY2bjm0Bp58w7zTvPiQ==) +Open for hire / relocation. + +Ludovic Landry + +Oleg Müller + +deeje cooley, [deeje.com](http://www.deeje.com/) \ No newline at end of file From 961fe5e640e8d39549abc05515b6cf5e3f4d7bc6 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 2 Dec 2018 18:34:03 -0800 Subject: [PATCH 042/203] reset Example bundle ID --- Example/CloudCoreExample.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index df9eee02..e1f17bcc 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -441,7 +441,6 @@ INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.deeje.example.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; }; @@ -458,7 +457,6 @@ INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.deeje.example.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; }; From 96871f385e72fecf1cedf3051df6173a5899c084 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 3 Dec 2018 15:59:34 -0800 Subject: [PATCH 043/203] update author list --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index d38e3674..fb295376 100755 --- a/README.md +++ b/README.md @@ -197,8 +197,6 @@ CloudKit objects can't be mocked up, that's why I create 2 different types of te Vasily Ulianov, [va...@me.com](http://www.google.com/recaptcha/mailhide/d?k=01eFEpy-HM-qd0Vf6QGABTjw==&c=JrKKY2bjm0Bp58w7zTvPiQ==) Open for hire / relocation. -Ludovic Landry - Oleg Müller deeje cooley, [deeje.com](http://www.deeje.com/) \ No newline at end of file From fb7a6f8de91601b2160131b47bdc592b7ef93b58 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 5 Dec 2018 14:06:49 -0800 Subject: [PATCH 044/203] [deeje] updated ReadMe to properly reflect status --- README.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fb295376..7375e044 100755 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # CloudCore -[![Documentation](https://sorix.github.io/CloudCore/badge.svg)](https://sorix.github.io/CloudCore/) -[![Version](https://img.shields.io/cocoapods/v/CloudCore.svg?style=flat)](https://cocoapods.org/pods/CloudCore) ![Platform](https://img.shields.io/cocoapods/p/CloudCore.svg?style=flat) ![Status](https://img.shields.io/badge/status-beta-orange.svg) -![Swift](https://img.shields.io/badge/swift-4-orange.svg) +![Swift](https://img.shields.io/badge/swift-4.2-orange.svg) **CloudCore** is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. @@ -18,8 +16,6 @@ * Parent-Child relationships can be defined for CloudKit Sharing * Respects of Core Data options (cascade deletions, external storage). * Knows and manages with CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. -* Covered with Unit and CloudKit online **tests**. -* All public methods are **[100% documented](https://sorix.github.io/CloudCore/)**. ## How it works? CloudCore is built using "black box" architecture, so it works invisibly for your application, you just need to add several lines to `AppDelegate` to enable it. Synchronization and error resolving is managed automatically. @@ -37,15 +33,11 @@ CloudCore is built using "black box" architecture, so it works invisibly for you it, simply add the following line to your Podfile: ```ruby -pod 'CloudCore', '~> 2.0' +pod 'CloudCore', :git => 'https://github.com/deeje/CloudCore.git' ``` ## How to help? -Current version of framework hasn't been deeply tested and may contain errors. If you can test framework, I will be very glad. If you found an error, please post [an issue](https://github.com/Sorix/CloudCore/issues). - -## Documentation -All public methods are documented using [XCode Markup](https://developer.apple.com/library/content/documentation/Xcode/Reference/xcode_markup_formatting_ref/) and available inside XCode. -HTML-generated version of that documentation is [**available here**](https://sorix.github.io/CloudCore/). +Current version of framework hasn't been deeply tested and may contain errors. If you can test framework, I will be very glad. If you found an error, please post [an issue](https://github.com/deeje/CloudCore/issues). ## Quick start 1. Enable CloudKit capability for you application: @@ -187,16 +179,20 @@ CloudKit objects can't be mocked up, that's why I create 2 different types of te ## Roadmap -- [x] Move from alpha to beta status. +- [ ] Properly push only changed fields from NSPersistenHistory +- [ ] Move beta to release status - [ ] Add `CloudCore.disable` method - [ ] Add methods to clear local cache and remote database - [ ] Add error resolving for `limitExceeded` error (split saves by relationships). ## Authors +deeje cooley, [deeje.com](http://www.deeje.com/) +- added NSPersistentHistory and CloudKit Sharing Support + Vasily Ulianov, [va...@me.com](http://www.google.com/recaptcha/mailhide/d?k=01eFEpy-HM-qd0Vf6QGABTjw==&c=JrKKY2bjm0Bp58w7zTvPiQ==) Open for hire / relocation. +- implemented version 1 and 2, with dynamic mapping between CoreData and CloudKit Oleg Müller - -deeje cooley, [deeje.com](http://www.deeje.com/) \ No newline at end of file +- added full support for CoreData relationships From eb30a80c26ee41bf762e5c3f52cdc39ef479570c Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 6 Dec 2018 16:57:39 -0800 Subject: [PATCH 045/203] optimize by using change.updatedProperties --- Source/Classes/Push/CoreDataObserver.swift | 7 ++++++- .../ObjectToRecord/ObjectToRecordConverter.swift | 8 +++++++- Source/Extensions/NSManagedObject.swift | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 407ac0a8..cbcf4513 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -205,7 +205,12 @@ class CoreDataObserver { case .update: if let inserted = try? moc.existingObject(with: change.changedObjectID) { - // TODO: optimize by using change.updatedProperties + if let updatedProperties = change.updatedProperties { + let updatedPropertyNames: [String] = updatedProperties.map { (propertyDescription) in + return propertyDescription.name + } + inserted.updatedPropertyNames = updatedPropertyNames + } updatedObject.insert(inserted) } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 5fa1d3d0..31c26c4d 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -64,7 +64,13 @@ class ObjectToRecordConverter { var changedAttributes: [String]? // Save changes keys only for updated object, for inserted objects full sync will be used - if case .updated = changeType { changedAttributes = Array(object.changedValues().keys) } + if case .updated = changeType { + changedAttributes = Array(object.changedValues().keys) + + if changedAttributes?.count == 0 { + changedAttributes = object.updatedPropertyNames + } + } let convertOperation = ObjectToRecordOperation(scope: scope, record: recordWithSystemFields, diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index 86bb5abc..a00e1b0e 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -65,4 +65,20 @@ extension NSManagedObject { return aRecord } + +} + +extension NSManagedObject { + + static var updatedPropertyNamesKey = "NSManagedObject_updatedPropertyNamesKey" + + var updatedPropertyNames: [String]? { + get { + return objc_getAssociatedObject(self, &NSManagedObject.updatedPropertyNamesKey) as? [String] + } + set { + objc_setAssociatedObject(self, &NSManagedObject.updatedPropertyNamesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + } From d2f1eabe10ebc8c6ec97c6a4e006c6190bdc8194 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 12:25:16 -0800 Subject: [PATCH 046/203] update CloudKitTests --- Tests/CloudKitTests/CloudKitTests.swift | 2 +- Tests/CloudKitTests/Helpers.swift | 2 +- .../model.xcdatamodel/contents | 18 +++++++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Tests/CloudKitTests/CloudKitTests.swift b/Tests/CloudKitTests/CloudKitTests.swift index 17421bae..3fe61376 100644 --- a/Tests/CloudKitTests/CloudKitTests.swift +++ b/Tests/CloudKitTests/CloudKitTests.swift @@ -50,7 +50,7 @@ class CloudKitTests: CoreDataTestCase { // Fetch data from CloudKit let fetchExpectation = expectation(description: "fetchExpectation") - CloudCore.fetchAndSave(to: freshPersistentContainer, error: { (error) in + CloudCore.pull(to: freshPersistentContainer, error: { (error) in XCTFail("Error while trying to fetch from CloudKit: \(error)") }) { fetchExpectation.fulfill() diff --git a/Tests/CloudKitTests/Helpers.swift b/Tests/CloudKitTests/Helpers.swift index dc3d941d..ebc63449 100644 --- a/Tests/CloudKitTests/Helpers.swift +++ b/Tests/CloudKitTests/Helpers.swift @@ -36,7 +36,7 @@ extension CoreDataTestCase { wait(for: [didSyncExpectation], timeout: 10) let fetchAndSaveExpectation = expectation(description: "fetchAndSave") - CloudCore.fetchAndSave(to: persistentContainer, error: { (error) in + CloudCore.pull(to: persistentContainer, error: { (error) in XCTFail("fetchAndSave error: \(error)") }) { fetchAndSaveExpectation.fulfill() diff --git a/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents b/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents index 7f8e6fcb..805562af 100644 --- a/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents +++ b/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -13,12 +13,14 @@ - + + - + + @@ -30,12 +32,14 @@ - + + - + + @@ -45,7 +49,7 @@ - - + + \ No newline at end of file From 8464baed03b68b0839354d1c193ff716686c0183 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 12:32:14 -0800 Subject: [PATCH 047/203] more updates to CloudKitTests --- Tests/CloudKitTests/CloudKitTests.swift | 2 ++ Tests/CloudKitTests/Helpers.swift | 8 ++++---- .../model.xcdatamodeld/model.xcdatamodel/contents | 6 ++++++ Tests/Shared/CoreDataTestCase.swift | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Tests/CloudKitTests/CloudKitTests.swift b/Tests/CloudKitTests/CloudKitTests.swift index 3fe61376..cf545a28 100644 --- a/Tests/CloudKitTests/CloudKitTests.swift +++ b/Tests/CloudKitTests/CloudKitTests.swift @@ -19,6 +19,8 @@ class CloudKitTests: CoreDataTestCase { super.setUp() configureCloudKitIfNeeded() CloudKitTests.deleteAllRecordsFromCloudKit() + + context.name = CloudCore.config.pushContextName } override class func tearDown() { diff --git a/Tests/CloudKitTests/Helpers.swift b/Tests/CloudKitTests/Helpers.swift index ebc63449..dcaf3068 100644 --- a/Tests/CloudKitTests/Helpers.swift +++ b/Tests/CloudKitTests/Helpers.swift @@ -35,14 +35,14 @@ extension CoreDataTestCase { wait(for: [didSyncExpectation], timeout: 10) - let fetchAndSaveExpectation = expectation(description: "fetchAndSave") + let pullExpectation = expectation(description: "pull") CloudCore.pull(to: persistentContainer, error: { (error) in - XCTFail("fetchAndSave error: \(error)") + XCTFail("pull error: \(error)") }) { - fetchAndSaveExpectation.fulfill() + pullExpectation.fulfill() } - wait(for: [fetchAndSaveExpectation], timeout: 10) + wait(for: [pullExpectation], timeout: 10) UserDefaults.standard.set(true, forKey: "isCloudKitConfigured") delegateListener.didSyncToCloudBlock = nil diff --git a/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents b/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents index 805562af..ed7b0aae 100644 --- a/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents +++ b/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents @@ -29,6 +29,9 @@ + + + @@ -46,6 +49,9 @@ + + + diff --git a/Tests/Shared/CoreDataTestCase.swift b/Tests/Shared/CoreDataTestCase.swift index f32b3445..aae35084 100644 --- a/Tests/Shared/CoreDataTestCase.swift +++ b/Tests/Shared/CoreDataTestCase.swift @@ -22,6 +22,7 @@ class CoreDataTestCase: XCTestCase { let container = NSPersistentContainer(name: "model", managedObjectModel: model) let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType + description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) container.persistentStoreDescriptions = [description] let expect = expectation(description: "CoreDataStackInitialize") From 2209cfd09de0a299e7fc7987e1b86755ead4b9c7 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 14:37:47 -0800 Subject: [PATCH 048/203] updating swift-version and travis.yml to latest --- .swift-version | 2 +- .travis.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.swift-version b/.swift-version index 5186d070..bf77d549 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.0 +4.2 diff --git a/.travis.yml b/.travis.yml index a60a057b..5cc99f0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ -osx_image: xcode9.2 -language: objective-c +osx_image: xcode10.1 +language: swift podfile: "Example/Podfile" branches: @@ -8,7 +8,7 @@ branches: env: - DESTINATION='platform=OS X' POD_LINT="YES" - - DESTINATION='platform=iOS Simulator,name=iPhone 6S' BUILD_EXAMPLE="YES" + - DESTINATION='platform=iOS Simulator,name=iPhone XS' BUILD_EXAMPLE="YES" - DESTINATION='platform=watchOS Simulator,name=Apple Watch - 38mm' SKIP_TEST="YES" - DESTINATION='platform=tvOS Simulator,name=Apple TV 4K' From 959b95d3c81b8945a9002a7873d58880c152f04e Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 14:48:57 -0800 Subject: [PATCH 049/203] tell travis to build the CloudCore.xcworkspace --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5cc99f0e..52beecfb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,9 +17,9 @@ before_install: script: - set -o pipefail - - xcodebuild -scheme CloudCore -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter` + - xcodebuild -workspace "CloudCore.xcworkspace" -scheme CloudCore -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter` - if [ "$SKIP_TEST" != "YES" ]; then - xcodebuild -scheme CloudCore -destination "$DESTINATION" test | xcpretty -f `xcpretty-travis-formatter`; + xcodebuild -workspace "CloudCore.xcworkspace" -scheme CloudCore -destination "$DESTINATION" test | xcpretty -f `xcpretty-travis-formatter`; fi # Example From e687f46bfb3491b325b989186ceeeff68d6aeaf6 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 14:56:39 -0800 Subject: [PATCH 050/203] =?UTF-8?q?don=E2=80=99t=20point=20travis=20to=20?= =?UTF-8?q?=E2=80=9CExample/Podfile=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 52beecfb..6b7c1f61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ osx_image: xcode10.1 language: swift -podfile: "Example/Podfile" +#podfile: "Example/Podfile" branches: only: From 2ae5ec0a8bf0671b5a1fe967add43d0221efd044 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 15:04:09 -0800 Subject: [PATCH 051/203] update test code to Swift 4.2 --- CloudCore.xcodeproj/project.pbxproj | 25 +++++++++++-------- .../xcshareddata/xcschemes/CloudCore.xcscheme | 8 +++--- Tests/CloudKitTests/App/AppDelegate.swift | 2 +- Tests/CloudKitTests/Helpers.swift | 4 +-- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 1c5148f8..b70577ff 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -129,7 +129,7 @@ E20A73CB1E68608100A6851A /* RecordToCoreDataOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordToCoreDataOperationTests.swift; sourceTree = ""; }; E21FA03D1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordToCoreDataOperation.swift; sourceTree = ""; }; E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitAttribute.swift; sourceTree = ""; }; - E22C40441E4291FB009469A1 /* CloudCore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CloudCore.podspec; sourceTree = ""; }; + E22C40441E4291FB009469A1 /* CloudCore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CloudCore.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; E22C40451E42956C009469A1 /* CoreDataObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = ""; }; E23C478B1E48A404004310F9 /* PushOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushOperationQueue.swift; sourceTree = ""; }; E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxyTests.swift; sourceTree = ""; }; @@ -616,7 +616,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0910; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = "Vasily Ulianov"; TargetAttributes = { D5B2E89E1C3A780C00C0327D = { @@ -626,7 +626,7 @@ }; D9B3C6F21FCEF38D00CDB7FF = { CreatedOnToolsVersion = 9.1; - DevelopmentTeam = 7X2PJ6H6YM; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Push = { @@ -640,6 +640,7 @@ D9B3C71B1FCEF96D00CDB7FF = { CreatedOnToolsVersion = 9.1; DevelopmentTeam = 7X2PJ6H6YM; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; TestTargetID = D9B3C6F21FCEF38D00CDB7FF; }; @@ -904,12 +905,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -962,12 +965,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -1065,16 +1070,15 @@ CODE_SIGN_ENTITLEMENTS = Tests/CloudKitTests/App/TestableApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7X2PJ6H6YM; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = Tests/CloudKitTests/App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = cloudtests.TestableApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1091,15 +1095,14 @@ CODE_SIGN_ENTITLEMENTS = Tests/CloudKitTests/App/TestableApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7X2PJ6H6YM; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = Tests/CloudKitTests/App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = cloudtests.TestableApp; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1123,7 +1126,7 @@ PRODUCT_BUNDLE_IDENTIFIER = cloudtests.CloudKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestableApp.app/TestableApp"; }; @@ -1147,7 +1150,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = cloudtests.CloudKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestableApp.app/TestableApp"; }; diff --git a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme b/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme index ac2a4ade..9fac0451 100644 --- a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme +++ b/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme @@ -1,6 +1,6 @@ + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -57,7 +56,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Tests/CloudKitTests/App/AppDelegate.swift b/Tests/CloudKitTests/App/AppDelegate.swift index 11f5e4d3..ba7c8ad5 100644 --- a/Tests/CloudKitTests/App/AppDelegate.swift +++ b/Tests/CloudKitTests/App/AppDelegate.swift @@ -15,7 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } diff --git a/Tests/CloudKitTests/Helpers.swift b/Tests/CloudKitTests/Helpers.swift index dcaf3068..bdeebdbf 100644 --- a/Tests/CloudKitTests/Helpers.swift +++ b/Tests/CloudKitTests/Helpers.swift @@ -50,7 +50,7 @@ extension CoreDataTestCase { static func deleteAllRecordsFromCloudKit() { let operationQueue = OperationQueue() - var recordIdsToDelete = [CKRecordID]() + var recordIdsToDelete = [CKRecord.ID]() let publicDatabase = CKContainer.default().privateCloudDatabase let queries = [ @@ -75,7 +75,7 @@ extension CoreDataTestCase { if recordIdsToDelete.isEmpty { return } let deleteOperation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIdsToDelete) - deleteOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, deletedRecordIDs: [CKRecordID]?, error: Error?) in + deleteOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, deletedRecordIDs: [CKRecord.ID]?, error: Error?) in if let error = error { XCTFail("Error while tried to clean test objects: \(error)") } From b2f6f46c6a8810ba985beba7a797893d8c976e6f Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 15:04:27 -0800 Subject: [PATCH 052/203] enable NSPersistentHistoryTrackingKey in iOS 11 --- Tests/Shared/CoreDataTestCase.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/Shared/CoreDataTestCase.swift b/Tests/Shared/CoreDataTestCase.swift index aae35084..d0ac2b19 100644 --- a/Tests/Shared/CoreDataTestCase.swift +++ b/Tests/Shared/CoreDataTestCase.swift @@ -22,7 +22,9 @@ class CoreDataTestCase: XCTestCase { let container = NSPersistentContainer(name: "model", managedObjectModel: model) let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType - description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + if #available(iOS 11.0, watchOS 4.0, *) { + description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + } container.persistentStoreDescriptions = [description] let expect = expectation(description: "CoreDataStackInitialize") From 1646c8630f796e0b79b9c9ae16277e9a45bbe5b2 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 15:32:18 -0800 Subject: [PATCH 053/203] disabling build Example in travis --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6b7c1f61..ce03642e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ branches: env: - DESTINATION='platform=OS X' POD_LINT="YES" - - DESTINATION='platform=iOS Simulator,name=iPhone XS' BUILD_EXAMPLE="YES" + - DESTINATION='platform=iOS Simulator,name=iPhone XS' #BUILD_EXAMPLE="YES" - DESTINATION='platform=watchOS Simulator,name=Apple Watch - 38mm' SKIP_TEST="YES" - DESTINATION='platform=tvOS Simulator,name=Apple TV 4K' @@ -23,9 +23,9 @@ script: fi # Example - - if [ "$BUILD_EXAMPLE" = "YES" ]; then - xcodebuild -workspace "Example/CloudCoreExample.xcworkspace" -scheme "CloudCoreExample" -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter`; - fi + #- if [ "$BUILD_EXAMPLE" = "YES" ]; then + # xcodebuild -workspace "Example/CloudCoreExample.xcworkspace" -scheme "CloudCoreExample" -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter`; + # fi - if [ "$POD_LINT" = "YES" ]; then pod lib lint --allow-warnings; From 8d4d862597ea2e1914c1d21b769124b874bb19a5 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 15:52:37 -0800 Subject: [PATCH 054/203] Only use Reachability on iOS --- Source/Classes/Push/CoreDataObserver.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index cbcf4513..a90bfefb 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -9,7 +9,7 @@ import Foundation import CoreData import CloudKit -#if os(iOS) || os(tvOS) || os(macOS) +#if os(iOS) import Reachability #endif @@ -26,7 +26,7 @@ class CoreDataObserver { weak var delegate: CloudCoreDelegate? var usePersistentHistoryForPush = false - #if os(iOS) || os(tvOS) || os(macOS) + #if os(iOS) var reachability: Reachability? var isOnline = true { didSet { @@ -43,13 +43,13 @@ class CoreDataObserver { self?.delegate?.error(error: $0, module: .some(.pushToCloud)) } - if #available(iOS 11.0, watchOS 4.0, *) { + if #available(iOS 11.0, watchOS 4.0, tvOS 11.0, OSX 10.13, *) { let storeDescription = container.persistentStoreDescriptions.first if let persistentHistoryNumber = storeDescription?.options[NSPersistentHistoryTrackingKey] as? NSNumber { usePersistentHistoryForPush = persistentHistoryNumber.boolValue } - #if os(iOS) || os(tvOS) || os(macOS) + #if os(iOS) if usePersistentHistoryForPush { reachability = Reachability(hostname: "icloud.com") } @@ -68,7 +68,7 @@ class CoreDataObserver { name: .NSManagedObjectContextDidSave, object: nil) - #if os(iOS) || os(tvOS) || os(macOS) + #if os(iOS) NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged(notification:)), name: .reachabilityChanged, @@ -82,7 +82,7 @@ class CoreDataObserver { func stop() { NotificationCenter.default.removeObserver(self) - #if os(iOS) || os(tvOS) || os(macOS) + #if os(iOS) reachability?.stopNotifier() #endif } @@ -171,7 +171,7 @@ class CoreDataObserver { } } - #if os(iOS) || os(tvOS) || os(macOS) + #if os(iOS) @objc private func reachabilityChanged(notification: Notification) { let reachability = notification.object as! Reachability @@ -180,11 +180,11 @@ class CoreDataObserver { #endif func processPersistentHistory() { - #if os(iOS) || os(tvOS) || os(macOS) + #if os(iOS) guard isOnline else { return } #endif - if #available(iOS 11.0, watchOSApplicationExtension 4.0, *) { + if #available(iOS 11.0, watchOSApplicationExtension 4.0, tvOS 11.0, OSX 10.13, *) { func process(_ transaction: NSPersistentHistoryTransaction, in moc: NSManagedObjectContext) -> Bool { var success = true From 0e848a4889035a31f18896548fcb1446e2ea9cf8 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 16:37:04 -0800 Subject: [PATCH 055/203] remove Reachability as a dependency its up to the host app to change CloudCore.isOnline as appropriate for history-driven push --- .travis.yml | 10 ++--- CloudCore.podspec | 10 ++--- CloudCore.xcodeproj/project.pbxproj | 45 ------------------- .../contents.xcworkspacedata | 10 ----- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ---- Example/Podfile | 1 + Example/Sources/AppDelegate.swift | 19 +++++++- Podfile | 14 ------ Podfile.lock | 16 ------- Source/Classes/CloudCore.swift | 10 ++++- Source/Classes/Push/CoreDataObserver.swift | 38 ++-------------- 11 files changed, 40 insertions(+), 141 deletions(-) delete mode 100644 CloudCore.xcworkspace/contents.xcworkspacedata delete mode 100644 CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 Podfile delete mode 100644 Podfile.lock diff --git a/.travis.yml b/.travis.yml index ce03642e..52beecfb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ osx_image: xcode10.1 language: swift -#podfile: "Example/Podfile" +podfile: "Example/Podfile" branches: only: @@ -8,7 +8,7 @@ branches: env: - DESTINATION='platform=OS X' POD_LINT="YES" - - DESTINATION='platform=iOS Simulator,name=iPhone XS' #BUILD_EXAMPLE="YES" + - DESTINATION='platform=iOS Simulator,name=iPhone XS' BUILD_EXAMPLE="YES" - DESTINATION='platform=watchOS Simulator,name=Apple Watch - 38mm' SKIP_TEST="YES" - DESTINATION='platform=tvOS Simulator,name=Apple TV 4K' @@ -23,9 +23,9 @@ script: fi # Example - #- if [ "$BUILD_EXAMPLE" = "YES" ]; then - # xcodebuild -workspace "Example/CloudCoreExample.xcworkspace" -scheme "CloudCoreExample" -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter`; - # fi + - if [ "$BUILD_EXAMPLE" = "YES" ]; then + xcodebuild -workspace "Example/CloudCoreExample.xcworkspace" -scheme "CloudCoreExample" -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter`; + fi - if [ "$POD_LINT" = "YES" ]; then pod lib lint --allow-warnings; diff --git a/CloudCore.podspec b/CloudCore.podspec index 0717296f..cd921531 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,10 +1,10 @@ Pod::Spec.new do |s| s.name = "CloudCore" - s.summary = "Framework that enables synchronization between CloudKit (iCloud) and Core Data. Can be used as CloudKit caching mechanism." - s.version = "2.0.1" + s.summary = "Framework that enables synchronization between CloudKit (iCloud) and Core Data." + s.version = "3.0" s.homepage = "https://github.com/sorix/CloudCore" s.license = 'MIT' - s.author = { "Vasily Ulianov" => "vasily@me.com" } + s.author = { "Vasily Ulianov" => "vasily@me.com", "deeje" => "deeje@mac.com" } s.source = { :git => "https://github.com/sorix/CloudCore.git", :tag => s.version.to_s @@ -20,8 +20,6 @@ Pod::Spec.new do |s| s.ios.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.osx.frameworks = 'Foundation', 'CloudKit', 'CoreData' - s.ios.dependency 'ReachabilitySwift' - - s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.0' } + s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.2' } s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' end diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index b70577ff..1ed4930d 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 501C5822F10154ED6A7BD2E8 /* Pods_CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */; }; 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57505AAF21A7591500D9CF8F /* PullResult.swift */; }; B34DB05E7C4B442CDBC5475B /* Pods_CloudCoreTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; @@ -89,12 +88,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0AB2CA993B2DDA9672D6AB2D /* Pods-CloudCoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests.debug.xcconfig"; sourceTree = ""; }; 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCore.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCore/Pods-CloudCore.debug.xcconfig"; sourceTree = ""; }; 57505AAF21A7591500D9CF8F /* PullResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullResult.swift; sourceTree = ""; }; 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCore.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCore/Pods-CloudCore.release.xcconfig"; sourceTree = ""; }; D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5C6298B1C3A8BBD007F7B7C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D9089D491FE14E57000FC60C /* SetupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupOperation.swift; sourceTree = ""; }; @@ -162,7 +158,6 @@ E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAttributeName.swift; sourceTree = ""; }; E2FA74431E769BF900C3489D /* RecordWithDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWithDatabase.swift; sourceTree = ""; }; E2FA74471E769D9400C3489D /* RecordIDWithDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordIDWithDatabase.swift; sourceTree = ""; }; - FD3EED3090CAFFEB7FCD9CC7 /* Pods-CloudCoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -170,7 +165,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 501C5822F10154ED6A7BD2E8 /* Pods_CloudCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -202,17 +196,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 4D5D43E1DB83FFB79D4B36AE /* Pods */ = { - isa = PBXGroup; - children = ( - 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */, - AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */, - 0AB2CA993B2DDA9672D6AB2D /* Pods-CloudCoreTests.debug.xcconfig */, - FD3EED3090CAFFEB7FCD9CC7 /* Pods-CloudCoreTests.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; D5B2E8951C3A780C00C0327D = { isa = PBXGroup; children = ( @@ -221,7 +204,6 @@ E29BB2291E436F310020F5B6 /* Tests */, E22C40441E4291FB009469A1 /* CloudCore.podspec */, D9B3C7301FCEFC9C00CDB7FF /* Frameworks */, - 4D5D43E1DB83FFB79D4B36AE /* Pods */, ); sourceTree = ""; }; @@ -539,7 +521,6 @@ isa = PBXNativeTarget; buildConfigurationList = D5B2E8B31C3A780C00C0327D /* Build configuration list for PBXNativeTarget "CloudCore" */; buildPhases = ( - E293C6A0E8D94DD87C14B67D /* [CP] Check Pods Manifest.lock */, D5B2E89A1C3A780C00C0327D /* Sources */, D5B2E89B1C3A780C00C0327D /* Frameworks */, D5B2E89C1C3A780C00C0327D /* Headers */, @@ -751,28 +732,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - E293C6A0E8D94DD87C14B67D /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CloudCore-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1009,7 +968,6 @@ }; D5B2E8B41C3A780C00C0327D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3A766AAC170F6F64576683BC /* Pods-CloudCore.debug.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -1035,7 +993,6 @@ }; D5B2E8B51C3A780C00C0327D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AED4F917B4094EAE4A0679D9 /* Pods-CloudCore.release.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -1158,7 +1115,6 @@ }; E29BB2311E436F310020F5B6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0AB2CA993B2DDA9672D6AB2D /* Pods-CloudCoreTests.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -1180,7 +1136,6 @@ }; E29BB2321E436F310020F5B6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FD3EED3090CAFFEB7FCD9CC7 /* Pods-CloudCoreTests.release.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; diff --git a/CloudCore.xcworkspace/contents.xcworkspacedata b/CloudCore.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 8f7e5e87..00000000 --- a/CloudCore.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/CloudCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Example/Podfile b/Example/Podfile index 4292b45f..439d06c3 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -9,4 +9,5 @@ target 'CloudCoreExample' do # Pods for CloudCoreExample pod 'Fakery', '~> 3.3.0' + pod 'ReachabilitySwift' end diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 074452b6..ead32b63 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -9,6 +9,7 @@ import UIKit import CoreData import CloudCore +import Reachability let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer @@ -16,7 +17,9 @@ let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persis class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { let delegateHandler = CloudCoreDelegateHandler() - + + var reachability: Reachability? + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Register for push notifications about changes application.registerForRemoteNotifications() @@ -25,9 +28,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele CloudCore.delegate = delegateHandler CloudCore.enable(persistentContainer: persistentContainer) + NotificationCenter.default.addObserver(self, + selector: #selector(reachabilityChanged(notification:)), + name: .reachabilityChanged, + object: reachability) + + reachability = Reachability(hostname: "icloud.com") + try? reachability?.startNotifier() + return true } + @objc private func reachabilityChanged(notification: Notification) { + let reachability = notification.object as! Reachability + + CloudCore.isOnline = reachability.connection != .none + } + // Notification from CloudKit about changes in remote database func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // Check if it CloudKit's and CloudCore notification diff --git a/Podfile b/Podfile deleted file mode 100644 index 46ce2ed5..00000000 --- a/Podfile +++ /dev/null @@ -1,14 +0,0 @@ - -target 'CloudCore' do - platform :ios, '10.0' - use_frameworks! - - pod 'ReachabilitySwift' -end - -target 'CloudCoreTests' do - platform :ios, '10.0' - use_frameworks! - - pod 'ReachabilitySwift' -end diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index e7cb9608..00000000 --- a/Podfile.lock +++ /dev/null @@ -1,16 +0,0 @@ -PODS: - - ReachabilitySwift (4.3.0) - -DEPENDENCIES: - - ReachabilitySwift - -SPEC REPOS: - https://github.com/cocoapods/specs.git: - - ReachabilitySwift - -SPEC CHECKSUMS: - ReachabilitySwift: 408477d1b6ed9779dba301953171e017c31241f3 - -PODFILE CHECKSUM: f59ea1aa3b1dbf873da9c79c15ed99a9ef75f238 - -COCOAPODS: 1.5.3 diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 02ee9d4e..7db4c8e3 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -52,7 +52,15 @@ open class CloudCore { // MARK: - Properties private(set) static var coreDataObserver: CoreDataObserver? - + public static var isOnline: Bool { + get { + return coreDataObserver?.isOnline ?? false + } + set { + coreDataObserver?.isOnline = newValue + } + } + /// CloudCore configuration, it's recommended to set up before calling any of CloudCore methods. You can read more at `CloudCoreConfig` struct description public static var config = CloudCoreConfig() diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index a90bfefb..f97e7b5f 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -9,9 +9,6 @@ import Foundation import CoreData import CloudKit -#if os(iOS) -import Reachability -#endif /// Class responsible for taking action on Core Data changes class CoreDataObserver { @@ -26,16 +23,13 @@ class CoreDataObserver { weak var delegate: CloudCoreDelegate? var usePersistentHistoryForPush = false - #if os(iOS) - var reachability: Reachability? var isOnline = true { didSet { - if isOnline != oldValue && isOnline == true { + if isOnline != oldValue && isOnline == true && usePersistentHistoryForPush == true { processPersistentHistory() } } } - #endif public init(container: NSPersistentContainer) { self.container = container @@ -49,11 +43,6 @@ class CoreDataObserver { { usePersistentHistoryForPush = persistentHistoryNumber.boolValue } - #if os(iOS) - if usePersistentHistoryForPush { - reachability = Reachability(hostname: "icloud.com") - } - #endif } } @@ -66,25 +55,12 @@ class CoreDataObserver { NotificationCenter.default.addObserver(self, selector: #selector(self.didSave(notification:)), name: .NSManagedObjectContextDidSave, - object: nil) - - #if os(iOS) - NotificationCenter.default.addObserver(self, - selector: #selector(reachabilityChanged(notification:)), - name: .reachabilityChanged, - object: reachability) - - try? reachability?.startNotifier() - #endif + object: nil) } /// Remove Core Data observers func stop() { - NotificationCenter.default.removeObserver(self) - - #if os(iOS) - reachability?.stopNotifier() - #endif + NotificationCenter.default.removeObserver(self) } deinit { @@ -170,15 +146,7 @@ class CoreDataObserver { } } } - - #if os(iOS) - @objc private func reachabilityChanged(notification: Notification) { - let reachability = notification.object as! Reachability - isOnline = reachability.connection != .none - } - #endif - func processPersistentHistory() { #if os(iOS) guard isOnline else { return } From 5ad3e46374963c7d9b1a621fc0f59090ef214084 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 16:40:27 -0800 Subject: [PATCH 056/203] update travis to build project instead of workspace --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 52beecfb..5cc99f0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,9 +17,9 @@ before_install: script: - set -o pipefail - - xcodebuild -workspace "CloudCore.xcworkspace" -scheme CloudCore -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter` + - xcodebuild -scheme CloudCore -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter` - if [ "$SKIP_TEST" != "YES" ]; then - xcodebuild -workspace "CloudCore.xcworkspace" -scheme CloudCore -destination "$DESTINATION" test | xcpretty -f `xcpretty-travis-formatter`; + xcodebuild -scheme CloudCore -destination "$DESTINATION" test | xcpretty -f `xcpretty-travis-formatter`; fi # Example From e2192b8d5ac055d15bbf689f83327e3a253d8dc7 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 16:49:20 -0800 Subject: [PATCH 057/203] clean out the last of the Cocoapods from project --- CloudCore.xcodeproj/project.pbxproj | 51 ----------------------------- 1 file changed, 51 deletions(-) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 1ed4930d..2118f0a4 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57505AAF21A7591500D9CF8F /* PullResult.swift */; }; - B34DB05E7C4B442CDBC5475B /* Pods_CloudCoreTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; @@ -189,7 +188,6 @@ buildActionMask = 2147483647; files = ( E29BB22D1E436F310020F5B6 /* CloudCore.framework in Frameworks */, - B34DB05E7C4B442CDBC5475B /* Pods_CloudCoreTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -574,11 +572,9 @@ isa = PBXNativeTarget; buildConfigurationList = E29BB2301E436F310020F5B6 /* Build configuration list for PBXNativeTarget "CloudCoreTests" */; buildPhases = ( - CA0ECDA8B5B1B5A7FFF86F6A /* [CP] Check Pods Manifest.lock */, E29BB2241E436F310020F5B6 /* Sources */, E29BB2251E436F310020F5B6 /* Frameworks */, E29BB2261E436F310020F5B6 /* Resources */, - 5DDB158E70ED5BCED209F262 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -687,53 +683,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 5DDB158E70ED5BCED209F262 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreTests/Pods-CloudCoreTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - CA0ECDA8B5B1B5A7FFF86F6A /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CloudCoreTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ D5B2E89A1C3A780C00C0327D /* Sources */ = { isa = PBXSourcesBuildPhase; From c47cc098ce8bdf51193d13567e066af26f946521 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 16:59:47 -0800 Subject: [PATCH 058/203] fix use of NSPersistentHistory in CoreDataTestCase --- Tests/Shared/CoreDataTestCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Shared/CoreDataTestCase.swift b/Tests/Shared/CoreDataTestCase.swift index d0ac2b19..5bbe9833 100644 --- a/Tests/Shared/CoreDataTestCase.swift +++ b/Tests/Shared/CoreDataTestCase.swift @@ -22,7 +22,7 @@ class CoreDataTestCase: XCTestCase { let container = NSPersistentContainer(name: "model", managedObjectModel: model) let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType - if #available(iOS 11.0, watchOS 4.0, *) { + if #available(iOS 11.0, watchOS 4.0, tvOS 11.0, OSX 10.13, *) { description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) } container.persistentStoreDescriptions = [description] From f1323b9f77735cf238293358cf23e2cdc8e11dc9 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 7 Dec 2018 17:25:44 -0800 Subject: [PATCH 059/203] updating podspec for Travis CI --- CloudCore.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index cd921531..6b89e2fe 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -2,11 +2,11 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit (iCloud) and Core Data." s.version = "3.0" - s.homepage = "https://github.com/sorix/CloudCore" + s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "Vasily Ulianov" => "vasily@me.com", "deeje" => "deeje@mac.com" } s.source = { - :git => "https://github.com/sorix/CloudCore.git", + :git => "https://github.com/deeje/CloudCore.git", :tag => s.version.to_s } From c360a6da602579ce134af02aea5a069eba173547 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 9 Dec 2018 13:34:12 -0800 Subject: [PATCH 060/203] skip unkown record values, probably newer schema --- .../RecordToCoreDataOperation.swift | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index c1767d85..e77eeed1 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -81,22 +81,26 @@ class RecordToCoreDataOperation: AsynchronousOperation { for key in record.allKeys() { let recordValue = record.value(forKey: key) - let attribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) - let coreDataValue = try attribute.makeCoreDataValue() + let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) + let coreDataValue = try ckAttribute.makeCoreDataValue() - if let attribute = object.entity.attributesByName[key], attribute.attributeType == .transformableAttributeType, - let data = coreDataValue as? Data { - if let name = attribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { - let value = transformer.transformedValue(coreDataValue) - object.setValue(value, forKey: key) - } else if let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: data) { - object.setValue(unarchivedObject, forKey: key) + if let cdAttribute = object.entity.attributesByName[key] { + if cdAttribute.attributeType == .transformableAttributeType, + let data = coreDataValue as? Data { + if let name = cdAttribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { + let value = transformer.transformedValue(coreDataValue) + object.setValue(value, forKey: key) + } else if let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: data) { + object.setValue(unarchivedObject, forKey: key) + } else { + object.setValue(coreDataValue, forKey: key) + } } else { object.setValue(coreDataValue, forKey: key) + missingObjectsPerEntities[object] = ckAttribute.notFoundRecordNamesForAttribute } } else { - object.setValue(coreDataValue, forKey: key) - missingObjectsPerEntities[object] = attribute.notFoundRecordNamesForAttribute + // skipping unkown record values from server, probably newer schema } } From 3c988e4d211c84d744adec16a84fe589851f3e96 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 10 Dec 2018 21:40:31 -0800 Subject: [PATCH 061/203] Properly ignore unknown data from CloudKit --- .../RecordToCoreDataOperation.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index e77eeed1..8e7556bc 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -84,23 +84,21 @@ class RecordToCoreDataOperation: AsynchronousOperation { let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) let coreDataValue = try ckAttribute.makeCoreDataValue() - if let cdAttribute = object.entity.attributesByName[key] { - if cdAttribute.attributeType == .transformableAttributeType, - let data = coreDataValue as? Data { - if let name = cdAttribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { - let value = transformer.transformedValue(coreDataValue) - object.setValue(value, forKey: key) - } else if let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: data) { - object.setValue(unarchivedObject, forKey: key) - } else { - object.setValue(coreDataValue, forKey: key) - } + if let cdAttribute = object.entity.attributesByName[key], cdAttribute.attributeType == .transformableAttributeType, + let data = coreDataValue as? Data { + if let name = cdAttribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { + let value = transformer.transformedValue(coreDataValue) + object.setValue(value, forKey: key) + } else if let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: data) { + object.setValue(unarchivedObject, forKey: key) } else { object.setValue(coreDataValue, forKey: key) - missingObjectsPerEntities[object] = ckAttribute.notFoundRecordNamesForAttribute } } else { - // skipping unkown record values from server, probably newer schema + if object.entity.attributesByName[key] != nil || object.entity.relationshipsByName[key] != nil { + object.setValue(coreDataValue, forKey: key) + } + missingObjectsPerEntities[object] = ckAttribute.notFoundRecordNamesForAttribute } } From 0e9846643957a69ed005ef1167937e86ae1eb7d2 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 27 Dec 2018 15:33:35 -0800 Subject: [PATCH 062/203] enable editing of shared data --- Source/Classes/Pull/PullOperation.swift | 2 +- Source/Classes/Push/CoreDataObserver.swift | 3 ++- .../ObjectToRecordConverter.swift | 18 +++++++++++-- .../ObjectToRecordOperation.swift | 4 +-- .../Setup/CreateCloudCoreZoneOperation.swift | 2 +- Source/Classes/Setup/SubscribeOperation.swift | 2 +- Source/Extensions/NSManagedObject.swift | 27 ++++++++++++++++++- Source/Model/CloudCoreConfig.swift | 5 +++- .../Extensions/NSManagedObjectTests.swift | 2 +- Tests/Shared/CorrectObject.swift | 2 +- 10 files changed, 55 insertions(+), 12 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 1d9c2026..90d409c0 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -201,7 +201,7 @@ public class PullOperation: Operation { // Our token is expired, we need to refetch everything again case .changeTokenExpired: tokens.tokensByRecordZoneID[zoneId] = nil - self.addRecordZoneChangesOperation(recordZoneIDs: [CloudCore.config.zoneID], database: database, context: context) + self.addRecordZoneChangesOperation(recordZoneIDs: [zoneId], database: database, context: context) default: errorBlock?(cloudError) } } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index f97e7b5f..2999b05f 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -186,7 +186,8 @@ class CoreDataObserver { if change.tombstone != nil { if let privateRecordData = change.tombstone!["privateRecordData"] as? Data { let ckRecord = CKRecord(archivedData: privateRecordData) - let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.privateCloudDatabase) + let database = ckRecord?.recordID.zoneID.ownerName == CKCurrentUserDefaultName ? CloudCore.config.container.privateCloudDatabase : CloudCore.config.container.sharedCloudDatabase + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, database) deletedRecordIDs.append(recordIDWithDatabase) } if let publicRecordData = change.tombstone!["publicRecordData"] as? Data { diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 31c26c4d..37bacb1b 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -84,7 +84,8 @@ class ObjectToRecordConverter { convertOperation.conversionCompletionBlock = { [weak self] record in guard let me = self else { return } - let cloudDatabase = me.database(for: scope) + let targetScope = me.targetScope(for: scope, and: object) + let cloudDatabase = me.database(for: targetScope) let recordWithDB = RecordWithDatabase(record, cloudDatabase) me.convertedRecords.append(recordWithDB) } @@ -108,7 +109,8 @@ class ObjectToRecordConverter { for scope in serviceAttributeNames.scopes { if let triedRestoredRecord = try? object.restoreRecordWithSystemFields(for: scope), let restoredRecord = triedRestoredRecord { - let database = self.database(for: scope) + let targetScope = self.targetScope(for: scope, and: object) + let database = self.database(for: targetScope) let recordIDWithDB = RecordIDWithDatabase(restoredRecord.recordID, database) recordIDs.append(recordIDWithDB) } @@ -143,4 +145,16 @@ class ObjectToRecordConverter { private func database(for scope: CKDatabase.Scope) -> CKDatabase { return CloudCore.config.container.database(with: scope) } + + private func targetScope(for scope: CKDatabase.Scope, and object: NSManagedObject) -> CKDatabase.Scope { + var target = scope + if scope == .private + { + if object.sharingOwnerName != CKCurrentUserDefaultName { + target = .shared + } + } + + return target + } } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index eba91859..aca99056 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -73,8 +73,8 @@ class ObjectToRecordOperation: Operation { record.setValue(references, forKey: attributeName) if let parentRef = references as? CKRecord.Reference, - parentRef.recordID.zoneID == CloudCore.config.zoneID, - let parentAttributeName = managedObject.entity.userInfo?[ServiceAttributeNames.keyParent] as? String, + parentRef.recordID.zoneID.ownerName == managedObject.sharingOwnerName, + let parentAttributeName = managedObject.parentAttributeName, parentAttributeName == attributeName { record.setParent(parentRef.recordID) diff --git a/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift b/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift index 7cef4c4c..e138bd61 100644 --- a/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift +++ b/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift @@ -17,7 +17,7 @@ class CreateCloudCoreZoneOperation: AsynchronousOperation { override func main() { super.main() - let cloudCoreZone = CKRecordZone(zoneName: CloudCore.config.zoneID.zoneName) + let cloudCoreZone = CKRecordZone(zoneName: CloudCore.config.zoneName) let recordZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: [cloudCoreZone], recordZoneIDsToDelete: nil) recordZoneOperation.modifyRecordZonesCompletionBlock = { if let error = $2 { diff --git a/Source/Classes/Setup/SubscribeOperation.swift b/Source/Classes/Setup/SubscribeOperation.swift index 81280de0..64f6eae7 100644 --- a/Source/Classes/Setup/SubscribeOperation.swift +++ b/Source/Classes/Setup/SubscribeOperation.swift @@ -55,7 +55,7 @@ class SubscribeOperation: AsynchronousOperation { notificationInfo.shouldSendContentAvailable = true let subscription = (database == CloudCore.config.container.sharedCloudDatabase) ?CKDatabaseSubscription(subscriptionID: id) : - CKRecordZoneSubscription(zoneID: CloudCore.config.zoneID, subscriptionID: id) + CKRecordZoneSubscription(zoneID: CloudCore.config.privateZoneID(), subscriptionID: id) subscription.notificationInfo = notificationInfo let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) diff --git a/Source/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift index a00e1b0e..1bc325e2 100644 --- a/Source/Extensions/NSManagedObject.swift +++ b/Source/Extensions/NSManagedObject.swift @@ -51,7 +51,8 @@ extension NSManagedObject { aRecord = publicRecord } else { - let privateRecordID = CKRecord.ID(recordName: recordName!, zoneID: CloudCore.config.zoneID) + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: self.sharingOwnerName) + let privateRecordID = CKRecord.ID(recordName: recordName!, zoneID: zoneID) let privateRecord = CKRecord(recordType: entityName, recordID: privateRecordID) self.setValue(privateRecord.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) @@ -66,6 +67,30 @@ extension NSManagedObject { return aRecord } + var parentAttributeName: String? { + get { + return entity.userInfo?[ServiceAttributeNames.keyParent] as? String + } + } + + var sharingOwnerName: String { + get { + if let parentAttributeName = parentAttributeName, + let parent: NSManagedObject = value(forKey: parentAttributeName) as? NSManagedObject, + let serviceAttributes = parent.entity.serviceAttributeNames, + let parentOwnerName: String = parent.value(forKey: serviceAttributes.ownerName) as? String + { + return parentOwnerName + } else if let serviceAttributes = entity.serviceAttributeNames, + let ownerName: String = value(forKey: serviceAttributes.ownerName) as? String + { + return ownerName + } + + return CKCurrentUserDefaultName + } + } + } extension NSManagedObject { diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 837f56b4..3e96b043 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -37,7 +37,10 @@ public struct CloudCoreConfig { /// RecordZone inside private database to store CoreData. /// /// Default value is `CloudCore` - public var zoneID = CKRecordZone.ID(zoneName: "CloudCore", ownerName: CKCurrentUserDefaultName) + public var zoneName = "CloudCore" + public func privateZoneID() -> CKRecordZone.ID { + return CKRecordZone.ID(zoneName: zoneName, ownerName: CKCurrentUserDefaultName) + } let subscriptionIDForPrivateDB = "CloudCorePrivate" let subscriptionIDForSharedDB = "CloudCoreShared" diff --git a/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift b/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift index dd5600d5..7b15fa91 100644 --- a/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift +++ b/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift @@ -21,7 +21,7 @@ class NSManagedObjectTests: CoreDataTestCase { let record = try object.restoreRecordWithSystemFields(for: .private) XCTAssertEqual(record?.recordType, "TestEntity") - XCTAssertEqual(record?.recordID.zoneID, CloudCore.config.zoneID) + XCTAssertEqual(record?.recordID.zoneID, CloudCore.config.privateZoneID()) } catch { XCTFail("\(error)") } diff --git a/Tests/Shared/CorrectObject.swift b/Tests/Shared/CorrectObject.swift index 5c96ae15..e49f041b 100644 --- a/Tests/Shared/CorrectObject.swift +++ b/Tests/Shared/CorrectObject.swift @@ -76,7 +76,7 @@ struct CorrectObject { } func makeRecord() -> CKRecord { - let record = CKRecord(recordType: "TestEntity", zoneID: CloudCore.config.zoneID) + let record = CKRecord(recordType: "TestEntity", zoneID: CloudCore.config.privateZoneID()) let asset = try? CoreDataAttribute.createAsset(for: externalBinary) XCTAssertNotNil(asset) From 36fb370c2ab2652145179c43f4fcbda31291d93c Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 2 Jan 2019 14:02:14 -0800 Subject: [PATCH 063/203] Tokens now fully managed by CloudCore --- README.md | 4 ---- Source/Classes/Pull/PullOperation.swift | 14 +++++++++---- Source/Model/Tokens.swift | 26 ++++++++----------------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 7375e044..b49e8410 100755 --- a/README.md +++ b/README.md @@ -81,10 +81,6 @@ func application(_ application: UIApplication, didReceiveRemoteNotification user } } -func applicationWillTerminate(_ application: UIApplication) { - // Save tokens on exit used to differential sync - CloudCore.tokens.saveToUserDefaults() -} ``` 5. If you want to enable offline support, **enable NSPersistentHistoryTracking** when you initialize your Core Data stack diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 90d409c0..ef504a5e 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -56,7 +56,7 @@ public class PullOperation: Operation { for database in self.databases { var changedZoneIDs = [CKRecordZone.ID]() var deletedZoneIDs = [CKRecordZone.ID]() - let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope] + let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope.rawValue] let databaseChangeOp = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) databaseChangeOp.database = database databaseChangeOp.recordZoneWithIDChangedBlock = { (recordZoneID) in @@ -68,10 +68,14 @@ public class PullOperation: Operation { databaseChangeOp.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in // TODO: error handling? - self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) - self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) + if changedZoneIDs.count > 0 { + self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) + } + if deletedZoneIDs.count > 0 { + self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) + } - self.tokens.tokensByDatabaseScope[database.databaseScope] = changeToken + self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken } self.queue.addOperation(databaseChangeOp) } @@ -84,6 +88,8 @@ public class PullOperation: Operation { errorBlock?(error) } + tokens.saveToUserDefaults() + CloudCore.delegate?.didSyncFromCloud() } diff --git a/Source/Model/Tokens.swift b/Source/Model/Tokens.swift index dcf650c9..0bf3556b 100644 --- a/Source/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -9,32 +9,22 @@ import CloudKit /** - CloudCore's class for storing global `CKToken` objects. Framework uses one to upload or download only changed data (smart-sync). - - To detect what data is new and old, framework uses CloudKit's `CKToken` objects and it is needed to be loaded every time application launches and saved on exit. + CloudCore's class for storing global `CKToken` objects. Framework uses one to download only changed data (smart-sync). Framework stores tokens in 2 places: * singleton `Tokens` object in `CloudCore.tokens` * tokens per record inside *Record Data* attribute, it is managed automatically you don't need to take any actions about that token - - You need to save `Tokens` object before application terminates otherwise you will loose smart-sync ability. - - ### Example - ```swift - func applicationWillTerminate(_ application: UIApplication) { - CloudCore.tokens.saveToUserDefaults() - } - ``` */ + open class Tokens: NSObject, NSCoding { + var tokensByDatabaseScope = [Int: CKServerChangeToken]() var tokensByRecordZoneID = [CKRecordZone.ID: CKServerChangeToken]() - var tokensByDatabaseScope = [CKDatabase.Scope: CKServerChangeToken]() private struct ArchiverKey { - static let tokensByRecordZoneID = "tokensByRecordZoneID" static let tokensByDatabaseScope = "tokensByDatabaseScope" + static let tokensByRecordZoneID = "tokensByRecordZoneID" } /// Create fresh object without any Tokens inside. Can be used to fetch full data. @@ -67,18 +57,18 @@ open class Tokens: NSObject, NSCoding { /// Returns an object initialized from data in a given unarchiver. public required init?(coder aDecoder: NSCoder) { + if let decodedTokensByScope = aDecoder.decodeObject(forKey: ArchiverKey.tokensByDatabaseScope) as? [Int: CKServerChangeToken] { + self.tokensByDatabaseScope = decodedTokensByScope + } if let decodedTokensByZone = aDecoder.decodeObject(forKey: ArchiverKey.tokensByRecordZoneID) as? [CKRecordZone.ID: CKServerChangeToken] { self.tokensByRecordZoneID = decodedTokensByZone } - if let decodedTokensByScope = aDecoder.decodeObject(forKey: ArchiverKey.tokensByDatabaseScope) as? [CKDatabase.Scope: CKServerChangeToken] { - self.tokensByDatabaseScope = decodedTokensByScope - } } /// Encodes the receiver using a given archiver. open func encode(with aCoder: NSCoder) { - aCoder.encode(tokensByRecordZoneID, forKey: ArchiverKey.tokensByRecordZoneID) aCoder.encode(tokensByDatabaseScope, forKey: ArchiverKey.tokensByDatabaseScope) + aCoder.encode(tokensByRecordZoneID, forKey: ArchiverKey.tokensByRecordZoneID) } } From c1624bdf72f82b7c8015ff045ebdbba5653860b3 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 2 Jan 2019 14:05:43 -0800 Subject: [PATCH 064/203] remove applicationWillTerminate from Example.app --- Example/Sources/AppDelegate.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index ead32b63..e5654cb6 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -57,12 +57,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele }) } } - - func applicationWillTerminate(_ application: UIApplication) { - // Save tokens on exit used to differential sync - CloudCore.tokens.saveToUserDefaults() - } - + // MARK: - Default Apple initialization, you can skip that var window: UIWindow? From d3bb2581aeb29122da6157540434bb7c0c9795d7 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 10 Jan 2019 08:54:47 -0800 Subject: [PATCH 065/203] Pull records serially, RecordToCD.performAndWait --- Source/Classes/Pull/PullOperation.swift | 1 + .../Classes/Pull/SubOperations/RecordToCoreDataOperation.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index ef504a5e..7621943b 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -42,6 +42,7 @@ public class PullOperation: Operation { self.persistentContainer = persistentContainer queue.name = "PullQueue" + queue.maxConcurrentOperationCount = 1 } /// Performs the receiver’s non-concurrent task. diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 8e7556bc..2177a677 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -35,7 +35,7 @@ class RecordToCoreDataOperation: AsynchronousOperation { override func main() { if self.isCancelled { return } - parentContext.perform { + parentContext.performAndWait { do { try self.setManagedObject(in: self.parentContext) } catch { From 6603267ae88bd1a5ad5625669b06c78ccad096b9 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 15 Jan 2019 10:45:57 -0800 Subject: [PATCH 066/203] on launch, check for pending changes --- Source/Classes/Push/CoreDataObserver.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 2999b05f..d5a44116 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -43,6 +43,10 @@ class CoreDataObserver { { usePersistentHistoryForPush = persistentHistoryNumber.boolValue } + + if usePersistentHistoryForPush { + processPersistentHistory() + } } } From 5a6a56d4eca76a8610bdd0d4858e73fc5b26551b Mon Sep 17 00:00:00 2001 From: Harish Yerra Date: Fri, 18 Jan 2019 18:26:28 -0600 Subject: [PATCH 067/203] Make sure only app extension APIs only are used to prevent warnings being thrown in WatchKit. --- CloudCore.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 2118f0a4..f765f31c 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -918,6 +918,7 @@ D5B2E8B41C3A780C00C0327D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; @@ -943,6 +944,7 @@ D5B2E8B51C3A780C00C0327D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; From a0b1c693ca42eeec76d0aad2910d9cc8e877bf6b Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 12 Apr 2019 17:45:33 -0700 Subject: [PATCH 068/203] support for query subscriptions on public database --- Source/Classes/CloudCore.swift | 14 +- .../FetchPublicSubscriptionsOperation.swift | 72 ++++--- .../PublicDatabaseSubscriptions.swift | 162 +++++++------- Source/Classes/Pull/PullOperation.swift | 200 +++++++++++------- Source/Enum/PullResult.swift | 2 +- 5 files changed, 250 insertions(+), 200 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 7db4c8e3..e924b095 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -134,11 +134,13 @@ open class CloudCore { - completion: `PullResult` enumeration with results of operation */ public static func pull(using userInfo: NotificationUserInfo, to container: NSPersistentContainer, error: ErrorBlock?, completion: @escaping (_ fetchResult: PullResult) -> Void) { - guard let cloudDatabase = self.database(for: userInfo) else { + let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) + + guard let cloudDatabase = self.database(for: notification) else { completion(.noData) return } - + DispatchQueue.global(qos: .utility).async { let errorProxy = ErrorBlockProxy(destination: error) let operation = PullOperation(from: [cloudDatabase], persistentContainer: container) @@ -174,13 +176,11 @@ open class CloudCore { - Returns: `true` if notification contains CloudCore data */ public static func isCloudCoreNotification(withUserInfo userInfo: NotificationUserInfo) -> Bool { - return (database(for: userInfo) != nil) + let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) + return (database(for: notification) != nil) } - static func database(for notificationUserInfo: NotificationUserInfo) -> CKDatabase? { - guard let notificationDictionary = notificationUserInfo as? [String: NSObject] else { return nil } - let notification = CKNotification(fromRemoteNotificationDictionary: notificationDictionary) - + static func database(for notification: CKNotification) -> CKDatabase? { guard let id = notification.subscriptionID else { return nil } switch id { diff --git a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift index bb1ceed0..7c89a57b 100644 --- a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift +++ b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift @@ -9,38 +9,40 @@ import CloudKit /// Fetch CloudCore's subscriptions from Public CKDatabase -// TODO: Add Public support in future versions -//class FetchPublicSubscriptionsOperation: AsynchronousOperation { -// var errorBlock: ErrorBlock? -// var fetchCompletionBlock: (([CKSubscription]) -> Void)? -// -// private let prefix = CloudCore.config.publicSubscriptionIDPrefix -// -// override func main() { -// super.main() -// -// CKContainer.default().publicCloudDatabase.fetchAllSubscriptions { (subscriptions, error) in -// defer { -// self.state = .finished -// } -// -// if let error = error { -// self.errorBlock?(error) -// return -// } -// -// guard let subscriptions = subscriptions else { -// self.fetchCompletionBlock?([CKSubscription]()) -// return -// } -// -// var cloudCoreSubscriptions = [CKSubscription]() -// for subscription in subscriptions { -// if !subscription.subscriptionID.hasPrefix(self.prefix) { continue } -// cloudCoreSubscriptions.append(subscription) -// } -// -// self.fetchCompletionBlock?(cloudCoreSubscriptions) -// } -// } -//} + +#if !os(watchOS) +class FetchPublicSubscriptionsOperation: AsynchronousOperation { + var errorBlock: ErrorBlock? + var fetchCompletionBlock: (([CKSubscription]) -> Void)? + + private let prefix = CloudCore.config.publicSubscriptionIDPrefix + + override func main() { + super.main() + + CKContainer.default().publicCloudDatabase.fetchAllSubscriptions { (subscriptions, error) in + defer { + self.state = .finished + } + + if let error = error { + self.errorBlock?(error) + return + } + + guard let subscriptions = subscriptions else { + self.fetchCompletionBlock?([CKSubscription]()) + return + } + + var cloudCoreSubscriptions = [CKSubscription]() + for subscription in subscriptions { + if !subscription.subscriptionID.hasPrefix(self.prefix) { continue } + cloudCoreSubscriptions.append(subscription) + } + + self.fetchCompletionBlock?(cloudCoreSubscriptions) + } + } +} +#endif diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index 68837835..098d2a90 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -8,89 +8,83 @@ import CloudKit -// TODO: Temporarily disabled, in development +// Use that class to manage subscriptions to public CloudKit database. +// If you want to sync some records with public database you need to subsrcibe for notifications on that changes to enable iCloud -> Local database syncing. +#if !os(watchOS) +public class PublicDatabaseSubscriptions { + + private static var prefix: String { return CloudCore.config.publicSubscriptionIDPrefix } + + static var cachedIDs = [String]() + + // Create `CKQuerySubscription` for public database, use it if you want to enable syncing public iCloud -> Core Data + // + // - Parameters: + // - recordType: The string that identifies the type of records to track. You are responsible for naming your app’s record types. This parameter must not be empty string. + // - predicate: The matching criteria to apply to the records. This parameter must not be nil. For information about the operators that are supported in search predicates, see the discussion in [CKQuery](apple-reference-documentation://hsDjQFvil9). + // - completion: returns subscriptionID and error upon operation completion + static public func subscribe(recordType: String, predicate: NSPredicate, completion: ((_ subscriptionID: String, _ error: Error?) -> Void)?) { + let id = prefix + recordType + "-" + predicate.predicateFormat + if let index = self.cachedIDs.index(of: id) { return } + + let options: CKQuerySubscription.Options = [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion] + let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: id, options: options) + + let notificationInfo = CKSubscription.NotificationInfo() + notificationInfo.shouldSendContentAvailable = true + subscription.notificationInfo = notificationInfo + + let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) + operation.modifySubscriptionsCompletionBlock = { _, _, error in + if error == nil { + self.cachedIDs.append(subscription.subscriptionID) + } -/// Use that class to manage subscriptions to public CloudKit database. -/// If you want to sync some records with public database you need to subsrcibe for notifications on that changes to enable iCloud -> Local database syncing. -//class PublicDatabaseSubscriptions { -// -// private static var userDefaultsKey: String { return CloudCore.config.userDefaultsKeyTokens } -// private static var prefix: String { return CloudCore.config.publicSubscriptionIDPrefix } -// -// internal(set) static var cachedIDs = UserDefaults.standard.stringArray(forKey: userDefaultsKey) ?? [String]() -// -// /// Create `CKQuerySubscription` for public database, use it if you want to enable syncing public iCloud -> Core Data -// /// -// /// - Parameters: -// /// - recordType: The string that identifies the type of records to track. You are responsible for naming your app’s record types. This parameter must not be empty string. -// /// - predicate: The matching criteria to apply to the records. This parameter must not be nil. For information about the operators that are supported in search predicates, see the discussion in [CKQuery](apple-reference-documentation://hsDjQFvil9). -// /// - completion: returns subscriptionID and error upon operation completion -// static func subscribe(recordType: String, predicate: NSPredicate, completion: ((_ subscriptionID: String, _ error: Error?) -> Void)?) { -// let id = prefix + UUID().uuidString -// let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: id, options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]) -// -// let notificationInfo = CKNotificationInfo() -// notificationInfo.shouldSendContentAvailable = true -// subscription.notificationInfo = notificationInfo -// -// let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) -// operation.modifySubscriptionsCompletionBlock = { _, _, error in -// if error == nil { -// self.cachedIDs.append(subscription.subscriptionID) -// UserDefaults.standard.set(self.cachedIDs, forKey: self.userDefaultsKey) -// UserDefaults.standard.synchronize() -// } -// -// completion?(subscription.subscriptionID, error) -// } -// -// operation.timeoutIntervalForResource = 20 -// CKContainer.default().publicCloudDatabase.add(operation) -// } -// -// /// Unsubscribe from public database -// /// -// /// - Parameters: -// /// - subscriptionID: id of subscription to remove -// static func unsubscribe(subscriptionID: String, completion: ((Error?) -> Void)?) { -// let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [], subscriptionIDsToDelete: [subscriptionID]) -// operation.modifySubscriptionsCompletionBlock = { _, _, error in -// if error == nil { -// if let index = self.cachedIDs.index(of: subscriptionID) { -// self.cachedIDs.remove(at: index) -// } -// UserDefaults.standard.set(self.cachedIDs, forKey: self.userDefaultsKey) -// UserDefaults.standard.synchronize() -// } -// -// completion?(error) -// } -// -// operation.timeoutIntervalForResource = 20 -// CKContainer.default().publicCloudDatabase.add(operation) -// } -// -// -// /// Refresh local `cachedIDs` variable with actual data from CloudKit. -// /// Recommended to use after application's UserDefaults reset. -// /// -// /// - Parameter completion: called upon operation completion, contains list of CloudCore subscriptions and error -// static func refreshCache(errorCompletion: ErrorBlock? = nil, successCompletion: (([CKSubscription]) -> Void)? = nil) { -// let operation = FetchPublicSubscriptionsOperation() -// operation.errorBlock = errorCompletion -// operation.fetchCompletionBlock = { subscriptions in -// self.setCache(from: subscriptions) -// successCompletion?(subscriptions) -// } -// operation.start() -// } -// -// internal static func setCache(from subscriptions: [CKSubscription]) { -// let ids = subscriptions.map { $0.subscriptionID } -// self.cachedIDs = ids -// -// UserDefaults.standard.set(ids, forKey: self.userDefaultsKey) -// UserDefaults.standard.synchronize() -// } -//} + completion?(subscription.subscriptionID, error) + } + + operation.timeoutIntervalForResource = 20 + CKContainer.default().publicCloudDatabase.add(operation) + } + + // Unsubscribe from public database + // + // - Parameters: + // - subscriptionID: id of subscription to remove + static public func unsubscribe(subscriptionID: String, completion: ((Error?) -> Void)?) { + let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [], subscriptionIDsToDelete: [subscriptionID]) + operation.modifySubscriptionsCompletionBlock = { _, _, error in + if error == nil { + if let index = self.cachedIDs.index(of: subscriptionID) { + self.cachedIDs.remove(at: index) + } + } + + completion?(error) + } + + operation.timeoutIntervalForResource = 20 + CKContainer.default().publicCloudDatabase.add(operation) + } + + + // Refresh local `cachedIDs` variable with actual data from CloudKit. + // Recommended to use after application's UserDefaults reset. + // + // - Parameter completion: called upon operation completion, contains list of CloudCore subscriptions and error + static public func refreshCache(errorCompletion: ErrorBlock? = nil, successCompletion: (([CKSubscription]) -> Void)? = nil) { + let operation = FetchPublicSubscriptionsOperation() + operation.errorBlock = errorCompletion + operation.fetchCompletionBlock = { subscriptions in + self.setCache(from: subscriptions) + successCompletion?(subscriptions) + } + operation.start() + } + internal static func setCache(from subscriptions: [CKSubscription]) { + self.cachedIDs = subscriptions.map { $0.subscriptionID } + } + +} +#endif diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 7621943b..d9eba7c8 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -14,33 +14,37 @@ public class PullOperation: Operation { /// Private cloud database for the CKContainer specified by CloudCoreConfig public static let allDatabases = [ -// CloudCore.config.container.publicCloudDatabase, + CloudCore.config.container.publicCloudDatabase, CloudCore.config.container.privateCloudDatabase, CloudCore.config.container.sharedCloudDatabase ] public typealias NotificationUserInfo = [AnyHashable : Any] - private let tokens: Tokens private let databases: [CKDatabase] private let persistentContainer: NSPersistentContainer - + private let tokens: Tokens + /// Called every time if error occurs public var errorBlock: ErrorBlock? private let queue = OperationQueue() + private var objectsWithMissingReferences = [MissingReferences]() + /// Initialize operation, it's recommended to set `errorBlock` /// /// - Parameters: /// - databases: list of databases to fetch data from (only private is supported now) /// - persistentContainer: `NSPersistentContainer` that will be used to save data /// - tokens: previously saved `Tokens`, you can generate new ones if you want to fetch all data - public init(from databases: [CKDatabase] = PullOperation.allDatabases, persistentContainer: NSPersistentContainer, tokens: Tokens = CloudCore.tokens) { - self.tokens = tokens + public init(from databases: [CKDatabase] = PullOperation.allDatabases, + persistentContainer: NSPersistentContainer, + tokens: Tokens = CloudCore.tokens) { self.databases = databases self.persistentContainer = persistentContainer - + self.tokens = tokens + queue.name = "PullQueue" queue.maxConcurrentOperationCount = 1 } @@ -53,34 +57,73 @@ public class PullOperation: Operation { let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.name = CloudCore.config.pullContextName - - for database in self.databases { - var changedZoneIDs = [CKRecordZone.ID]() - var deletedZoneIDs = [CKRecordZone.ID]() - let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope.rawValue] - let databaseChangeOp = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) - databaseChangeOp.database = database - databaseChangeOp.recordZoneWithIDChangedBlock = { (recordZoneID) in - changedZoneIDs.append(recordZoneID) - } - databaseChangeOp.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in - deletedZoneIDs.append(recordZoneID) - } - databaseChangeOp.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in - // TODO: error handling? - - if changedZoneIDs.count > 0 { - self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) + + for database in self.databases { + if database.databaseScope == .public { + let changedRecordIDs: NSMutableSet = [] + let deletedRecordIDs: NSMutableSet = [] + let previousToken = self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] + let notesOp = CKFetchNotificationChangesOperation(previousServerChangeToken: previousToken) + notesOp.notificationChangedBlock = { (innerNotification) in + if let innerQueryNotification = innerNotification as? CKQueryNotification { + if innerQueryNotification.queryNotificationReason == .recordDeleted { + deletedRecordIDs.add(innerQueryNotification.recordID!) + } else { + changedRecordIDs.add(innerQueryNotification.recordID!) + } + } } - if deletedZoneIDs.count > 0 { - self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) + notesOp.fetchNotificationChangesCompletionBlock = { (changeToken, error) in + let allChangedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] + let fetch = CKFetchRecordsOperation(recordIDs: allChangedRecordIDs) + fetch.database = CloudCore.config.container.publicCloudDatabase + fetch.perRecordCompletionBlock = { (record, recordID, error) in + if error == nil { + self.addConvertRecordOperation(record: record!, context: backgroundContext) + } + } + fetch.fetchRecordsCompletionBlock = { (_, error) in + self.processMissingReferences(context: backgroundContext) + } + self.queue.addOperation(fetch) + + let allDeletedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] + for recordID in allDeletedRecordIDs { + self.addDeleteRecordOperation(recordID: recordID, context: backgroundContext) + } + + self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken } + self.queue.addOperation(notesOp) + } else { + var changedZoneIDs = [CKRecordZone.ID]() + var deletedZoneIDs = [CKRecordZone.ID]() - self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken + let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope.rawValue] + let databaseChangeOp = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) + databaseChangeOp.database = database + databaseChangeOp.recordZoneWithIDChangedBlock = { (recordZoneID) in + changedZoneIDs.append(recordZoneID) + } + databaseChangeOp.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in + deletedZoneIDs.append(recordZoneID) + } + databaseChangeOp.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in + // TODO: error handling? + + if changedZoneIDs.count > 0 { + self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) + } + if deletedZoneIDs.count > 0 { + self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) + } + + self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken + } + self.queue.addOperation(databaseChangeOp) } - self.queue.addOperation(databaseChangeOp) - } - + } + self.queue.waitUntilAllOperationsAreFinished() do { @@ -94,27 +137,34 @@ public class PullOperation: Operation { CloudCore.delegate?.didSyncFromCloud() } + private func addConvertRecordOperation(record: CKRecord, context: NSManagedObjectContext) { + // Convert and write CKRecord To NSManagedObject Operation + let convertOperation = RecordToCoreDataOperation(parentContext: context, record: record) + convertOperation.errorBlock = { self.errorBlock?($0) } + convertOperation.completionBlock = { + self.objectsWithMissingReferences.append(convertOperation.missingObjectsPerEntities) + } + self.queue.addOperation(convertOperation) + } + + private func addDeleteRecordOperation(recordID: CKRecord.ID, context: NSManagedObjectContext) { + // Delete NSManagedObject with specified recordID Operation + let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: recordID) + deleteOperation.errorBlock = { self.errorBlock?($0) } + self.queue.addOperation(deleteOperation) + } + private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZone.ID], database: CKDatabase, context: NSManagedObjectContext) { if recordZoneIDs.isEmpty { return } let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) - var objectsWithMissingReferences = [MissingReferences]() recordZoneChangesOperation.recordChangedBlock = { - // Convert and write CKRecord To NSManagedObject Operation - let convertOperation = RecordToCoreDataOperation(parentContext: context, record: $0) - convertOperation.errorBlock = { self.errorBlock?($0) } - convertOperation.completionBlock = { - objectsWithMissingReferences.append(convertOperation.missingObjectsPerEntities) - } - self.queue.addOperation(convertOperation) + self.addConvertRecordOperation(record: $0, context: context) } recordZoneChangesOperation.recordWithIDWasDeletedBlock = { - // Delete NSManagedObject with specified recordID Operation - let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: $0) - deleteOperation.errorBlock = { self.errorBlock?($0) } - self.queue.addOperation(deleteOperation) + self.addDeleteRecordOperation(recordID: $0, context: context) } recordZoneChangesOperation.errorBlock = { zoneID, error in @@ -122,46 +172,50 @@ public class PullOperation: Operation { } recordZoneChangesOperation.completionBlock = { - // iterate over all missing references and fix them, now are all NSManagedObjects created - for missingReferences in objectsWithMissingReferences { - for (object, references) in missingReferences { - guard let serviceAttributes = object.entity.serviceAttributeNames else { continue } - - for (attributeName, recordNames) in references { - for recordName in recordNames { - guard let relationship = object.entity.relationshipsByName[attributeName], let targetEntityName = relationship.destinationEntity?.name else { continue } - - // TODO: move to extension - let fetchRequest = NSFetchRequest(entityName: targetEntityName) - fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordName + " == %@" , recordName) - fetchRequest.fetchLimit = 1 - fetchRequest.includesPropertyValues = false + self.processMissingReferences(context: context) + } + + queue.addOperation(recordZoneChangesOperation) + } + + private func processMissingReferences(context: NSManagedObjectContext) { + // iterate over all missing references and fix them, now are all NSManagedObjects created + for missingReferences in objectsWithMissingReferences { + for (object, references) in missingReferences { + guard let serviceAttributes = object.entity.serviceAttributeNames else { continue } + + for (attributeName, recordNames) in references { + for recordName in recordNames { + guard let relationship = object.entity.relationshipsByName[attributeName], let targetEntityName = relationship.destinationEntity?.name else { continue } + + // TODO: move to extension + let fetchRequest = NSFetchRequest(entityName: targetEntityName) + fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordName + " == %@" , recordName) + fetchRequest.fetchLimit = 1 + fetchRequest.includesPropertyValues = false + + do { + let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject - do { - let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject - - if let foundObject = foundObject { - if relationship.isToMany { - let set = object.value(forKey: attributeName) as? NSMutableSet ?? NSMutableSet() - set.add(foundObject) - object.setValue(set, forKey: attributeName) - } else { - object.setValue(foundObject, forKey: attributeName) - } + if let foundObject = foundObject { + if relationship.isToMany { + let set = object.value(forKey: attributeName) as? NSMutableSet ?? NSMutableSet() + set.add(foundObject) + object.setValue(set, forKey: attributeName) } else { - print("warning: object not found " + recordName) + object.setValue(foundObject, forKey: attributeName) } - } catch { - self.errorBlock?(error) + } else { + print("warning: object not found " + recordName) } + } catch { + self.errorBlock?(error) } } } } } - - queue.addOperation(recordZoneChangesOperation) - } + } private func deleteRecordsFromDeletedZones(recordZoneIDs: [CKRecordZone.ID]) { persistentContainer.performBackgroundTask { (moc) in diff --git a/Source/Enum/PullResult.swift b/Source/Enum/PullResult.swift index a947bee5..40a6514f 100644 --- a/Source/Enum/PullResult.swift +++ b/Source/Enum/PullResult.swift @@ -29,7 +29,7 @@ public enum PullResult: UInt { /// Convert `self` to `UIBackgroundFetchResult` /// /// Very usefull at `application(_:didReceiveRemoteNotification:fetchCompletionHandler)` as `completionHandler` - public var uiBackgroundFetchResult: UIBackgroundFetchResult { + var uiBackgroundFetchResult: UIBackgroundFetchResult { return UIBackgroundFetchResult(rawValue: self.rawValue)! } From 8a58af346d4a9b33e3ca0d8621eeb5533b256f18 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 14 Apr 2019 12:55:27 -0700 Subject: [PATCH 069/203] (silence a warning) --- .../Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index 098d2a90..497ed33b 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -25,7 +25,7 @@ public class PublicDatabaseSubscriptions { // - completion: returns subscriptionID and error upon operation completion static public func subscribe(recordType: String, predicate: NSPredicate, completion: ((_ subscriptionID: String, _ error: Error?) -> Void)?) { let id = prefix + recordType + "-" + predicate.predicateFormat - if let index = self.cachedIDs.index(of: id) { return } + if self.cachedIDs.index(of: id) != nil { return } let options: CKQuerySubscription.Options = [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion] let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: id, options: options) From ac7b7e98fc4a19df1ac37772686ffe9e3f75fd90 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 19 Apr 2019 01:19:13 -0700 Subject: [PATCH 070/203] =?UTF-8?q?don=E2=80=99t=20bother=20updating=20del?= =?UTF-8?q?eted=20records?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/Classes/Pull/PullOperation.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index d9eba7c8..9e8063f4 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -68,6 +68,7 @@ public class PullOperation: Operation { if let innerQueryNotification = innerNotification as? CKQueryNotification { if innerQueryNotification.queryNotificationReason == .recordDeleted { deletedRecordIDs.add(innerQueryNotification.recordID!) + changedRecordIDs.remove(innerQueryNotification.recordID!) } else { changedRecordIDs.add(innerQueryNotification.recordID!) } @@ -87,7 +88,7 @@ public class PullOperation: Operation { } self.queue.addOperation(fetch) - let allDeletedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] + let allDeletedRecordIDs = deletedRecordIDs.allObjects as! [CKRecord.ID] for recordID in allDeletedRecordIDs { self.addDeleteRecordOperation(recordID: recordID, context: backgroundContext) } From bf39fcc00358c889eed868add30f5e2ae1fdf06b Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 23 Apr 2019 11:55:37 -0700 Subject: [PATCH 071/203] add a matching unsubscribe call, generate same id --- .../PublicSubscriptions/PublicDatabaseSubscriptions.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index 497ed33b..7e1a730b 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -68,6 +68,13 @@ public class PublicDatabaseSubscriptions { } + static public func unsubscribe(recordType: String, predicate: NSPredicate, completion: ((Error?) -> Void)?) { + let id = prefix + recordType + "-" + predicate.predicateFormat + + self.unsubscribe(subscriptionID: id, completion: completion) + } + + // Refresh local `cachedIDs` variable with actual data from CloudKit. // Recommended to use after application's UserDefaults reset. // From f7172a5a0f7f1b87d1fa11ebfc0071238fe50de5 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 25 Apr 2019 10:50:41 -0700 Subject: [PATCH 072/203] remove unused/duplicate typealias --- Source/Classes/Pull/PullOperation.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 9e8063f4..00dc169b 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -18,9 +18,7 @@ public class PullOperation: Operation { CloudCore.config.container.privateCloudDatabase, CloudCore.config.container.sharedCloudDatabase ] - - public typealias NotificationUserInfo = [AnyHashable : Any] - + private let databases: [CKDatabase] private let persistentContainer: NSPersistentContainer private let tokens: Tokens From 0be39b48d57d3bcaf83c387aba7ecb093aeef034 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 26 Apr 2019 17:07:35 -0700 Subject: [PATCH 073/203] making RecordToCoreDataOperation for app use --- Source/Classes/AsynchronousOperation.swift | 8 ++++---- .../Pull/SubOperations/RecordToCoreDataOperation.swift | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/Classes/AsynchronousOperation.swift b/Source/Classes/AsynchronousOperation.swift index 564924a9..a287e20f 100644 --- a/Source/Classes/AsynchronousOperation.swift +++ b/Source/Classes/AsynchronousOperation.swift @@ -12,7 +12,7 @@ import Foundation /// ## How to use: /// 1. Call `super.main()` when override `main` method, call `super.start()` when override `start` method. /// 2. When operation is finished or cancelled set `self.state = .finished` -class AsynchronousOperation: Operation { +public class AsynchronousOperation: Operation { open override var isAsynchronous: Bool { return true } open override var isExecuting: Bool { return state == .executing } open override var isFinished: Bool { return state == .finished } @@ -28,14 +28,14 @@ class AsynchronousOperation: Operation { } } - enum State: String { + public enum State: String { case ready = "Ready" case executing = "Executing" case finished = "Finished" fileprivate var keyPath: String { return "is" + self.rawValue } } - override func start() { + override public func start() { if self.isCancelled { state = .finished } else { @@ -44,7 +44,7 @@ class AsynchronousOperation: Operation { } } - override func main() { + override public func main() { if self.isCancelled { state = .finished } else { diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 2177a677..10001414 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -14,7 +14,7 @@ typealias RecordName = String typealias MissingReferences = [NSManagedObject: [AttributeName: [RecordName]]] /// Convert CKRecord to NSManagedObject and save it to parent context, thread-safe -class RecordToCoreDataOperation: AsynchronousOperation { +public class RecordToCoreDataOperation: AsynchronousOperation { let parentContext: NSManagedObjectContext let record: CKRecord var errorBlock: ErrorBlock? @@ -23,7 +23,7 @@ class RecordToCoreDataOperation: AsynchronousOperation { /// - Parameters: /// - parentContext: operation will be safely performed in that context, **operation doesn't save that context** you need to do it manually /// - record: record that will be converted to `NSManagedObject` - init(parentContext: NSManagedObjectContext, record: CKRecord) { + public init(parentContext: NSManagedObjectContext, record: CKRecord) { self.parentContext = parentContext self.record = record @@ -32,7 +32,7 @@ class RecordToCoreDataOperation: AsynchronousOperation { self.name = "RecordToCoreDataOperation" } - override func main() { + override public func main() { if self.isCancelled { return } parentContext.performAndWait { From b0b874eaff6665a90fe56ebd9b64ebfffa21c236 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 6 Jun 2019 15:12:00 -0700 Subject: [PATCH 074/203] udpated ReadMe to include discussion of iOS 13 --- README.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b49e8410..2b459fca 100755 --- a/README.md +++ b/README.md @@ -14,17 +14,43 @@ * **private database** and **shared database** push and pull is supported. * **public database** push is supported * Parent-Child relationships can be defined for CloudKit Sharing -* Respects of Core Data options (cascade deletions, external storage). -* Knows and manages with CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. +* Respects Core Data options (cascade deletions, external storage). +* Knows and manages CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. + +#### CloudCore vs iOS 13? + +At WWDC 2019, Apple announced support for NSPersistentCloudKitContainer in iOS 13, which provides native support for Core Data <-> CloudKit synchronization. Here are some initial thoughts on the differences between these two approaches. + +###### NSPersistentCloudKitContainer +* Simple to enable +* Private Database only, no Sharing or Public support +* Synchronizes All Records +* No CloudKit Metadata (e.g. recordName, systemFields, owner) +* Record-level Synchronization (entire objects are pushed) +* Offline Synchronization is opaque, but doesn't appear to require NSPersistentHistoryTracking +* All Core Data names are preceeded with "CD_" in CloudKit +* Core Data Relationships are mapped thru CDMR records in CloudKit + +###### CloudCore +* Support requires specific configuration in the Core Data Model +* Support for Private, Shared, and Public databases +* Selective Synchronization (e.g. can delete local objects without deleting remote records) +* Explicit CloudKit Metadata +* Field-level Synchronization (only changed attributes are pushed) +* Offline Synchronziation via NSPersistentHistoryTracking +* Core Data names are mapped exactly in CloudKit +* Core Data Relationships are mapped to CloudKit CKReferences + +During their WWDC presentation, Apple very clearly stated that NSPersistentCloudKitContainer is a foundation for future support of more advanced features #YMMV ## How it works? -CloudCore is built using "black box" architecture, so it works invisibly for your application, you just need to add several lines to `AppDelegate` to enable it. Synchronization and error resolving is managed automatically. +CloudCore is built using a "black box" architecture, so it works invisibly for your application. You just need to add several lines to your `AppDelegate` to enable it, as well as identify various aspects of your Core Data Model schema. Synchronization and error resolving is managed automatically. 1. CloudCore stores *change tokens* from CloudKit, so only changed data is downloaded. 2. When CloudCore is enabled (`CloudCore.enable`) it pulls changed data from CloudKit and subscribes to CloudKit push notifications about new changes. 3. When `CloudCore.pull` is called manually or by push notification, CloudCore pulls and saves changed data to Core Data. -4. When data is written to persistent container (parent context is saved) CloudCore founds locally changed data and pushed to CloudKit. -5. when leveraging NSPersistentHistory, changes are pushed only when online. +4. When data is written to your persistent container (parent context is saved) CloudCore finds locally changed data and pushes to CloudKit. +5. By leveraging NSPersistentHistory, changes can be queued when offline and pushed when online. ## Installation @@ -175,7 +201,6 @@ CloudKit objects can't be mocked up, that's why I create 2 different types of te ## Roadmap -- [ ] Properly push only changed fields from NSPersistenHistory - [ ] Move beta to release status - [ ] Add `CloudCore.disable` method - [ ] Add methods to clear local cache and remote database From b56202549389fd15516ea40956be6115a6c27b3e Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 20 Aug 2019 13:20:46 -0700 Subject: [PATCH 075/203] added additional note regarding iOS 13 native sync --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2b459fca..00b0fa58 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ At WWDC 2019, Apple announced support for NSPersistentCloudKitContainer in iOS 1 * Offline Synchronization is opaque, but doesn't appear to require NSPersistentHistoryTracking * All Core Data names are preceeded with "CD_" in CloudKit * Core Data Relationships are mapped thru CDMR records in CloudKit +* Uses a specific custom zone in the Private Database ###### CloudCore * Support requires specific configuration in the Core Data Model From 2f2f345a8259f43456fe082d92a0de4d65761e37 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 20 Aug 2019 13:32:38 -0700 Subject: [PATCH 076/203] update podspec to use Swift 5.0 --- CloudCore.podspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 6b89e2fe..137b68c7 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,10 +1,10 @@ Pod::Spec.new do |s| s.name = "CloudCore" - s.summary = "Framework that enables synchronization between CloudKit (iCloud) and Core Data." + s.summary = "Framework that enables synchronization between CloudKit and Core Data." s.version = "3.0" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' - s.author = { "Vasily Ulianov" => "vasily@me.com", "deeje" => "deeje@mac.com" } + s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } s.source = { :git => "https://github.com/deeje/CloudCore.git", :tag => s.version.to_s @@ -20,6 +20,6 @@ Pod::Spec.new do |s| s.ios.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.osx.frameworks = 'Foundation', 'CloudKit', 'CoreData' - s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.2' } + s.pod_target_xcconfig = { 'SWIFT_VERSION' => '5.0' } s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' end From 38504bd7ec6520a71806c2360e2eab715f13b1e7 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 20 Aug 2019 14:12:29 -0700 Subject: [PATCH 077/203] update Example project to Swift 5 --- .../CloudCoreExample.xcodeproj/project.pbxproj | 15 +++++++++------ .../xcschemes/CloudCoreExample.xcscheme | 2 +- .../Sources/Class/FRCTableViewDataSource.swift | 8 ++++++-- .../View Controller/DetailViewController.swift | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index e1f17bcc..68404889 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -187,13 +187,13 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1030; ORGANIZATIONNAME = "Vasily Ulianov"; TargetAttributes = { E2C3E34F1E53299800A733BF = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = 26Y8AQV29F; - LastSwiftMigration = 1010; + LastSwiftMigration = 1030; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -214,6 +214,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -265,7 +266,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh", "${BUILT_PRODUCTS_DIR}/CloudCore/CloudCore.framework", "${BUILT_PRODUCTS_DIR}/Fakery/Fakery.framework", "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", @@ -278,7 +279,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -327,6 +328,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -384,6 +386,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -442,7 +445,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -458,7 +461,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme b/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme index fe665656..348ba421 100644 --- a/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme +++ b/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme @@ -1,6 +1,6 @@ : NSObject case .delete: tableView?.deleteSections(sectionIndexSet, with: .automatic) case .update: tableView?.reloadSections(sectionIndexSet, with: .automatic) case .move: break - } + @unknown default: + break + } } func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { @@ -88,7 +90,9 @@ class FRCTableViewDataSource: NSObject case .move: guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return } tableView?.moveRow(at: indexPath, to: newIndexPath) - } + @unknown default: + break + } } func controllerDidChangeContent(_ controller: NSFetchedResultsController) { diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift index 78d6f22a..dc0a3052 100644 --- a/Example/Sources/View Controller/DetailViewController.swift +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -39,7 +39,7 @@ class DetailViewController: UITableViewController { moc.name = CloudCore.config.pushContextName let employee = ModelFactory.insertEmployee(context: moc) - let organization = try? moc.existingObject(with: self.organizationID) as! Organization + let organization = try? moc.existingObject(with: self.organizationID) as? Organization employee.organization = organization try? moc.save() @@ -51,7 +51,7 @@ class DetailViewController: UITableViewController { persistentContainer.performBackgroundTask { (moc) in moc.name = CloudCore.config.pushContextName - let organization = try? moc.existingObject(with: self.organizationID) as! Organization + let organization = try? moc.existingObject(with: self.organizationID) as? Organization organization?.name = newTitle try? moc.save() From 485a79df5b58c04de353cd4106ed0df3fcd4edb1 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 20 Aug 2019 14:12:47 -0700 Subject: [PATCH 078/203] remove .swift-version, use version in podspec --- .swift-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .swift-version diff --git a/.swift-version b/.swift-version deleted file mode 100644 index bf77d549..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.2 From 99bc2046abb74fc6b24a43a4a707b08bdf70c7e7 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 20 Aug 2019 14:13:17 -0700 Subject: [PATCH 079/203] update to Swift 5.0 --- CloudCore.xcodeproj/project.pbxproj | 14 ++++++-------- Source/Classes/CloudCore.swift | 7 +++---- .../PublicDatabaseSubscriptions.swift | 4 ++-- Source/Classes/Push/CoreDataObserver.swift | 3 +++ .../ObjectToRecord/ObjectToRecordConverter.swift | 3 +-- Source/Classes/Push/PushOperationQueue.swift | 4 +++- Source/Model/CloudKitAttribute.swift | 13 ++++++++++--- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 2118f0a4..a13d7bf2 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -623,14 +623,14 @@ }; E29BB2271E436F310020F5B6 = { CreatedOnToolsVersion = 8.2.1; - LastSwiftMigration = 0900; + LastSwiftMigration = 1030; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = D5B2E8991C3A780C00C0327D /* Build configuration list for PBXProject "CloudCore" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -934,7 +934,7 @@ SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.0; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; @@ -958,7 +958,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.0; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; @@ -1078,8 +1078,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1097,8 +1096,7 @@ PRODUCT_BUNDLE_IDENTIFIER = uvasily.CloudCoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index e924b095..a35a6c56 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -134,9 +134,7 @@ open class CloudCore { - completion: `PullResult` enumeration with results of operation */ public static func pull(using userInfo: NotificationUserInfo, to container: NSPersistentContainer, error: ErrorBlock?, completion: @escaping (_ fetchResult: PullResult) -> Void) { - let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) - - guard let cloudDatabase = self.database(for: notification) else { + guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo), let cloudDatabase = self.database(for: notification) else { completion(.noData) return } @@ -176,7 +174,8 @@ open class CloudCore { - Returns: `true` if notification contains CloudCore data */ public static func isCloudCoreNotification(withUserInfo userInfo: NotificationUserInfo) -> Bool { - let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) + guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else { return false } + return (database(for: notification) != nil) } diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index 7e1a730b..a9a0b73f 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -25,7 +25,7 @@ public class PublicDatabaseSubscriptions { // - completion: returns subscriptionID and error upon operation completion static public func subscribe(recordType: String, predicate: NSPredicate, completion: ((_ subscriptionID: String, _ error: Error?) -> Void)?) { let id = prefix + recordType + "-" + predicate.predicateFormat - if self.cachedIDs.index(of: id) != nil { return } + if self.cachedIDs.firstIndex(of: id) != nil { return } let options: CKQuerySubscription.Options = [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion] let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: id, options: options) @@ -55,7 +55,7 @@ public class PublicDatabaseSubscriptions { let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [], subscriptionIDsToDelete: [subscriptionID]) operation.modifySubscriptionsCompletionBlock = { _, _, error in if error == nil { - if let index = self.cachedIDs.index(of: subscriptionID) { + if let index = self.cachedIDs.firstIndex(of: subscriptionID) { self.cachedIDs.remove(at: index) } } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index d5a44116..60a52670 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -200,6 +200,9 @@ class CoreDataObserver { deletedRecordIDs.append(recordIDWithDatabase) } } + + default: + break } } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 37bacb1b..69141eb3 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -107,8 +107,7 @@ class ObjectToRecordConverter { guard let serviceAttributeNames = object.entity.serviceAttributeNames else { continue } for scope in serviceAttributeNames.scopes { - if let triedRestoredRecord = try? object.restoreRecordWithSystemFields(for: scope), - let restoredRecord = triedRestoredRecord { + if let restoredRecord = try? object.restoreRecordWithSystemFields(for: scope) { let targetScope = self.targetScope(for: scope, and: object) let database = self.database(for: targetScope) let recordIDWithDB = RecordIDWithDatabase(restoredRecord.recordID, database) diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index f8825b5c..95f8d572 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -72,7 +72,9 @@ class PushOperationQueue: OperationQueue { private func removeCachedAssets(for record: CKRecord) { for key in record.allKeys() { guard let asset = record.value(forKey: key) as? CKAsset else { continue } - try? FileManager.default.removeItem(at: asset.fileURL) + if let url = asset.fileURL { + try? FileManager.default.removeItem(at: url) + } } } diff --git a/Source/Model/CloudKitAttribute.swift b/Source/Model/CloudKitAttribute.swift index 1f769748..27ba5a37 100644 --- a/Source/Model/CloudKitAttribute.swift +++ b/Source/Model/CloudKitAttribute.swift @@ -31,7 +31,9 @@ class CloudKitAttribute { func makeCoreDataValue() throws -> Any? { switch value { - case let reference as CKRecord.Reference: return try findManagedObject(for: reference.recordID) + case let reference as CKRecord.Reference: + return try findManagedObject(for: reference.recordID) + case let references as [CKRecord.Reference]: let managedObjects = NSMutableSet() for ref in references { @@ -41,8 +43,13 @@ class CloudKitAttribute { if managedObjects.count == 0 { return nil } return managedObjects - case let asset as CKAsset: return try Data(contentsOf: asset.fileURL) - default: return value + + case let asset as CKAsset: + guard let url = asset.fileURL else { return nil } + return try Data(contentsOf: url) + + default: + return value } } From a9b2157aeae45ae94667747b36ab3b0a957bf283 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 20 Aug 2019 14:14:17 -0700 Subject: [PATCH 080/203] update tests to Swift 5.0 --- .../Upload/ObjectToRecord/CoreDataAttributeTests.swift | 4 ++-- .../Upload/ObjectToRecord/CoreDataRelationshipTests.swift | 8 ++++---- Tests/CloudCoreTests/CustomFunctions.swift | 2 +- Tests/CloudCoreTests/Model/CKRecordTests.swift | 5 +++-- Tests/Shared/CorrectObject.swift | 7 ++++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift index 2645553c..758a92cd 100644 --- a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift +++ b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift @@ -63,7 +63,7 @@ class CoreDataAttributeTests: CoreDataTestCase { do { // External binary if let recordExternalValue = try externalAttribute?.makeRecordValue() as? CKAsset { - let recordData = try Data(contentsOf: recordExternalValue.fileURL) + let recordData = try Data(contentsOf: recordExternalValue.fileURL!) XCTAssertEqual(recordData, externalData) } else { XCTFail("External binary isn't stored correctly") @@ -71,7 +71,7 @@ class CoreDataAttributeTests: CoreDataTestCase { // External big binary if let recordExternalValue = try externalBigAttribute?.makeRecordValue() as? CKAsset { - let recordData = try Data(contentsOf: recordExternalValue.fileURL) + let recordData = try Data(contentsOf: recordExternalValue.fileURL!) XCTAssertEqual(recordData, externalBigData) } else { XCTFail("External big binary isn't stored correctly") diff --git a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift index 7f3e155c..fb652045 100644 --- a/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift +++ b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift @@ -25,7 +25,7 @@ class CoreDataRelationshipTests: CoreDataTestCase { let filledObjectRecord = try! object.restoreRecordWithSystemFields(for: .private)! var manyUsers = [UserEntity]() - var manyUsersRecordsIDs = [CKRecordID]() + var manyUsersRecordsIDs = [CKRecord.ID]() for _ in 0...2 { let user = UserEntity(context: context) try! user.setRecordInformation(for: .private) @@ -56,12 +56,12 @@ class CoreDataRelationshipTests: CoreDataTestCase { } // Check single relationship - let singleReference = filledObjectRecord.value(forKey: "singleRelationship") as! CKReference + let singleReference = filledObjectRecord.value(forKey: "singleRelationship") as! CKRecord.Reference XCTAssertEqual(manyUsersRecordsIDs[0], singleReference.recordID) // Check many relationships - let multipleReferences = filledObjectRecord.value(forKey: "manyRelationship") as! [CKReference] - var filledRecordRelationshipIDs = [CKRecordID]() + let multipleReferences = filledObjectRecord.value(forKey: "manyRelationship") as! [CKRecord.Reference] + var filledRecordRelationshipIDs = [CKRecord.ID]() for recordReference in multipleReferences { filledRecordRelationshipIDs.append(recordReference.recordID) diff --git a/Tests/CloudCoreTests/CustomFunctions.swift b/Tests/CloudCoreTests/CustomFunctions.swift index 30937eb4..2f7fc8c6 100644 --- a/Tests/CloudCoreTests/CustomFunctions.swift +++ b/Tests/CloudCoreTests/CustomFunctions.swift @@ -9,7 +9,7 @@ import XCTest func XCTAssertThrowsSpecific(_ expression: @autoclosure () throws -> T, _ error: Error) { - XCTAssertThrowsError(expression) { (throwedError) in + XCTAssertThrowsError(try expression()) { (throwedError) in XCTAssertEqual("\(throwedError)", "\(error)", "XCTAssertThrowsSpecific: errors are not equal") } } diff --git a/Tests/CloudCoreTests/Model/CKRecordTests.swift b/Tests/CloudCoreTests/Model/CKRecordTests.swift index cacaea0d..8aea2089 100644 --- a/Tests/CloudCoreTests/Model/CKRecordTests.swift +++ b/Tests/CloudCoreTests/Model/CKRecordTests.swift @@ -13,8 +13,9 @@ import CloudKit class CKRecordTests: XCTestCase { func testEncodeAndInit() { - let zoneID = CKRecordZoneID(zoneName: "zone", ownerName: CKCurrentUserDefaultName) - let record = CKRecord(recordType: "type", zoneID: zoneID) + let zoneID = CKRecordZone.ID(zoneName: "zone", ownerName: CKCurrentUserDefaultName) + let recordID = CKRecord.ID(recordName: "name", zoneID: zoneID) + let record = CKRecord(recordType: "type", recordID: recordID) record.setValue("testValue", forKey: "testKey") let encodedData = record.encdodedSystemFields diff --git a/Tests/Shared/CorrectObject.swift b/Tests/Shared/CorrectObject.swift index e49f041b..606115ec 100644 --- a/Tests/Shared/CorrectObject.swift +++ b/Tests/Shared/CorrectObject.swift @@ -76,8 +76,9 @@ struct CorrectObject { } func makeRecord() -> CKRecord { - let record = CKRecord(recordType: "TestEntity", zoneID: CloudCore.config.privateZoneID()) - + let recordID = CKRecord.ID(recordName: UUID().uuidString, zoneID: CloudCore.config.privateZoneID()) + let record = CKRecord(recordType: "TestEntity", recordID: recordID) + let asset = try? CoreDataAttribute.createAsset(for: externalBinary) XCTAssertNotNil(asset) record.setValue(asset, forKey: "externalBinary") @@ -143,7 +144,7 @@ func assertEqualPlainTextAttributes(_ managedObject: TestEntity, _ record: CKRec func assertEqualBinaryAttributes(_ managedObject: TestEntity, _ record: CKRecord) { if let recordAsset = record.value(forKey: "externalBinary") as! CKAsset? { - let downloadedData = try! Data(contentsOf: recordAsset.fileURL) + let downloadedData = try! Data(contentsOf: recordAsset.fileURL!) XCTAssertEqual(managedObject.externalBinary, downloadedData) } From bdd1bb148c02e85901186a1747977d6772903e21 Mon Sep 17 00:00:00 2001 From: deeje cooley Date: Tue, 20 Aug 2019 14:31:09 -0700 Subject: [PATCH 081/203] use Xcode 10.3 for Swift 5.0 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5cc99f0e..9dd0d2cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -osx_image: xcode10.1 +osx_image: xcode10.3 language: swift podfile: "Example/Podfile" From 6f0c6c68864dc453df74bc5561534062b2033b9f Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 21 Aug 2019 11:55:24 -0700 Subject: [PATCH 082/203] Release 3.0.1 --- CloudCore.podspec | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 137b68c7..bfd62a5a 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "3.0" + s.version = "3.0.1" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } @@ -20,6 +20,6 @@ Pod::Spec.new do |s| s.ios.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.osx.frameworks = 'Foundation', 'CloudKit', 'CoreData' - s.pod_target_xcconfig = { 'SWIFT_VERSION' => '5.0' } + s.swift_versions = [5.0] s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' end diff --git a/README.md b/README.md index 00b0fa58..b1b95f6e 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Platform](https://img.shields.io/cocoapods/p/CloudCore.svg?style=flat) ![Status](https://img.shields.io/badge/status-beta-orange.svg) -![Swift](https://img.shields.io/badge/swift-4.2-orange.svg) +![Swift](https://img.shields.io/badge/swift-5.0-orange.svg) **CloudCore** is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. From 58f854de519fa1aee0a3c6d726e69ce03ff20f8f Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 21 Aug 2019 12:33:11 -0700 Subject: [PATCH 083/203] update ReadMe CocoaPods instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1b95f6e..1024f360 100755 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ CloudCore is built using a "black box" architecture, so it works invisibly for y it, simply add the following line to your Podfile: ```ruby -pod 'CloudCore', :git => 'https://github.com/deeje/CloudCore.git' +pod 'CloudCore' ``` ## How to help? From 55a3305b0e88af1979dde75e7be8448856398e84 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 15 Sep 2019 12:08:16 -0700 Subject: [PATCH 084/203] update to latest KeyedArchiver APIs --- .../Pull/SubOperations/RecordToCoreDataOperation.swift | 2 +- Source/Classes/Push/CoreDataObserver.swift | 7 ++++--- .../Classes/Push/ObjectToRecord/CoreDataAttribute.swift | 2 +- Source/Model/CKRecord.swift | 8 +++----- Source/Model/Tokens.swift | 9 +++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 10001414..9857b761 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -89,7 +89,7 @@ public class RecordToCoreDataOperation: AsynchronousOperation { if let name = cdAttribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { let value = transformer.transformedValue(coreDataValue) object.setValue(value, forKey: key) - } else if let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: data) { + } else if let unarchivedObject = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSObject.classForKeyedUnarchiver()], from: data) { object.setValue(unarchivedObject, forKey: key) } else { object.setValue(coreDataValue, forKey: key) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 60a52670..217f0d82 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -225,7 +225,7 @@ class CoreDataObserver { let settings = UserDefaults.standard var token: NSPersistentHistoryToken? = nil if let data = settings.object(forKey: key) as? Data { - token = NSKeyedUnarchiver.unarchiveObject(with: data) as? NSPersistentHistoryToken + token = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken } let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) do { @@ -237,8 +237,9 @@ class CoreDataObserver { let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) try moc.execute(deleteRequest) - let data = NSKeyedArchiver.archivedData(withRootObject: transaction.token) - settings.set(data, forKey: key) + if let data = try? NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) { + settings.set(data, forKey: key) + } } else { break } diff --git a/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift b/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift index 0b6167fd..d079521d 100644 --- a/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift +++ b/Source/Classes/Push/ObjectToRecord/CoreDataAttribute.swift @@ -34,7 +34,7 @@ class CoreDataAttribute { let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: transformerName)) { self.value = transformer.reverseTransformedValue(value) } else { - self.value = NSKeyedArchiver.archivedData(withRootObject: value!) + self.value = try? NSKeyedArchiver.archivedData(withRootObject: value!, requiringSecureCoding: false) } } else { self.value = value diff --git a/Source/Model/CKRecord.swift b/Source/Model/CKRecord.swift index e4770137..ec141128 100644 --- a/Source/Model/CKRecord.swift +++ b/Source/Model/CKRecord.swift @@ -10,18 +10,16 @@ import CloudKit public extension CKRecord { convenience init?(archivedData: Data) { - let unarchiver = NSKeyedUnarchiver(forReadingWith: archivedData) + let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: archivedData) unarchiver.requiresSecureCoding = true self.init(coder: unarchiver) } var encdodedSystemFields: Data { - let archivedData = NSMutableData() - let archiver = NSKeyedArchiver(forWritingWith: archivedData) - archiver.requiresSecureCoding = true + let archiver = NSKeyedArchiver(requiringSecureCoding: true) self.encodeSystemFields(with: archiver) archiver.finishEncoding() - return archivedData as Data + return archiver.encodedData } } diff --git a/Source/Model/Tokens.swift b/Source/Model/Tokens.swift index 0bf3556b..539a111c 100644 --- a/Source/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -39,7 +39,7 @@ open class Tokens: NSObject, NSCoding { /// - Returns: previously saved `Token` object, if tokens weren't saved before newly initialized `Tokens` object will be returned public static func loadFromUserDefaults() -> Tokens { guard let tokensData = UserDefaults.standard.data(forKey: CloudCore.config.userDefaultsKeyTokens), - let tokens = NSKeyedUnarchiver.unarchiveObject(with: tokensData) as? Tokens else { + let tokens = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [Tokens.classForKeyedUnarchiver()], from: tokensData) as? Tokens else { return Tokens() } @@ -48,9 +48,10 @@ open class Tokens: NSObject, NSCoding { /// Save tokens to UserDefaults and synchronize. Key is used from `CloudCoreConfig.userDefaultsKeyTokens` open func saveToUserDefaults() { - let tokensData = NSKeyedArchiver.archivedData(withRootObject: self) - UserDefaults.standard.set(tokensData, forKey: CloudCore.config.userDefaultsKeyTokens) - UserDefaults.standard.synchronize() + if let tokensData = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) { + UserDefaults.standard.set(tokensData, forKey: CloudCore.config.userDefaultsKeyTokens) + UserDefaults.standard.synchronize() + } } // MARK: NSCoding From 3d3dd35f9a70d851848ae9a51d4896246912afaa Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 15 Sep 2019 12:08:50 -0700 Subject: [PATCH 085/203] refactor to use ZoneConfiguration, not ZoneOptions --- .../FetchRecordZoneChangesOperation.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index d0923c32..13d1f0ed 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -19,7 +19,7 @@ class FetchRecordZoneChangesOperation: Operation { var recordChangedBlock: ((CKRecord) -> Void)? var recordWithIDWasDeletedBlock: ((CKRecord.ID) -> Void)? - private let optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions] + private let optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneConfiguration] private let fetchQueue = OperationQueue() init(from database: CKDatabase, recordZoneIDs: [CKRecordZone.ID], tokens: Tokens) { @@ -27,9 +27,9 @@ class FetchRecordZoneChangesOperation: Operation { self.database = database self.recordZoneIDs = recordZoneIDs - var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]() + var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneConfiguration]() for zoneID in recordZoneIDs { - let options = CKFetchRecordZoneChangesOperation.ZoneOptions() + let options = CKFetchRecordZoneChangesOperation.ZoneConfiguration() options.previousServerChangeToken = self.tokens.tokensByRecordZoneID[zoneID] optionsByRecordZoneID[zoneID] = options } @@ -49,9 +49,9 @@ class FetchRecordZoneChangesOperation: Operation { fetchQueue.waitUntilAllOperationsAreFinished() } - private func makeFetchOperation(optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]) -> CKFetchRecordZoneChangesOperation { + private func makeFetchOperation(optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneConfiguration]) -> CKFetchRecordZoneChangesOperation { // Init Fetch Operation - let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, optionsByRecordZoneID: optionsByRecordZoneID) + let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, configurationsByRecordZoneID: optionsByRecordZoneID) fetchOperation.recordChangedBlock = { self.recordChangedBlock?($0) From 54e7dfe8d4c4e8b6ce22e6bfb4cff3bc88051142 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 15 Sep 2019 12:09:30 -0700 Subject: [PATCH 086/203] refactor to use CKOperation.Configuration --- .../PublicDatabaseSubscriptions.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index a9a0b73f..c3424af8 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -43,7 +43,10 @@ public class PublicDatabaseSubscriptions { completion?(subscription.subscriptionID, error) } - operation.timeoutIntervalForResource = 20 + let config = CKOperation.Configuration() + config.timeoutIntervalForResource = 20 + operation.configuration = config + CKContainer.default().publicCloudDatabase.add(operation) } @@ -63,7 +66,10 @@ public class PublicDatabaseSubscriptions { completion?(error) } - operation.timeoutIntervalForResource = 20 + let config = CKOperation.Configuration() + config.timeoutIntervalForResource = 20 + operation.configuration = config + CKContainer.default().publicCloudDatabase.add(operation) } From 99df221a6f26928ac8eac920029f4e59a1965396 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 17 Feb 2020 13:28:44 -0800 Subject: [PATCH 087/203] update the license --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 2eee9f31..3c778128 100755 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Vasily Ulianov +Copyright (c) 2020 deeje cooley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From cf1e917f5944f0e415872c21f7f5a66a9ecfa44c Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 17 Feb 2020 13:35:23 -0800 Subject: [PATCH 088/203] require iOS 13, macOS 10.15, watchOS 6, Swift 5.1 --- CloudCore.podspec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index bfd62a5a..a4c4113d 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "3.0.1" + s.version = "3.1" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } @@ -10,16 +10,16 @@ Pod::Spec.new do |s| :tag => s.version.to_s } - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' + s.ios.deployment_target = '13.0' + s.osx.deployment_target = '10.15' s.tvos.deployment_target = '10.0' - s.watchos.deployment_target = '3.0' + s.watchos.deployment_target = '6.0' s.source_files = 'Source/**/*.swift' s.ios.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.osx.frameworks = 'Foundation', 'CloudKit', 'CoreData' - s.swift_versions = [5.0] + s.swift_versions = [5.1] s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' end From 01ce56c5da853fbe61e5174d025a5d5e93b39e84 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 17 Feb 2020 14:03:18 -0800 Subject: [PATCH 089/203] enable watchOS 6 support --- Source/Classes/CloudCore.swift | 4 ---- .../FetchPublicSubscriptionsOperation.swift | 2 -- .../PublicDatabaseSubscriptions.swift | 2 -- Source/Classes/Push/CoreDataObserver.swift | 10 ++++------ Source/Classes/Setup/SetupOperation.swift | 4 ---- Source/Classes/Setup/SubscribeOperation.swift | 3 --- 6 files changed, 4 insertions(+), 21 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index a35a6c56..d619ebd6 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -92,11 +92,9 @@ open class CloudCore { self.coreDataObserver = observer // Subscribe (subscription may be outdated/removed) - #if !os(watchOS) let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = { handle(subscriptionError: $0, container: container) } queue.addOperation(subscribeOperation) - #endif // Fetch updated data (e.g. push notifications weren't received) let updateFromCloudOperation = PullOperation(persistentContainer: container) @@ -104,9 +102,7 @@ open class CloudCore { self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } - #if !os(watchOS) updateFromCloudOperation.addDependency(subscribeOperation) - #endif queue.addOperation(updateFromCloudOperation) } diff --git a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift index 7c89a57b..58d782fa 100644 --- a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift +++ b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift @@ -10,7 +10,6 @@ import CloudKit /// Fetch CloudCore's subscriptions from Public CKDatabase -#if !os(watchOS) class FetchPublicSubscriptionsOperation: AsynchronousOperation { var errorBlock: ErrorBlock? var fetchCompletionBlock: (([CKSubscription]) -> Void)? @@ -45,4 +44,3 @@ class FetchPublicSubscriptionsOperation: AsynchronousOperation { } } } -#endif diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index c3424af8..a698fa6f 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -10,7 +10,6 @@ import CloudKit // Use that class to manage subscriptions to public CloudKit database. // If you want to sync some records with public database you need to subsrcibe for notifications on that changes to enable iCloud -> Local database syncing. -#if !os(watchOS) public class PublicDatabaseSubscriptions { private static var prefix: String { return CloudCore.config.publicSubscriptionIDPrefix } @@ -100,4 +99,3 @@ public class PublicDatabaseSubscriptions { } } -#endif diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 217f0d82..c592ee32 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -277,12 +277,10 @@ class CoreDataObserver { } // Subscribe operation - #if !os(watchOS) - let subscribeOperation = SubscribeOperation() - subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } - subscribeOperation.addDependency(createZoneOperation) - pushOperationQueue.addOperation(subscribeOperation) - #endif + let subscribeOperation = SubscribeOperation() + subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } + subscribeOperation.addDependency(createZoneOperation) + pushOperationQueue.addOperation(subscribeOperation) // Upload all local data let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) diff --git a/Source/Classes/Setup/SetupOperation.swift b/Source/Classes/Setup/SetupOperation.swift index 35fd4003..da117a80 100644 --- a/Source/Classes/Setup/SetupOperation.swift +++ b/Source/Classes/Setup/SetupOperation.swift @@ -47,21 +47,17 @@ class SetupOperation: Operation { operations.append(createZoneOperation) // Subscribe operation - #if !os(watchOS) let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = errorBlock subscribeOperation.addDependency(createZoneOperation) operations.append(subscribeOperation) - #endif if uploadAllData { // Upload all local data let uploadOperation = PushAllLocalDataOperation(parentContext: childContext, managedObjectModel: container.managedObjectModel) uploadOperation.errorBlock = errorBlock - #if !os(watchOS) uploadOperation.addDependency(subscribeOperation) - #endif operations.append(uploadOperation) } diff --git a/Source/Classes/Setup/SubscribeOperation.swift b/Source/Classes/Setup/SubscribeOperation.swift index 64f6eae7..5a094586 100644 --- a/Source/Classes/Setup/SubscribeOperation.swift +++ b/Source/Classes/Setup/SubscribeOperation.swift @@ -9,8 +9,6 @@ import Foundation import CloudKit -#if !os(watchOS) -@available(watchOS, unavailable) class SubscribeOperation: AsynchronousOperation { var errorBlock: ErrorBlock? @@ -87,4 +85,3 @@ class SubscribeOperation: AsynchronousOperation { } } -#endif From c7e9a056cb79b818358d6262daac756f787d9506 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 24 Jun 2020 21:40:06 -0700 Subject: [PATCH 090/203] gracefully handle unknown references --- .../RecordToCoreDataOperation.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 9857b761..4691b6be 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -82,23 +82,23 @@ public class RecordToCoreDataOperation: AsynchronousOperation { let recordValue = record.value(forKey: key) let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) - let coreDataValue = try ckAttribute.makeCoreDataValue() - - if let cdAttribute = object.entity.attributesByName[key], cdAttribute.attributeType == .transformableAttributeType, - let data = coreDataValue as? Data { - if let name = cdAttribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { - let value = transformer.transformedValue(coreDataValue) - object.setValue(value, forKey: key) - } else if let unarchivedObject = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSObject.classForKeyedUnarchiver()], from: data) { - object.setValue(unarchivedObject, forKey: key) + if let coreDataValue = try? ckAttribute.makeCoreDataValue() { + if let cdAttribute = object.entity.attributesByName[key], cdAttribute.attributeType == .transformableAttributeType, + let data = coreDataValue as? Data { + if let name = cdAttribute.valueTransformerName, let transformer = ValueTransformer(forName: NSValueTransformerName(rawValue: name)) { + let value = transformer.transformedValue(coreDataValue) + object.setValue(value, forKey: key) + } else if let unarchivedObject = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSObject.classForKeyedUnarchiver()], from: data) { + object.setValue(unarchivedObject, forKey: key) + } else { + object.setValue(coreDataValue, forKey: key) + } } else { - object.setValue(coreDataValue, forKey: key) - } - } else { - if object.entity.attributesByName[key] != nil || object.entity.relationshipsByName[key] != nil { - object.setValue(coreDataValue, forKey: key) + if object.entity.attributesByName[key] != nil || object.entity.relationshipsByName[key] != nil { + object.setValue(coreDataValue, forKey: key) + } + missingObjectsPerEntities[object] = ckAttribute.notFoundRecordNamesForAttribute } - missingObjectsPerEntities[object] = ckAttribute.notFoundRecordNamesForAttribute } } From 0392745a5c28b42183dbd37b68bc0dccff2c10de Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 15 Oct 2020 17:32:44 -0700 Subject: [PATCH 091/203] fix broken tokens --- Source/Model/Tokens.swift | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Source/Model/Tokens.swift b/Source/Model/Tokens.swift index 539a111c..e47f53b1 100644 --- a/Source/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -17,7 +17,7 @@ import CloudKit * tokens per record inside *Record Data* attribute, it is managed automatically you don't need to take any actions about that token */ -open class Tokens: NSObject, NSCoding { +open class Tokens: NSObject, NSSecureCoding { var tokensByDatabaseScope = [Int: CKServerChangeToken]() var tokensByRecordZoneID = [CKRecordZone.ID: CKServerChangeToken]() @@ -27,6 +27,10 @@ open class Tokens: NSObject, NSCoding { static let tokensByRecordZoneID = "tokensByRecordZoneID" } + public static var supportsSecureCoding: Bool { + return true + } + /// Create fresh object without any Tokens inside. Can be used to fetch full data. public override init() { super.init() @@ -38,17 +42,26 @@ open class Tokens: NSObject, NSCoding { /// /// - Returns: previously saved `Token` object, if tokens weren't saved before newly initialized `Tokens` object will be returned public static func loadFromUserDefaults() -> Tokens { - guard let tokensData = UserDefaults.standard.data(forKey: CloudCore.config.userDefaultsKeyTokens), - let tokens = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [Tokens.classForKeyedUnarchiver()], from: tokensData) as? Tokens else { - return Tokens() + if let tokensData = UserDefaults.standard.data(forKey: CloudCore.config.userDefaultsKeyTokens) { + do { + let allowableClasses = [Tokens.classForKeyedUnarchiver(), + NSDictionary.classForKeyedUnarchiver(), + CKRecordZone.ID.classForKeyedUnarchiver(), + CKServerChangeToken.classForKeyedUnarchiver()] + let tokens = try NSKeyedUnarchiver.unarchivedObject(ofClasses: allowableClasses, from: tokensData) as! Tokens + + return tokens + } catch { +// print("\(error)") + } } - return tokens + return Tokens() } /// Save tokens to UserDefaults and synchronize. Key is used from `CloudCoreConfig.userDefaultsKeyTokens` open func saveToUserDefaults() { - if let tokensData = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) { + if let tokensData = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: true) { UserDefaults.standard.set(tokensData, forKey: CloudCore.config.userDefaultsKeyTokens) UserDefaults.standard.synchronize() } From 5f5676be6ab40a7fae97c2769db12db15441a660 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 17 Nov 2020 15:56:04 -0800 Subject: [PATCH 092/203] fix bug in CoreData where _defaultOwner != __defaultOwner__ --- .../Classes/Pull/SubOperations/RecordToCoreDataOperation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 4691b6be..db7047ce 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -105,7 +105,7 @@ public class RecordToCoreDataOperation: AsynchronousOperation { // Set system headers object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName) object.setValue(record.recordID.zoneID.ownerName, forKey: serviceAttributeNames.ownerName) - if record.recordID.zoneID == CKRecordZone.default().zoneID { + if record.recordID.zoneID.zoneName == CKRecordZone.default().zoneID.zoneName { object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.publicRecordData) } else { object.setValue(record.encdodedSystemFields, forKey: serviceAttributeNames.privateRecordData) From 32cdd72ae86c0463a511c7ea498776049bb3bc71 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 6 Feb 2021 17:01:43 -0800 Subject: [PATCH 093/203] fix a race condition in processChanges ? --- Source/Classes/Push/CoreDataObserver.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index c592ee32..7ca433d9 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -19,6 +19,8 @@ class CoreDataObserver { let cloudContextName = "CloudCoreSync" + let processSemaphore = DispatchSemaphore(value: 1) + // Used for errors delegation weak var delegate: CloudCoreDelegate? @@ -82,6 +84,11 @@ class CoreDataObserver { } func processChanges() -> Bool { + processSemaphore.wait() + defer { + processSemaphore.signal() + } + var success = true CloudCore.delegate?.willSyncToCloud() From 463556de44bcb106b8494977aa13833536b33167 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 15 Mar 2021 19:09:21 -0700 Subject: [PATCH 094/203] make CloudCoreConfig fully public --- Source/Model/CloudCoreConfig.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 3e96b043..d0832b56 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -41,15 +41,15 @@ public struct CloudCoreConfig { public func privateZoneID() -> CKRecordZone.ID { return CKRecordZone.ID(zoneName: zoneName, ownerName: CKCurrentUserDefaultName) } - let subscriptionIDForPrivateDB = "CloudCorePrivate" - let subscriptionIDForSharedDB = "CloudCoreShared" + public let subscriptionIDForPrivateDB = "CloudCorePrivate" + public let subscriptionIDForSharedDB = "CloudCoreShared" /// subscriptionID's prefix for custom CKSubscription in public databases - var publicSubscriptionIDPrefix = "CloudCore-" + public var publicSubscriptionIDPrefix = "CloudCore-" // MARK: Core Data public let pushContextName = "CloudCorePushContext" - let pullContextName = "CloudCorePullContext" + public let pullContextName = "CloudCorePullContext" /// Default entity's attribute name for *Record Name* if User Info is not specified /// @@ -78,4 +78,8 @@ public struct CloudCoreConfig { /// Default value is `CloudCoreTokens` public var userDefaultsKeyTokens = "CloudCoreTokens" + public init() { + + } + } From b3d136f676185b83ca60b673551e0a5832c36f11 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 15 Mar 2021 19:09:48 -0700 Subject: [PATCH 095/203] implement PullResult.wkBackgroundFetchResult --- Source/Enum/PullResult.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Source/Enum/PullResult.swift b/Source/Enum/PullResult.swift index 40a6514f..49715319 100644 --- a/Source/Enum/PullResult.swift +++ b/Source/Enum/PullResult.swift @@ -35,3 +35,15 @@ public enum PullResult: UInt { } #endif + +#if os(watchOS) + import WatchKit + + public extension PullResult { + + var wkBackgroundFetchResult: WKBackgroundFetchResult { + return WKBackgroundFetchResult(rawValue: self.rawValue)! + } + + } +#endif From 5a237be03f64e1844ee177a67940a36e20c0ac19 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 15 Mar 2021 19:11:31 -0700 Subject: [PATCH 096/203] =?UTF-8?q?don=E2=80=99t=20assume=20CKContainer.de?= =?UTF-8?q?fault(),=20use=20Config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FetchPublicSubscriptionsOperation.swift | 2 +- .../PublicSubscriptions/PublicDatabaseSubscriptions.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift index 58d782fa..3bf61905 100644 --- a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift +++ b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift @@ -19,7 +19,7 @@ class FetchPublicSubscriptionsOperation: AsynchronousOperation { override func main() { super.main() - CKContainer.default().publicCloudDatabase.fetchAllSubscriptions { (subscriptions, error) in + CloudCore.config.container.publicCloudDatabase.fetchAllSubscriptions { (subscriptions, error) in defer { self.state = .finished } diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index a698fa6f..dbb6a7c4 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -46,7 +46,7 @@ public class PublicDatabaseSubscriptions { config.timeoutIntervalForResource = 20 operation.configuration = config - CKContainer.default().publicCloudDatabase.add(operation) + CloudCore.config.container.publicCloudDatabase.add(operation) } // Unsubscribe from public database @@ -69,7 +69,7 @@ public class PublicDatabaseSubscriptions { config.timeoutIntervalForResource = 20 operation.configuration = config - CKContainer.default().publicCloudDatabase.add(operation) + CloudCore.config.container.publicCloudDatabase.add(operation) } From 1e3be9a30b1274341052b18600e07e7186815f66 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 17 Mar 2021 17:31:11 -0700 Subject: [PATCH 097/203] container and db op queues, QoS = .userInteractive this commit improves overall performance of sync in CloudCore, and makes sync of a large iCloud account (like mine) work on watchOS. --- Source/Classes/CloudCore.swift | 16 +++-- .../FetchPublicSubscriptionsOperation.swift | 7 ++ .../PublicDatabaseSubscriptions.swift | 24 +++---- Source/Classes/Pull/PullOperation.swift | 67 +++++++++++++------ .../DeleteFromCoreDataOperation.swift | 3 +- .../FetchRecordZoneChangesOperation.swift | 31 +++++---- .../PurgeLocalDatabaseOperation.swift | 3 + .../RecordToCoreDataOperation.swift | 3 +- Source/Classes/Push/CoreDataObserver.swift | 1 + .../ObjectToRecordOperation.swift | 4 +- Source/Classes/Push/PushOperationQueue.swift | 23 ++++--- .../Setup/CreateCloudCoreZoneOperation.swift | 7 ++ .../Setup/PushAllLocalDataOperation.swift | 5 ++ Source/Classes/Setup/SetupOperation.swift | 5 ++ Source/Classes/Setup/SubscribeOperation.swift | 34 ++++++---- 15 files changed, 157 insertions(+), 76 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index d619ebd6..180ed71f 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -93,18 +93,20 @@ open class CloudCore { // Subscribe (subscription may be outdated/removed) let subscribeOperation = SubscribeOperation() - subscribeOperation.errorBlock = { handle(subscriptionError: $0, container: container) } - queue.addOperation(subscribeOperation) + subscribeOperation.errorBlock = { + handle(subscriptionError: $0, container: container) + } // Fetch updated data (e.g. push notifications weren't received) - let updateFromCloudOperation = PullOperation(persistentContainer: container) - updateFromCloudOperation.errorBlock = { + let pullOperation = PullOperation(persistentContainer: container) + pullOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } - updateFromCloudOperation.addDependency(subscribeOperation) - - queue.addOperation(updateFromCloudOperation) + pullOperation.addDependency(subscribeOperation) + + queue.addOperation(subscribeOperation) + queue.addOperation(pullOperation) } /// Disables synchronization (push notifications won't be sent also) diff --git a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift index 3bf61905..ebe055d1 100644 --- a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift +++ b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift @@ -16,6 +16,13 @@ class FetchPublicSubscriptionsOperation: AsynchronousOperation { private let prefix = CloudCore.config.publicSubscriptionIDPrefix + public override init() { + super.init() + + name = "FetchPublicSubscriptionsOperation" + qualityOfService = .userInteractive + } + override func main() { super.main() diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index dbb6a7c4..6e399c61 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -27,26 +27,26 @@ public class PublicDatabaseSubscriptions { if self.cachedIDs.firstIndex(of: id) != nil { return } let options: CKQuerySubscription.Options = [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion] - let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: id, options: options) + let querySubscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: id, options: options) let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.shouldSendContentAvailable = true - subscription.notificationInfo = notificationInfo + querySubscription.notificationInfo = notificationInfo - let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) - operation.modifySubscriptionsCompletionBlock = { _, _, error in + let modifySubscriptions = CKModifySubscriptionsOperation(subscriptionsToSave: [querySubscription], subscriptionIDsToDelete: []) + modifySubscriptions.modifySubscriptionsCompletionBlock = { _, _, error in if error == nil { - self.cachedIDs.append(subscription.subscriptionID) + self.cachedIDs.append(querySubscription.subscriptionID) } - completion?(subscription.subscriptionID, error) + completion?(querySubscription.subscriptionID, error) } let config = CKOperation.Configuration() config.timeoutIntervalForResource = 20 - operation.configuration = config + modifySubscriptions.configuration = config - CloudCore.config.container.publicCloudDatabase.add(operation) + CloudCore.config.container.publicCloudDatabase.add(modifySubscriptions) } // Unsubscribe from public database @@ -54,8 +54,8 @@ public class PublicDatabaseSubscriptions { // - Parameters: // - subscriptionID: id of subscription to remove static public func unsubscribe(subscriptionID: String, completion: ((Error?) -> Void)?) { - let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [], subscriptionIDsToDelete: [subscriptionID]) - operation.modifySubscriptionsCompletionBlock = { _, _, error in + let modifySubscription = CKModifySubscriptionsOperation(subscriptionsToSave: [], subscriptionIDsToDelete: [subscriptionID]) + modifySubscription.modifySubscriptionsCompletionBlock = { _, _, error in if error == nil { if let index = self.cachedIDs.firstIndex(of: subscriptionID) { self.cachedIDs.remove(at: index) @@ -67,9 +67,9 @@ public class PublicDatabaseSubscriptions { let config = CKOperation.Configuration() config.timeoutIntervalForResource = 20 - operation.configuration = config + modifySubscription.configuration = config - CloudCore.config.container.publicCloudDatabase.add(operation) + CloudCore.config.container.publicCloudDatabase.add(modifySubscription) } diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 00dc169b..6e904a3d 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -42,15 +42,20 @@ public class PullOperation: Operation { self.databases = databases self.persistentContainer = persistentContainer self.tokens = tokens - - queue.name = "PullQueue" + + super.init() + + name = "PullOperation" + qualityOfService = .userInteractive + + queue.name = "PullQueue" queue.maxConcurrentOperationCount = 1 } /// Performs the receiver’s non-concurrent task. override public func main() { if self.isCancelled { return } - + CloudCore.delegate?.willSyncFromCloud() let backgroundContext = persistentContainer.newBackgroundContext() @@ -61,8 +66,9 @@ public class PullOperation: Operation { let changedRecordIDs: NSMutableSet = [] let deletedRecordIDs: NSMutableSet = [] let previousToken = self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] - let notesOp = CKFetchNotificationChangesOperation(previousServerChangeToken: previousToken) - notesOp.notificationChangedBlock = { (innerNotification) in + let fetchNotificationChanges = CKFetchNotificationChangesOperation(previousServerChangeToken: previousToken) + fetchNotificationChanges.qualityOfService = .userInteractive + fetchNotificationChanges.notificationChangedBlock = { (innerNotification) in if let innerQueryNotification = innerNotification as? CKQueryNotification { if innerQueryNotification.queryNotificationReason == .recordDeleted { deletedRecordIDs.add(innerQueryNotification.recordID!) @@ -72,19 +78,23 @@ public class PullOperation: Operation { } } } - notesOp.fetchNotificationChangesCompletionBlock = { (changeToken, error) in + fetchNotificationChanges.fetchNotificationChangesCompletionBlock = { (changeToken, error) in let allChangedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] - let fetch = CKFetchRecordsOperation(recordIDs: allChangedRecordIDs) - fetch.database = CloudCore.config.container.publicCloudDatabase - fetch.perRecordCompletionBlock = { (record, recordID, error) in + let fetchRecords = CKFetchRecordsOperation(recordIDs: allChangedRecordIDs) + fetchRecords.database = CloudCore.config.container.publicCloudDatabase + fetchRecords.qualityOfService = .userInteractive + fetchRecords.perRecordCompletionBlock = { (record, recordID, error) in if error == nil { self.addConvertRecordOperation(record: record!, context: backgroundContext) } } - fetch.fetchRecordsCompletionBlock = { (_, error) in + fetchRecords.fetchRecordsCompletionBlock = { (_, error) in self.processMissingReferences(context: backgroundContext) } - self.queue.addOperation(fetch) + let finished = BlockOperation { } + finished.addDependency(fetchRecords) + database.add(fetchRecords) + self.queue.addOperation(finished) let allDeletedRecordIDs = deletedRecordIDs.allObjects as! [CKRecord.ID] for recordID in allDeletedRecordIDs { @@ -93,21 +103,25 @@ public class PullOperation: Operation { self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken } - self.queue.addOperation(notesOp) + let finished = BlockOperation { } + finished.addDependency(fetchNotificationChanges) + CloudCore.config.container.add(fetchNotificationChanges) + self.queue.addOperation(finished) } else { var changedZoneIDs = [CKRecordZone.ID]() var deletedZoneIDs = [CKRecordZone.ID]() let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope.rawValue] - let databaseChangeOp = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) - databaseChangeOp.database = database - databaseChangeOp.recordZoneWithIDChangedBlock = { (recordZoneID) in + let fetchDatabaseChanges = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) + fetchDatabaseChanges.database = database + fetchDatabaseChanges.qualityOfService = .userInteractive + fetchDatabaseChanges.recordZoneWithIDChangedBlock = { (recordZoneID) in changedZoneIDs.append(recordZoneID) } - databaseChangeOp.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in + fetchDatabaseChanges.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in deletedZoneIDs.append(recordZoneID) } - databaseChangeOp.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in + fetchDatabaseChanges.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in // TODO: error handling? if changedZoneIDs.count > 0 { @@ -119,12 +133,25 @@ public class PullOperation: Operation { self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken } - self.queue.addOperation(databaseChangeOp) + /* + To improve performance overall, and on watchOS in particular + make sure to queue up CK operations on the proper queue + whether its for the container in general or a specific database. + + To maintain the overall logic of CloudCore, we shadow these ops + in our own queues, using no-op block ops with dependencies. + + You will see this pattern elsewhere in CloudCore when appropriate. + */ + let finished = BlockOperation { } + finished.addDependency(fetchDatabaseChanges) + database.add(fetchDatabaseChanges) + self.queue.addOperation(finished) } } self.queue.waitUntilAllOperationsAreFinished() - + do { try backgroundContext.save() } catch { @@ -157,7 +184,7 @@ public class PullOperation: Operation { if recordZoneIDs.isEmpty { return } let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) - + recordZoneChangesOperation.qualityOfService = .userInteractive recordZoneChangesOperation.recordChangedBlock = { self.addConvertRecordOperation(record: $0, context: context) } diff --git a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift index bc84b338..af50ff76 100644 --- a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift @@ -20,7 +20,8 @@ class DeleteFromCoreDataOperation: Operation { super.init() - self.name = "DeleteFromCoreDataOperation" + name = "DeleteFromCoreDataOperation" + qualityOfService = .userInteractive } override func main() { diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index 13d1f0ed..63238666 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -37,29 +37,33 @@ class FetchRecordZoneChangesOperation: Operation { super.init() - self.name = "FetchRecordZoneChangesOperation" + name = "FetchRecordZoneChangesOperation" + qualityOfService = .userInteractive } override func main() { super.main() let fetchOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) - fetchQueue.addOperation(fetchOperation) + let finish = BlockOperation { } + finish.addDependency(fetchOperation) + database.add(fetchOperation) + fetchQueue.addOperation(finish) fetchQueue.waitUntilAllOperationsAreFinished() } private func makeFetchOperation(optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneConfiguration]) -> CKFetchRecordZoneChangesOperation { // Init Fetch Operation - let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, configurationsByRecordZoneID: optionsByRecordZoneID) - - fetchOperation.recordChangedBlock = { + let fetchRecordZoneChanges = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, configurationsByRecordZoneID: optionsByRecordZoneID) + + fetchRecordZoneChanges.recordChangedBlock = { self.recordChangedBlock?($0) } - fetchOperation.recordWithIDWasDeletedBlock = { recordID, _ in + fetchRecordZoneChanges.recordWithIDWasDeletedBlock = { recordID, _ in self.recordWithIDWasDeletedBlock?(recordID) } - fetchOperation.recordZoneFetchCompletionBlock = { zoneId, serverChangeToken, clientChangeTokenData, isMore, error in + fetchRecordZoneChanges.recordZoneFetchCompletionBlock = { zoneId, serverChangeToken, clientChangeTokenData, isMore, error in self.tokens.tokensByRecordZoneID[zoneId] = serverChangeToken if let error = error { @@ -68,13 +72,16 @@ class FetchRecordZoneChangesOperation: Operation { if isMore { let moreOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) - self.fetchQueue.addOperation(moreOperation) + let finish = BlockOperation { } + finish.addDependency(moreOperation) + self.database.add(moreOperation) + self.fetchQueue.addOperation(finish) } } - fetchOperation.qualityOfService = self.qualityOfService - fetchOperation.database = self.database - - return fetchOperation + fetchRecordZoneChanges.database = self.database + fetchRecordZoneChanges.qualityOfService = .userInteractive + + return fetchRecordZoneChanges } } diff --git a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift index 219ab273..a1ecb375 100644 --- a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift +++ b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift @@ -19,6 +19,9 @@ class PurgeLocalDatabaseOperation: Operation { self.managedObjectModel = managedObjectModel super.init() + + name = "PurgeLocalDatabaseOperation" + qualityOfService = .userInteractive } override func main() { diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index db7047ce..0a5549f5 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -29,7 +29,8 @@ public class RecordToCoreDataOperation: AsynchronousOperation { super.init() - self.name = "RecordToCoreDataOperation" + name = "RecordToCoreDataOperation" + qualityOfService = .userInteractive } override public func main() { diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 7ca433d9..5d2d52ce 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -287,6 +287,7 @@ class CoreDataObserver { let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } subscribeOperation.addDependency(createZoneOperation) + pushOperationQueue.addOperation(subscribeOperation) // Upload all local data diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index aca99056..513dc959 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -30,7 +30,9 @@ class ObjectToRecordOperation: Operation { self.serviceAttributeNames = serviceAttributeNames super.init() - self.name = "ObjectToRecordOperation" + + name = "ObjectToRecordOperation" + qualityOfService = .userInteractive } override func main() { diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index 95f8d572..14594a0c 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -46,10 +46,12 @@ class PushOperationQueue: OperationQueue { private func addOperation(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], database: CKDatabase) { // Modify CKRecord Operation - let modifyOperation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) - modifyOperation.savePolicy = .changedKeys - - modifyOperation.perRecordCompletionBlock = { record, error in + let modifyRecords = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) + modifyRecords.database = database + modifyRecords.savePolicy = .changedKeys + modifyRecords.qualityOfService = .userInteractive + + modifyRecords.perRecordCompletionBlock = { record, error in if let error = error { self.errorBlock?(error) } else { @@ -57,15 +59,16 @@ class PushOperationQueue: OperationQueue { } } - modifyOperation.modifyRecordsCompletionBlock = { _, _, error in + modifyRecords.modifyRecordsCompletionBlock = { _, _, error in if let error = error { self.errorBlock?(error) } } - - modifyOperation.database = database - - self.addOperation(modifyOperation) + + let finish = BlockOperation { } + finish.addDependency(modifyRecords) + database.add(modifyRecords) + self.addOperation(finish) } /// Remove locally cached assets prepared for uploading at CloudKit @@ -77,7 +80,7 @@ class PushOperationQueue: OperationQueue { } } } - + } fileprivate class DatabaseModifyDataSource { diff --git a/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift b/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift index e138bd61..374caf70 100644 --- a/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift +++ b/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift @@ -14,6 +14,13 @@ class CreateCloudCoreZoneOperation: AsynchronousOperation { var errorBlock: ErrorBlock? private var createZoneOperation: CKModifyRecordZonesOperation? + public override init() { + super.init() + + name = "CreateCloudCoreZoneOperation" + qualityOfService = .userInteractive + } + override func main() { super.main() diff --git a/Source/Classes/Setup/PushAllLocalDataOperation.swift b/Source/Classes/Setup/PushAllLocalDataOperation.swift index b7f4f42f..1a9d0ba4 100644 --- a/Source/Classes/Setup/PushAllLocalDataOperation.swift +++ b/Source/Classes/Setup/PushAllLocalDataOperation.swift @@ -27,6 +27,11 @@ class PushAllLocalDataOperation: Operation { init(parentContext: NSManagedObjectContext, managedObjectModel: NSManagedObjectModel) { self.parentContext = parentContext self.managedObjectModel = managedObjectModel + + super.init() + + name = "PushAllLocalDataOperation" + qualityOfService = .userInteractive } override func main() { diff --git a/Source/Classes/Setup/SetupOperation.swift b/Source/Classes/Setup/SetupOperation.swift index da117a80..ed81aff5 100644 --- a/Source/Classes/Setup/SetupOperation.swift +++ b/Source/Classes/Setup/SetupOperation.swift @@ -28,6 +28,11 @@ class SetupOperation: Operation { init(container: NSPersistentContainer, uploadAllData: Bool) { self.container = container self.uploadAllData = uploadAllData + + super.init() + + name = "SetupOperation" + qualityOfService = .userInteractive } private let queue = OperationQueue() diff --git a/Source/Classes/Setup/SubscribeOperation.swift b/Source/Classes/Setup/SubscribeOperation.swift index 5a094586..1b2ddedd 100644 --- a/Source/Classes/Setup/SubscribeOperation.swift +++ b/Source/Classes/Setup/SubscribeOperation.swift @@ -14,7 +14,14 @@ class SubscribeOperation: AsynchronousOperation { var errorBlock: ErrorBlock? private let queue = OperationQueue() - + + public override init() { + super.init() + + name = "SubscribeOperation" + qualityOfService = .userInteractive + } + override func main() { super.main() @@ -41,23 +48,25 @@ class SubscribeOperation: AsynchronousOperation { finishOperation.addDependency(subscribeToShared) finishOperation.addDependency(fetchSharedSubscription) - queue.addOperations([subcribeToPrivate, - fetchPrivateSubscription, - subscribeToShared, - fetchSharedSubscription, - finishOperation], waitUntilFinished: false) + subcribeToPrivate.database?.add(subcribeToPrivate) + fetchPrivateSubscription.database?.add(fetchPrivateSubscription) + subscribeToShared.database?.add(subscribeToShared) + fetchSharedSubscription.database?.add(fetchSharedSubscription) + + queue.addOperation(finishOperation) } private func makeRecordZoneSubscriptionOperation(for database: CKDatabase, id: String) -> CKModifySubscriptionsOperation { let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.shouldSendContentAvailable = true - let subscription = (database == CloudCore.config.container.sharedCloudDatabase) ?CKDatabaseSubscription(subscriptionID: id) : + let subscription = (database == CloudCore.config.container.sharedCloudDatabase) ? CKDatabaseSubscription(subscriptionID: id) : CKRecordZoneSubscription(zoneID: CloudCore.config.privateZoneID(), subscriptionID: id) subscription.notificationInfo = notificationInfo - let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) - operation.modifySubscriptionsCompletionBlock = { + let modifySubscriptions = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) + modifySubscriptions.database = database + modifySubscriptions.modifySubscriptionsCompletionBlock = { if let error = $2 { // Cancellation is not an error if case CKError.operationCancelled = error { return } @@ -66,9 +75,9 @@ class SubscribeOperation: AsynchronousOperation { } } - operation.database = database + modifySubscriptions.qualityOfService = .userInteractive - return operation + return modifySubscriptions } private func makeFetchSubscriptionOperation(for database: CKDatabase, searchForSubscriptionID subscriptionID: String, operationToCancelIfSubcriptionExists operationToCancel: CKModifySubscriptionsOperation) -> CKFetchSubscriptionsOperation { @@ -80,7 +89,8 @@ class SubscribeOperation: AsynchronousOperation { operationToCancel.cancel() } } - + fetchSubscriptions.qualityOfService = .userInteractive + return fetchSubscriptions } From 3d9ffdfeb70e8744108663bd670ff4f298d21589 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 23 Mar 2021 17:05:20 -0700 Subject: [PATCH 098/203] ability to pull a root record, useful for sharing --- Source/Classes/CloudCore.swift | 31 ++- .../Classes/Pull/PullChangesOperation.swift | 234 +++++++++++++++++ Source/Classes/Pull/PullOperation.swift | 248 ++---------------- Source/Classes/Pull/PullRecordOperation.swift | 86 ++++++ 4 files changed, 364 insertions(+), 235 deletions(-) create mode 100644 Source/Classes/Pull/PullChangesOperation.swift create mode 100644 Source/Classes/Pull/PullRecordOperation.swift diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 180ed71f..9d237fd0 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -98,7 +98,7 @@ open class CloudCore { } // Fetch updated data (e.g. push notifications weren't received) - let pullOperation = PullOperation(persistentContainer: container) + let pullOperation = PullChangesOperation(persistentContainer: container) pullOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } @@ -139,7 +139,7 @@ open class CloudCore { DispatchQueue.global(qos: .utility).async { let errorProxy = ErrorBlockProxy(destination: error) - let operation = PullOperation(from: [cloudDatabase], persistentContainer: container) + let operation = PullChangesOperation(from: [cloudDatabase], persistentContainer: container) operation.errorBlock = { errorProxy.send(error: $0) } operation.start() @@ -159,13 +159,36 @@ open class CloudCore { - completion: `PullResult` enumeration with results of operation */ public static func pull(to container: NSPersistentContainer, error: ErrorBlock?, completion: (() -> Void)?) { - let operation = PullOperation(persistentContainer: container) + let operation = PullChangesOperation(persistentContainer: container) operation.errorBlock = error operation.completionBlock = completion - + queue.addOperation(operation) } + /** Fetch one full record from all CloudKit databases and save it to Core Data + + - Parameters: + - recordID: `CKRecord.ID` identifies the record to retrieve + - database: `CKDatabase` identifies which database from the container to use + - container: `NSPersistentContainer` that will be used to save fetched data + - error: block will be called every time when error occurs during process + - completion: `PullResult` enumeration with results of operation + */ + public static func pull(rootRecordID: CKRecord.ID, + database: CKDatabase = config.container.sharedCloudDatabase, + container: NSPersistentContainer, + error: ErrorBlock?, + completion: (() -> Void)?) { + let operation = PullRecordOperation(rootRecordID: rootRecordID, + database: database, + persistentContainer: container) + operation.errorBlock = error + operation.completionBlock = completion + + queue.addOperation(operation) + } + /** Check if notification is CloudKit notification containing CloudCore data - Parameter userInfo: userInfo of notification diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift new file mode 100644 index 00000000..b72e4745 --- /dev/null +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -0,0 +1,234 @@ +// +// PullChangesOperation.swift +// CloudCore +// +// Created by Vasily Ulianov on 13/03/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import CloudKit +import CoreData + +/// An operation that fetches data from CloudKit and saves it to Core Data, you can use it without calling `CloudCore.pull` methods if you application relies on `Operation` +public class PullChangesOperation: PullOperation { + + /// Private cloud database for the CKContainer specified by CloudCoreConfig + public static let allDatabases = [ + CloudCore.config.container.publicCloudDatabase, + CloudCore.config.container.privateCloudDatabase, + CloudCore.config.container.sharedCloudDatabase + ] + + private let databases: [CKDatabase] + private let tokens: Tokens + + /// Initialize operation, it's recommended to set `errorBlock` + /// + /// - Parameters: + /// - databases: list of databases to fetch data from (only private is supported now) + /// - persistentContainer: `NSPersistentContainer` that will be used to save data + /// - tokens: previously saved `Tokens`, you can generate new ones if you want to fetch all data + public init(from databases: [CKDatabase] = PullChangesOperation.allDatabases, + persistentContainer: NSPersistentContainer, + tokens: Tokens = CloudCore.tokens) { + self.databases = databases + self.tokens = tokens + + super.init(persistentContainer: persistentContainer) + + name = "PullChangesOperation" + } + + /// Performs the receiver’s non-concurrent task. + override public func main() { + if self.isCancelled { return } + + CloudCore.delegate?.willSyncFromCloud() + + let backgroundContext = persistentContainer.newBackgroundContext() + backgroundContext.name = CloudCore.config.pullContextName + + for database in self.databases { + if database.databaseScope == .public { + let changedRecordIDs: NSMutableSet = [] + let deletedRecordIDs: NSMutableSet = [] + let previousToken = self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] + let fetchNotificationChanges = CKFetchNotificationChangesOperation(previousServerChangeToken: previousToken) + fetchNotificationChanges.qualityOfService = .userInteractive + fetchNotificationChanges.notificationChangedBlock = { (innerNotification) in + if let innerQueryNotification = innerNotification as? CKQueryNotification { + if innerQueryNotification.queryNotificationReason == .recordDeleted { + deletedRecordIDs.add(innerQueryNotification.recordID!) + changedRecordIDs.remove(innerQueryNotification.recordID!) + } else { + changedRecordIDs.add(innerQueryNotification.recordID!) + } + } + } + fetchNotificationChanges.fetchNotificationChangesCompletionBlock = { (changeToken, error) in + let allChangedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] + let fetchRecords = CKFetchRecordsOperation(recordIDs: allChangedRecordIDs) + fetchRecords.database = CloudCore.config.container.publicCloudDatabase + fetchRecords.qualityOfService = .userInteractive + fetchRecords.perRecordCompletionBlock = { (record, recordID, error) in + if error == nil { + self.addConvertRecordOperation(record: record!, context: backgroundContext) + } + } + fetchRecords.fetchRecordsCompletionBlock = { (_, error) in + self.processMissingReferences(context: backgroundContext) + } + let finished = BlockOperation { } + finished.addDependency(fetchRecords) + database.add(fetchRecords) + self.queue.addOperation(finished) + + let allDeletedRecordIDs = deletedRecordIDs.allObjects as! [CKRecord.ID] + for recordID in allDeletedRecordIDs { + self.addDeleteRecordOperation(recordID: recordID, context: backgroundContext) + } + + self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken + } + let finished = BlockOperation { } + finished.addDependency(fetchNotificationChanges) + CloudCore.config.container.add(fetchNotificationChanges) + self.queue.addOperation(finished) + } else { + var changedZoneIDs = [CKRecordZone.ID]() + var deletedZoneIDs = [CKRecordZone.ID]() + + let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope.rawValue] + let fetchDatabaseChanges = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) + fetchDatabaseChanges.database = database + fetchDatabaseChanges.qualityOfService = .userInteractive + fetchDatabaseChanges.recordZoneWithIDChangedBlock = { (recordZoneID) in + changedZoneIDs.append(recordZoneID) + } + fetchDatabaseChanges.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in + deletedZoneIDs.append(recordZoneID) + } + fetchDatabaseChanges.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in + // TODO: error handling? + + if changedZoneIDs.count > 0 { + self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) + } + if deletedZoneIDs.count > 0 { + self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) + } + + self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken + } + /* + To improve performance overall, and on watchOS in particular + make sure to queue up CK operations on the proper queue + whether its for the container in general or a specific database. + + To maintain the overall logic of CloudCore, we shadow these ops + in our own queues, using no-op block ops with dependencies. + + You will see this pattern elsewhere in CloudCore when appropriate. + */ + let finished = BlockOperation { } + finished.addDependency(fetchDatabaseChanges) + database.add(fetchDatabaseChanges) + self.queue.addOperation(finished) + } + } + + self.queue.waitUntilAllOperationsAreFinished() + + do { + try backgroundContext.save() + } catch { + errorBlock?(error) + } + + tokens.saveToUserDefaults() + + CloudCore.delegate?.didSyncFromCloud() + } + + private func addDeleteRecordOperation(recordID: CKRecord.ID, context: NSManagedObjectContext) { + // Delete NSManagedObject with specified recordID Operation + let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: recordID) + deleteOperation.errorBlock = { self.errorBlock?($0) } + self.queue.addOperation(deleteOperation) + } + + private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZone.ID], database: CKDatabase, context: NSManagedObjectContext) { + if recordZoneIDs.isEmpty { return } + + let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) + recordZoneChangesOperation.qualityOfService = .userInteractive + recordZoneChangesOperation.recordChangedBlock = { + self.addConvertRecordOperation(record: $0, context: context) + } + + recordZoneChangesOperation.recordWithIDWasDeletedBlock = { + self.addDeleteRecordOperation(recordID: $0, context: context) + } + + recordZoneChangesOperation.errorBlock = { zoneID, error in + self.handle(recordZoneChangesError: error, in: zoneID, database: database, context: context) + } + + recordZoneChangesOperation.completionBlock = { + self.processMissingReferences(context: context) + } + + queue.addOperation(recordZoneChangesOperation) + } + + private func deleteRecordsFromDeletedZones(recordZoneIDs: [CKRecordZone.ID]) { + persistentContainer.performBackgroundTask { (moc) in + for entity in self.persistentContainer.managedObjectModel.entities { + if let serviceAttributes = entity.serviceAttributeNames { + for recordZoneID in recordZoneIDs { + do { + let request = NSFetchRequest(entityName: entity.name!) + request.predicate = NSPredicate(format: "%K == %@", serviceAttributes.ownerName, recordZoneID.ownerName) + let results = try moc.fetch(request) as! [NSManagedObject] + for object in results { + moc.delete(object) + } + } catch { + print("Unexpected error: \(error).") + } + } + } + } + + do { + try moc.save() + } catch { + print("Unexpected error: \(error).") + } + } + } + + private func handle(recordZoneChangesError: Error, in zoneId: CKRecordZone.ID, database: CKDatabase, context: NSManagedObjectContext) { + guard let cloudError = recordZoneChangesError as? CKError else { + errorBlock?(recordZoneChangesError) + return + } + + switch cloudError.code { + // User purged cloud database, we need to delete local cache (according Apple Guidelines) + case .userDeletedZone: + queue.cancelAllOperations() + + let purgeOperation = PurgeLocalDatabaseOperation(parentContext: context, managedObjectModel: persistentContainer.managedObjectModel) + purgeOperation.errorBlock = errorBlock + queue.addOperation(purgeOperation) + + // Our token is expired, we need to refetch everything again + case .changeTokenExpired: + tokens.tokensByRecordZoneID[zoneId] = nil + self.addRecordZoneChangesOperation(recordZoneIDs: [zoneId], database: database, context: context) + default: errorBlock?(cloudError) + } + } + +} diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 6e904a3d..7b93be3a 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -2,168 +2,35 @@ // PullOperation.swift // CloudCore // -// Created by Vasily Ulianov on 13/03/2017. -// Copyright © 2017 Vasily Ulianov. All rights reserved. +// Created by deeje cooley on 3/23/21. // import CloudKit import CoreData -/// An operation that fetches data from CloudKit and saves it to Core Data, you can use it without calling `CloudCore.pull` methods if you application relies on `Operation` public class PullOperation: Operation { - - /// Private cloud database for the CKContainer specified by CloudCoreConfig - public static let allDatabases = [ - CloudCore.config.container.publicCloudDatabase, - CloudCore.config.container.privateCloudDatabase, - CloudCore.config.container.sharedCloudDatabase - ] - - private let databases: [CKDatabase] - private let persistentContainer: NSPersistentContainer - private let tokens: Tokens - - /// Called every time if error occurs - public var errorBlock: ErrorBlock? - - private let queue = OperationQueue() - - private var objectsWithMissingReferences = [MissingReferences]() - - /// Initialize operation, it's recommended to set `errorBlock` - /// - /// - Parameters: - /// - databases: list of databases to fetch data from (only private is supported now) - /// - persistentContainer: `NSPersistentContainer` that will be used to save data - /// - tokens: previously saved `Tokens`, you can generate new ones if you want to fetch all data - public init(from databases: [CKDatabase] = PullOperation.allDatabases, - persistentContainer: NSPersistentContainer, - tokens: Tokens = CloudCore.tokens) { - self.databases = databases - self.persistentContainer = persistentContainer - self.tokens = tokens + + internal let persistentContainer: NSPersistentContainer + + /// Called every time if error occurs + public var errorBlock: ErrorBlock? + + internal let queue = OperationQueue() + + internal var objectsWithMissingReferences = [MissingReferences]() + + public init(persistentContainer: NSPersistentContainer) { + self.persistentContainer = persistentContainer super.init() - name = "PullOperation" qualityOfService = .userInteractive queue.name = "PullQueue" queue.maxConcurrentOperationCount = 1 - } - - /// Performs the receiver’s non-concurrent task. - override public func main() { - if self.isCancelled { return } - - CloudCore.delegate?.willSyncFromCloud() - - let backgroundContext = persistentContainer.newBackgroundContext() - backgroundContext.name = CloudCore.config.pullContextName - - for database in self.databases { - if database.databaseScope == .public { - let changedRecordIDs: NSMutableSet = [] - let deletedRecordIDs: NSMutableSet = [] - let previousToken = self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] - let fetchNotificationChanges = CKFetchNotificationChangesOperation(previousServerChangeToken: previousToken) - fetchNotificationChanges.qualityOfService = .userInteractive - fetchNotificationChanges.notificationChangedBlock = { (innerNotification) in - if let innerQueryNotification = innerNotification as? CKQueryNotification { - if innerQueryNotification.queryNotificationReason == .recordDeleted { - deletedRecordIDs.add(innerQueryNotification.recordID!) - changedRecordIDs.remove(innerQueryNotification.recordID!) - } else { - changedRecordIDs.add(innerQueryNotification.recordID!) - } - } - } - fetchNotificationChanges.fetchNotificationChangesCompletionBlock = { (changeToken, error) in - let allChangedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] - let fetchRecords = CKFetchRecordsOperation(recordIDs: allChangedRecordIDs) - fetchRecords.database = CloudCore.config.container.publicCloudDatabase - fetchRecords.qualityOfService = .userInteractive - fetchRecords.perRecordCompletionBlock = { (record, recordID, error) in - if error == nil { - self.addConvertRecordOperation(record: record!, context: backgroundContext) - } - } - fetchRecords.fetchRecordsCompletionBlock = { (_, error) in - self.processMissingReferences(context: backgroundContext) - } - let finished = BlockOperation { } - finished.addDependency(fetchRecords) - database.add(fetchRecords) - self.queue.addOperation(finished) - - let allDeletedRecordIDs = deletedRecordIDs.allObjects as! [CKRecord.ID] - for recordID in allDeletedRecordIDs { - self.addDeleteRecordOperation(recordID: recordID, context: backgroundContext) - } - - self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken - } - let finished = BlockOperation { } - finished.addDependency(fetchNotificationChanges) - CloudCore.config.container.add(fetchNotificationChanges) - self.queue.addOperation(finished) - } else { - var changedZoneIDs = [CKRecordZone.ID]() - var deletedZoneIDs = [CKRecordZone.ID]() - - let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope.rawValue] - let fetchDatabaseChanges = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) - fetchDatabaseChanges.database = database - fetchDatabaseChanges.qualityOfService = .userInteractive - fetchDatabaseChanges.recordZoneWithIDChangedBlock = { (recordZoneID) in - changedZoneIDs.append(recordZoneID) - } - fetchDatabaseChanges.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in - deletedZoneIDs.append(recordZoneID) - } - fetchDatabaseChanges.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in - // TODO: error handling? - - if changedZoneIDs.count > 0 { - self.addRecordZoneChangesOperation(recordZoneIDs: changedZoneIDs, database: database, context: backgroundContext) - } - if deletedZoneIDs.count > 0 { - self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) - } - - self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken - } - /* - To improve performance overall, and on watchOS in particular - make sure to queue up CK operations on the proper queue - whether its for the container in general or a specific database. - - To maintain the overall logic of CloudCore, we shadow these ops - in our own queues, using no-op block ops with dependencies. - - You will see this pattern elsewhere in CloudCore when appropriate. - */ - let finished = BlockOperation { } - finished.addDependency(fetchDatabaseChanges) - database.add(fetchDatabaseChanges) - self.queue.addOperation(finished) - } - } - - self.queue.waitUntilAllOperationsAreFinished() - - do { - try backgroundContext.save() - } catch { - errorBlock?(error) - } - - tokens.saveToUserDefaults() - - CloudCore.delegate?.didSyncFromCloud() - } - - private func addConvertRecordOperation(record: CKRecord, context: NSManagedObjectContext) { + } + + internal func addConvertRecordOperation(record: CKRecord, context: NSManagedObjectContext) { // Convert and write CKRecord To NSManagedObject Operation let convertOperation = RecordToCoreDataOperation(parentContext: context, record: record) convertOperation.errorBlock = { self.errorBlock?($0) } @@ -173,38 +40,7 @@ public class PullOperation: Operation { self.queue.addOperation(convertOperation) } - private func addDeleteRecordOperation(recordID: CKRecord.ID, context: NSManagedObjectContext) { - // Delete NSManagedObject with specified recordID Operation - let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: recordID) - deleteOperation.errorBlock = { self.errorBlock?($0) } - self.queue.addOperation(deleteOperation) - } - - private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZone.ID], database: CKDatabase, context: NSManagedObjectContext) { - if recordZoneIDs.isEmpty { return } - - let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) - recordZoneChangesOperation.qualityOfService = .userInteractive - recordZoneChangesOperation.recordChangedBlock = { - self.addConvertRecordOperation(record: $0, context: context) - } - - recordZoneChangesOperation.recordWithIDWasDeletedBlock = { - self.addDeleteRecordOperation(recordID: $0, context: context) - } - - recordZoneChangesOperation.errorBlock = { zoneID, error in - self.handle(recordZoneChangesError: error, in: zoneID, database: database, context: context) - } - - recordZoneChangesOperation.completionBlock = { - self.processMissingReferences(context: context) - } - - queue.addOperation(recordZoneChangesOperation) - } - - private func processMissingReferences(context: NSManagedObjectContext) { + internal func processMissingReferences(context: NSManagedObjectContext) { // iterate over all missing references and fix them, now are all NSManagedObjects created for missingReferences in objectsWithMissingReferences { for (object, references) in missingReferences { @@ -243,54 +79,4 @@ public class PullOperation: Operation { } } - private func deleteRecordsFromDeletedZones(recordZoneIDs: [CKRecordZone.ID]) { - persistentContainer.performBackgroundTask { (moc) in - for entity in self.persistentContainer.managedObjectModel.entities { - if let serviceAttributes = entity.serviceAttributeNames { - for recordZoneID in recordZoneIDs { - do { - let request = NSFetchRequest(entityName: entity.name!) - request.predicate = NSPredicate(format: "%K == %@", serviceAttributes.ownerName, recordZoneID.ownerName) - let results = try moc.fetch(request) as! [NSManagedObject] - for object in results { - moc.delete(object) - } - } catch { - print("Unexpected error: \(error).") - } - } - } - } - - do { - try moc.save() - } catch { - print("Unexpected error: \(error).") - } - } - } - - private func handle(recordZoneChangesError: Error, in zoneId: CKRecordZone.ID, database: CKDatabase, context: NSManagedObjectContext) { - guard let cloudError = recordZoneChangesError as? CKError else { - errorBlock?(recordZoneChangesError) - return - } - - switch cloudError.code { - // User purged cloud database, we need to delete local cache (according Apple Guidelines) - case .userDeletedZone: - queue.cancelAllOperations() - - let purgeOperation = PurgeLocalDatabaseOperation(parentContext: context, managedObjectModel: persistentContainer.managedObjectModel) - purgeOperation.errorBlock = errorBlock - queue.addOperation(purgeOperation) - - // Our token is expired, we need to refetch everything again - case .changeTokenExpired: - tokens.tokensByRecordZoneID[zoneId] = nil - self.addRecordZoneChangesOperation(recordZoneIDs: [zoneId], database: database, context: context) - default: errorBlock?(cloudError) - } - } - } diff --git a/Source/Classes/Pull/PullRecordOperation.swift b/Source/Classes/Pull/PullRecordOperation.swift new file mode 100644 index 00000000..c8203767 --- /dev/null +++ b/Source/Classes/Pull/PullRecordOperation.swift @@ -0,0 +1,86 @@ +// +// PullRecordOperation.swift +// CloudCore +// +// Created by deeje cooley on 3/23/21. +// + +import CloudKit +import CoreData + +/// An operation that fetches data from CloudKit for one record and all its child records, and saves it to Core Data +public class PullRecordOperation: PullOperation { + + let rootRecordID: CKRecord.ID + let database: CKDatabase + + var fetchedRecordIDs: [CKRecord.ID] = [] + + public init(rootRecordID: CKRecord.ID, database: CKDatabase, persistentContainer: NSPersistentContainer) { + self.rootRecordID = rootRecordID + self.database = database + + super.init(persistentContainer: persistentContainer) + + name = "PullRecordOperation" + } + + override public func main() { + if self.isCancelled { return } + + CloudCore.delegate?.willSyncFromCloud() + + let backgroundContext = persistentContainer.newBackgroundContext() + backgroundContext.name = CloudCore.config.pullContextName + + addFetchRecordsOp(recordIDs: [rootRecordID], backgroundContext: backgroundContext) + + self.queue.waitUntilAllOperationsAreFinished() + + self.processMissingReferences(context: backgroundContext) + + do { + try backgroundContext.save() + } catch { + errorBlock?(error) + } + + CloudCore.delegate?.didSyncFromCloud() + } + + private func addFetchRecordsOp(recordIDs: [CKRecord.ID], backgroundContext: NSManagedObjectContext) { + let fetchRecords = CKFetchRecordsOperation(recordIDs: recordIDs) + fetchRecords.database = database + fetchRecords.qualityOfService = .userInteractive + fetchRecords.perRecordCompletionBlock = { record, recordID, error in + if let record = record { + self.fetchedRecordIDs.append(recordID!) + + self.addConvertRecordOperation(record: record, context: backgroundContext) + + var childIDs: [CKRecord.ID] = [] + record.allKeys().forEach { key in + if let reference = record[key] as? CKRecord.Reference, !self.fetchedRecordIDs.contains(reference.recordID) { + childIDs.append(reference.recordID) + } + if let array = record[key] as? [CKRecord.Reference] { + array.forEach { reference in + if !self.fetchedRecordIDs.contains(reference.recordID) { + childIDs.append(reference.recordID) + } + } + } + } + + if !childIDs.isEmpty { + self.addFetchRecordsOp(recordIDs: childIDs, backgroundContext: backgroundContext) + } + } + } + let finished = BlockOperation { } + finished.addDependency(fetchRecords) + database.add(fetchRecords) + self.queue.addOperation(finished) + } + +} From c19078f20dd3262b090fe0006605ef602cf61a5a Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 30 Mar 2021 12:46:40 -0700 Subject: [PATCH 099/203] formally define persistentHistoryTokenKey --- Source/Classes/Push/CoreDataObserver.swift | 13 ++++++------- Source/Model/CloudCoreConfig.swift | 1 + 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 5d2d52ce..3a9b07fb 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -16,8 +16,8 @@ class CoreDataObserver { let converter = ObjectToRecordConverter() let pushOperationQueue = PushOperationQueue() - - let cloudContextName = "CloudCoreSync" + + static let syncContextName = "CloudCoreSync" let processSemaphore = DispatchSemaphore(value: 1) @@ -94,7 +94,7 @@ class CoreDataObserver { CloudCore.delegate?.willSyncToCloud() let backgroundContext = container.newBackgroundContext() - backgroundContext.name = cloudContextName + backgroundContext.name = CoreDataObserver.syncContextName let records = converter.processPendingOperations(in: backgroundContext) pushOperationQueue.errorBlock = { @@ -228,10 +228,9 @@ class CoreDataObserver { } container.performBackgroundTask { (moc) in - let key = "lastPersistentHistoryTokenKey" let settings = UserDefaults.standard var token: NSPersistentHistoryToken? = nil - if let data = settings.object(forKey: key) as? Data { + if let data = settings.object(forKey: CloudCore.config.persistentHistoryTokenKey) as? Data { token = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken } let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) @@ -245,7 +244,7 @@ class CoreDataObserver { try moc.execute(deleteRequest) if let data = try? NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) { - settings.set(data, forKey: key) + settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) } } else { break @@ -256,7 +255,7 @@ class CoreDataObserver { let nserror = error as NSError switch nserror.code { case NSPersistentHistoryTokenExpiredError: - settings.set(nil, forKey: key) + settings.set(nil, forKey: CloudCore.config.persistentHistoryTokenKey) default: fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index d0832b56..c70d838a 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -77,6 +77,7 @@ public struct CloudCoreConfig { /// /// Default value is `CloudCoreTokens` public var userDefaultsKeyTokens = "CloudCoreTokens" + public var persistentHistoryTokenKey = "lastPersistentHistoryTokenKey" public init() { From e91811654dc8e5c31b46a95862438553e0bb4f05 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 26 Apr 2021 17:56:59 -0700 Subject: [PATCH 100/203] save backghroundContexts in their own thread --- Source/Classes/Pull/PullChangesOperation.swift | 12 +++++++----- Source/Classes/Pull/PullRecordOperation.swift | 10 ++++++---- Source/Classes/Push/CoreDataObserver.swift | 14 ++++++++------ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift index b72e4745..86de0c46 100644 --- a/Source/Classes/Pull/PullChangesOperation.swift +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -139,11 +139,13 @@ public class PullChangesOperation: PullOperation { self.queue.waitUntilAllOperationsAreFinished() - do { - try backgroundContext.save() - } catch { - errorBlock?(error) - } + backgroundContext.performAndWait { + do { + try backgroundContext.save() + } catch { + errorBlock?(error) + } + } tokens.saveToUserDefaults() diff --git a/Source/Classes/Pull/PullRecordOperation.swift b/Source/Classes/Pull/PullRecordOperation.swift index c8203767..f139b65e 100644 --- a/Source/Classes/Pull/PullRecordOperation.swift +++ b/Source/Classes/Pull/PullRecordOperation.swift @@ -39,10 +39,12 @@ public class PullRecordOperation: PullOperation { self.processMissingReferences(context: backgroundContext) - do { - try backgroundContext.save() - } catch { - errorBlock?(error) + backgroundContext.performAndWait { + do { + try backgroundContext.save() + } catch { + errorBlock?(error) + } } CloudCore.delegate?.didSyncFromCloud() diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 3a9b07fb..a151ec1e 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -105,13 +105,15 @@ class CoreDataObserver { pushOperationQueue.waitUntilAllOperationsAreFinished() if success { - do { - if backgroundContext.hasChanges { - try backgroundContext.save() + backgroundContext.performAndWait { + do { + if backgroundContext.hasChanges { + try backgroundContext.save() + } + } catch { + delegate?.error(error: error, module: .some(.pushToCloud)) + success = false } - } catch { - delegate?.error(error: error, module: .some(.pushToCloud)) - success = false } } From ff25623b47b0cf2a6dec995a0eb87c70212106fe Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 27 Apr 2021 15:57:09 -0700 Subject: [PATCH 101/203] ensure all Ops ask for background task time --- Source/Classes/Pull/PullChangesOperation.swift | 10 ++++++++++ Source/Classes/Pull/PullRecordOperation.swift | 10 ++++++++++ .../SubOperations/DeleteFromCoreDataOperation.swift | 12 +++++++++++- .../FetchRecordZoneChangesOperation.swift | 12 +++++++++++- .../SubOperations/PurgeLocalDatabaseOperation.swift | 10 ++++++++++ .../SubOperations/RecordToCoreDataOperation.swift | 12 +++++++++++- .../ObjectToRecord/ObjectToRecordOperation.swift | 10 ++++++++++ Source/Classes/Setup/PushAllLocalDataOperation.swift | 10 ++++++++++ Source/Classes/Setup/SetupOperation.swift | 10 ++++++++++ 9 files changed, 93 insertions(+), 3 deletions(-) diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift index 86de0c46..dcc59650 100644 --- a/Source/Classes/Pull/PullChangesOperation.swift +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -43,6 +43,16 @@ public class PullChangesOperation: PullOperation { override public func main() { if self.isCancelled { return } + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + CloudCore.delegate?.willSyncFromCloud() let backgroundContext = persistentContainer.newBackgroundContext() diff --git a/Source/Classes/Pull/PullRecordOperation.swift b/Source/Classes/Pull/PullRecordOperation.swift index f139b65e..2617f042 100644 --- a/Source/Classes/Pull/PullRecordOperation.swift +++ b/Source/Classes/Pull/PullRecordOperation.swift @@ -28,6 +28,16 @@ public class PullRecordOperation: PullOperation { override public func main() { if self.isCancelled { return } + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + CloudCore.delegate?.willSyncFromCloud() let backgroundContext = persistentContainer.newBackgroundContext() diff --git a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift index af50ff76..3db94209 100644 --- a/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/DeleteFromCoreDataOperation.swift @@ -13,7 +13,7 @@ class DeleteFromCoreDataOperation: Operation { let parentContext: NSManagedObjectContext let recordID: CKRecord.ID var errorBlock: ErrorBlock? - + init(parentContext: NSManagedObjectContext, recordID: CKRecord.ID) { self.parentContext = parentContext self.recordID = recordID @@ -27,6 +27,16 @@ class DeleteFromCoreDataOperation: Operation { override func main() { if self.isCancelled { return } + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) childContext.performAndWait { childContext.parent = parentContext diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index 63238666..4a660e06 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -43,7 +43,17 @@ class FetchRecordZoneChangesOperation: Operation { override func main() { super.main() - + + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + let fetchOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) let finish = BlockOperation { } finish.addDependency(fetchOperation) diff --git a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift index a1ecb375..cb04c79e 100644 --- a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift +++ b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift @@ -27,6 +27,16 @@ class PurgeLocalDatabaseOperation: Operation { override func main() { super.main() + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) childContext.performAndWait { childContext.parent = parentContext diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 0a5549f5..c3dc8282 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -35,7 +35,17 @@ public class RecordToCoreDataOperation: AsynchronousOperation { override public func main() { if self.isCancelled { return } - + + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + parentContext.performAndWait { do { try self.setManagedObject(in: self.parentContext) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 513dc959..ac0ae404 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -43,6 +43,16 @@ class ObjectToRecordOperation: Operation { return } + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) childContext.performAndWait { childContext.parent = parentContext diff --git a/Source/Classes/Setup/PushAllLocalDataOperation.swift b/Source/Classes/Setup/PushAllLocalDataOperation.swift index 1a9d0ba4..2ca681af 100644 --- a/Source/Classes/Setup/PushAllLocalDataOperation.swift +++ b/Source/Classes/Setup/PushAllLocalDataOperation.swift @@ -37,6 +37,16 @@ class PushAllLocalDataOperation: Operation { override func main() { super.main() + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + CloudCore.delegate?.willSyncToCloud() defer { CloudCore.delegate?.didSyncToCloud() diff --git a/Source/Classes/Setup/SetupOperation.swift b/Source/Classes/Setup/SetupOperation.swift index ed81aff5..f65f2e6c 100644 --- a/Source/Classes/Setup/SetupOperation.swift +++ b/Source/Classes/Setup/SetupOperation.swift @@ -40,6 +40,16 @@ class SetupOperation: Operation { override func main() { super.main() + #if TARGET_OS_IOS + let app = UIApplication.shared + var backgroundTaskID = app.beginBackgroundTask(withName: name) { + app.endBackgroundTask(backgroundTaskID!) + } + defer { + app.endBackgroundTask(backgroundTaskID!) + } + #endif + let childContext = container.newBackgroundContext() var operations: [Operation] = [] From cbeaaebc4eef3b77698fd89b0ae373f8ab680afd Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 11 May 2021 13:29:49 -0700 Subject: [PATCH 102/203] NSPersistentContainer.performBackgroundPushTask convenience func to automatically name the background context, which triggers the push to CloudKit --- Source/Extensions/NSManagedContainer.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Source/Extensions/NSManagedContainer.swift diff --git a/Source/Extensions/NSManagedContainer.swift b/Source/Extensions/NSManagedContainer.swift new file mode 100644 index 00000000..a78e4629 --- /dev/null +++ b/Source/Extensions/NSManagedContainer.swift @@ -0,0 +1,19 @@ +// +// NSManagedObjectContext.swift +// CloudCore +// +// Created by deeje cooley on 5/11/21. +// + +import CoreData + +extension NSPersistentContainer { + + public func performBackgroundPushTask(_ block: @escaping (NSManagedObjectContext) -> Void) { + performBackgroundTask { moc in + moc.name = CloudCore.config.pushContextName + block(moc) + } + } + +} From 66d68ca9919a30a032e20985861f2ead995fe8d8 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 11 May 2021 13:44:52 -0700 Subject: [PATCH 103/203] grab ownerName, targetScope inside context! --- .../Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 69141eb3..06d5c6d5 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -81,11 +81,11 @@ class ObjectToRecordConverter { self?.errorBlock?(error) } + let targetScope = targetScope(for: scope, and: object) + let cloudDatabase = database(for: targetScope) convertOperation.conversionCompletionBlock = { [weak self] record in guard let me = self else { return } - let targetScope = me.targetScope(for: scope, and: object) - let cloudDatabase = me.database(for: targetScope) let recordWithDB = RecordWithDatabase(record, cloudDatabase) me.convertedRecords.append(recordWithDB) } From 0ad603baa646af90e858d0f7cbd3dd691bf0b119 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 19 May 2021 13:19:30 -0700 Subject: [PATCH 104/203] eliminate warning re CloudCoreDelegate: AnyObject --- Source/Protocols/CloudCoreDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Protocols/CloudCoreDelegate.swift b/Source/Protocols/CloudCoreDelegate.swift index d0f28cf0..d4492f0c 100644 --- a/Source/Protocols/CloudCoreDelegate.swift +++ b/Source/Protocols/CloudCoreDelegate.swift @@ -11,7 +11,7 @@ import Foundation /// Delegate for framework that can be used for proccesses tracking and error handling. /// Maybe usefull to activate `UIApplication.networkActivityIndicatorVisible`. /// All methods are optional. -public protocol CloudCoreDelegate: class { +public protocol CloudCoreDelegate: AnyObject { // MARK: Notifications From 11c94d2894bb6e900e38b443ab51c559dd0d6002 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 19 May 2021 13:19:59 -0700 Subject: [PATCH 105/203] update Example to use Connectivity cocoapod --- .../project.pbxproj | 19 +++++++------- .../xcschemes/CloudCoreExample.xcscheme | 10 +++---- Example/Podfile | 4 +-- Example/Sources/AppDelegate.swift | 26 ++++++++----------- .../Class/FRCTableViewDataSource.swift | 2 +- 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index 68404889..efb479d9 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 1030; + LastUpgradeCheck = 1250; ORGANIZATIONNAME = "Vasily Ulianov"; TargetAttributes = { E2C3E34F1E53299800A733BF = { @@ -211,10 +211,9 @@ }; buildConfigurationList = E2C3E34B1E53299800A733BF /* Build configuration list for PBXProject "CloudCoreExample" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - English, en, Base, ); @@ -268,14 +267,14 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh", "${BUILT_PRODUCTS_DIR}/CloudCore/CloudCore.framework", + "${BUILT_PRODUCTS_DIR}/Connectivity/Connectivity.framework", "${BUILT_PRODUCTS_DIR}/Fakery/Fakery.framework", - "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CloudCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Connectivity.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Fakery.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -349,6 +348,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -373,7 +373,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -407,6 +407,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -425,7 +426,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -442,7 +443,7 @@ CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; DEVELOPMENT_TEAM = 26Y8AQV29F; INFOPLIST_FILE = Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -458,7 +459,7 @@ CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; DEVELOPMENT_TEAM = 26Y8AQV29F; INFOPLIST_FILE = Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme b/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme index 348ba421..4e594a8e 100644 --- a/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme +++ b/Example/CloudCoreExample.xcodeproj/xcshareddata/xcschemes/CloudCoreExample.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - 3.3.0' - pod 'ReachabilitySwift' + pod 'Connectivity' end diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index e5654cb6..ae85c71b 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -9,7 +9,7 @@ import UIKit import CoreData import CloudCore -import Reachability +import Connectivity let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer @@ -18,7 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele let delegateHandler = CloudCoreDelegateHandler() - var reachability: Reachability? + var connectivity: Connectivity? func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Register for push notifications about changes @@ -28,23 +28,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele CloudCore.delegate = delegateHandler CloudCore.enable(persistentContainer: persistentContainer) - NotificationCenter.default.addObserver(self, - selector: #selector(reachabilityChanged(notification:)), - name: .reachabilityChanged, - object: reachability) + let connectivityChanged: (Connectivity) -> Void = { connectivity in + let online : [ConnectivityStatus] = [.connected, .connectedViaCellular, .connectedViaWiFi] + CloudCore.isOnline = online.contains(connectivity.status) + } - reachability = Reachability(hostname: "icloud.com") - try? reachability?.startNotifier() + connectivity = Connectivity(shouldUseHTTPS: false) + connectivity?.whenConnected = connectivityChanged + connectivity?.whenDisconnected = connectivityChanged + connectivity?.startNotifier() return true } - - @objc private func reachabilityChanged(notification: Notification) { - let reachability = notification.object as! Reachability - - CloudCore.isOnline = reachability.connection != .none - } - + // Notification from CloudKit about changes in remote database func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // Check if it CloudKit's and CloudCore notification diff --git a/Example/Sources/Class/FRCTableViewDataSource.swift b/Example/Sources/Class/FRCTableViewDataSource.swift index db5cbf09..10832bd7 100644 --- a/Example/Sources/Class/FRCTableViewDataSource.swift +++ b/Example/Sources/Class/FRCTableViewDataSource.swift @@ -4,7 +4,7 @@ import UIKit import CoreData -protocol FRCTableViewDelegate: class { +protocol FRCTableViewDelegate: AnyObject { func frcTableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell } From f564b41b31955c1bc4cfe316d8253f7424daeea5 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 19 May 2021 13:30:38 -0700 Subject: [PATCH 106/203] update Xcode project, target iOS 13, watchOS 6 --- CloudCore.xcodeproj/project.pbxproj | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index a13d7bf2..1a10e3b1 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57505AAF21A7591500D9CF8F /* PullResult.swift */; }; + 575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */; }; + 575ADF472655AB7C0050D693 /* PullChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; @@ -89,6 +91,8 @@ /* Begin PBXFileReference section */ 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57505AAF21A7591500D9CF8F /* PullResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullResult.swift; sourceTree = ""; }; + 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullRecordOperation.swift; sourceTree = ""; }; + 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullChangesOperation.swift; sourceTree = ""; }; 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5C6298B1C3A8BBD007F7B7C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -338,6 +342,8 @@ E277DB0F1E77FC9F00DC334A /* PublicSubscriptions */, E2C02A171E4CDEDA001B2871 /* SubOperations */, E2E4D83D1E76D4EF00550CBE /* PullOperation.swift */, + 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */, + 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */, ); path = Pull; sourceTree = ""; @@ -704,8 +710,10 @@ D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */, D97465FA1FE31A650060EA66 /* Module.swift in Sources */, 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */, + 575ADF472655AB7C0050D693 /* PullChangesOperation.swift in Sources */, E29BB2371E4377F80020F5B6 /* CoreDataAttribute.swift in Sources */, E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */, + 575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */, E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */, E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */, D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */, @@ -848,16 +856,18 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - MACOSX_DEPLOYMENT_TARGET = 10.12; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SUPPORTED_PLATFORMS = "macosx watchsimulator watchos appletvsimulator appletvos iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; + TVOS_DEPLOYMENT_TARGET = 13.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Debug; }; @@ -902,16 +912,18 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - MACOSX_DEPLOYMENT_TARGET = 10.12; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SUPPORTED_PLATFORMS = "macosx watchsimulator watchos appletvsimulator appletvos iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; + TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Release; }; @@ -935,8 +947,6 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 10.0; - WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; @@ -959,8 +969,6 @@ SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 10.0; - WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; @@ -980,7 +988,6 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = Tests/CloudKitTests/App/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; @@ -1005,7 +1012,6 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = Tests/CloudKitTests/App/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; @@ -1027,7 +1033,6 @@ DEVELOPMENT_TEAM = 7X2PJ6H6YM; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Tests/CloudKitTests/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = cloudtests.CloudKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1052,7 +1057,6 @@ DEVELOPMENT_TEAM = 7X2PJ6H6YM; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Tests/CloudKitTests/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = cloudtests.CloudKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1071,7 +1075,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/CloudCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = uvasily.CloudCoreTests; @@ -1090,7 +1093,6 @@ CODE_SIGN_IDENTITY = ""; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/CloudCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = uvasily.CloudCoreTests; From 2ead889b86d0aabf02c6ce037c5a9bc96a5a2a0c Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 19 May 2021 13:31:03 -0700 Subject: [PATCH 107/203] use Xcode 12.5 for travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9dd0d2cf..f7237cce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -osx_image: xcode10.3 +osx_image: xcode12.5 language: swift podfile: "Example/Podfile" From d9092f56e81d1c0434e6de702d08931ecdc2199d Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 25 May 2021 11:16:40 -0700 Subject: [PATCH 108/203] CloudCore now fetches userRecordID --- Source/Classes/CloudCore.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 9d237fd0..f1748eaa 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -76,6 +76,8 @@ open class CloudCore { public typealias NotificationUserInfo = [AnyHashable : Any] + public static var userRecordID: CKRecord.ID? = nil + static private let queue = OperationQueue() // MARK: - Methods @@ -107,6 +109,12 @@ open class CloudCore { queue.addOperation(subscribeOperation) queue.addOperation(pullOperation) + + config.container.fetchUserRecordID { recordID, error in + if error == nil { + self.userRecordID = recordID + } + } } /// Disables synchronization (push notifications won't be sent also) @@ -119,6 +127,11 @@ open class CloudCore { // FIXME: unsubscribe } + /// return the user's record name + public static func userRecordName() -> String? { + return userRecordID?.recordName + } + // MARK: Fetchers /** Fetch changes from one CloudKit database and save it to CoreData from `didReceiveRemoteNotification` method. From bc8d77cddd9c89a89833bb4da8739f98f0eaae1c Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 27 May 2021 17:05:13 -0700 Subject: [PATCH 109/203] add Sharing support --- Source/Classes/Sharing/CloudCoreSharing.swift | 139 ++++++++++++++++++ .../Sharing/CloudCoreSharingController.swift | 130 ++++++++++++++++ .../UIViewController+CloudKit.swift | 54 +++++++ Source/Protocols/CloudCoreType.swift | 20 +++ Source/Protocols/CloudKitSharing.swift | 16 ++ 5 files changed, 359 insertions(+) create mode 100644 Source/Classes/Sharing/CloudCoreSharing.swift create mode 100644 Source/Classes/Sharing/CloudCoreSharingController.swift create mode 100644 Source/Extensions/UIViewController+CloudKit.swift create mode 100644 Source/Protocols/CloudCoreType.swift create mode 100644 Source/Protocols/CloudKitSharing.swift diff --git a/Source/Classes/Sharing/CloudCoreSharing.swift b/Source/Classes/Sharing/CloudCoreSharing.swift new file mode 100644 index 00000000..fcc3700f --- /dev/null +++ b/Source/Classes/Sharing/CloudCoreSharing.swift @@ -0,0 +1,139 @@ +// +// CloudCoreSharing.swift +// CloudCore +// +// Created by deeje cooley on 5/25/21. +// + +import CoreData +import CloudKit + +public typealias FetchedEditablePermissionsCompletionBlock = (_ canEdit: Bool) -> Void +public typealias StopSharingCompletionBlock = (_ didStop: Bool) -> Void + +public protocol CloudCoreSharing: CloudKitSharing, CloudCoreType { + + var isOwnedByCurrentUser: Bool { get } + var shareRecordData: Data? { get set } + + func fetchExistingShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) + func fetchShareRecord(in persistentContainer: NSPersistentContainer, completion: @escaping ((CKShare?, Error?) -> Void)) + func fetchEditablePermissions(completion: @escaping FetchedEditablePermissionsCompletionBlock) + func setShare(data: Data?, in persistentContainer: NSPersistentContainer) + func stopSharing(completion: @escaping StopSharingCompletionBlock) + +} + +extension CloudCoreSharing { + + public var isOwnedByCurrentUser: Bool { + get { + return ownerName == CKCurrentUserDefaultName + } + } + + public func fetchExistingShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) { + if let shareData = shareRecordData { + let aShare = CKShare(archivedData: shareData)! + + var database = CloudCore.config.container.sharedCloudDatabase + var ownerUUID = ownerName! + if ownerUUID == CKCurrentUserDefaultName { + database = CloudCore.config.container.privateCloudDatabase + ownerUUID = CloudCore.userRecordName()! + } + + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerUUID) + let shareID = CKRecord.ID(recordName: aShare.recordID.recordName, zoneID: zoneID) + database.fetch(withRecordID: shareID) { (record, error) in + completion(record as? CKShare, error) + } + } else { + completion(nil, nil) + } + } + + public func fetchShareRecord(in persistentContainer: NSPersistentContainer, completion: @escaping ((CKShare?, Error?) -> Void)) { + fetchExistingShareRecord { share, error in + var createIt = false + if share == nil && error == nil { + createIt = true + } + if let ckError = error as? CKError, + ckError.code == .unknownItem { + createIt = true + } + if createIt, let aRecord = try? self.restoreRecordWithSystemFields(for: .private) { + let newShare = CKShare(rootRecord: aRecord) + newShare[CKShare.SystemFieldKey.title] = self.sharingTitle as CKRecordValue? + newShare[CKShare.SystemFieldKey.shareType] = self.sharingType as CKRecordValue? + + let modOp: CKModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [newShare, aRecord], recordIDsToDelete: nil) + modOp.savePolicy = .ifServerRecordUnchanged + modOp.modifyRecordsCompletionBlock = {records, recordIDs, error in + let savedShare = records?.first as? CKShare + if let savedShare = savedShare { + let shareData = savedShare.encdodedSystemFields + self.setShare(data: shareData, in: persistentContainer) + } + completion(savedShare, error) + } + CloudCore.config.container.privateCloudDatabase.add(modOp) + } else { + completion(share, error) + } + } + } + + public func fetchEditablePermissions(completion: @escaping FetchedEditablePermissionsCompletionBlock) { + if isOwnedByCurrentUser { + completion(true) + } else { + fetchExistingShareRecord { record, error in + var canEdit = false + + if let fetchedShare = record { + for aParticipant in fetchedShare.participants { + if aParticipant.userIdentity.userRecordID?.recordName == CKCurrentUserDefaultName { + canEdit = aParticipant.permission == .readWrite + break + } + } + } + + DispatchQueue.main.async { + completion(canEdit) + } + } + } + } + + public func setShare(data: Data?, in persistentContainer: NSPersistentContainer) { + persistentContainer.performBackgroundPushTask { moc in + if let updatedObject = try? moc.existingObject(with: self.objectID) as? CloudCoreSharing { + updatedObject.shareRecordData = data + try? moc.save() + } + } + } + + public func stopSharing(completion: @escaping StopSharingCompletionBlock) { + if isOwnedByCurrentUser { + completion(false) + + return + } + + if let shareData = shareRecordData { + let sharedDB = CloudCore.config.container.sharedCloudDatabase + + let aShare = CKShare(archivedData: shareData)! + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerName!) + let shareID = CKRecord.ID(recordName: aShare.recordID.recordName, zoneID: zoneID) + sharedDB.delete(withRecordID: shareID) { recordID, error in + completion(error == nil) + } + } + } + +} diff --git a/Source/Classes/Sharing/CloudCoreSharingController.swift b/Source/Classes/Sharing/CloudCoreSharingController.swift new file mode 100644 index 00000000..7168327e --- /dev/null +++ b/Source/Classes/Sharing/CloudCoreSharingController.swift @@ -0,0 +1,130 @@ +// +// CloudCoreSharingController.swift +// CloudCore +// +// Created by deeje cooley on 5/25/21. +// + +#if os(iOS) + +import UIKit +import CoreData +import CloudKit + +public typealias ConfigureSharingCompletionBlock = (_ sharingController: UICloudSharingController?) -> Void + +public class CloudCoreSharingController: NSObject, UICloudSharingControllerDelegate { + + let persistentContainer: NSPersistentContainer + let object: CloudCoreSharing + + public init(persistentContainer: NSPersistentContainer, object: CloudCoreSharing) { + self.persistentContainer = persistentContainer + self.object = object + } + + public func configureSharingController(permissions: UICloudSharingController.PermissionOptions, + completion: @escaping ConfigureSharingCompletionBlock) { + + func commonConfigure(_ sharingController: UICloudSharingController) { + sharingController.availablePermissions = permissions + sharingController.delegate = self + completion(sharingController) + } + + if let aRecord = try? object.restoreRecordWithSystemFields(for: .private) { + let aShare: CKShare + if let shareData = object.shareRecordData { + aShare = CKShare(archivedData: shareData)! + if object.isOwnedByCurrentUser { + CloudCore.config.container.privateCloudDatabase.fetch(withRecordID: aShare.recordID) { (record, error) in + if let fetchedShare = record as? CKShare { + DispatchQueue.main.async { + let sharingController = UICloudSharingController(share: fetchedShare, container: CloudCore.config.container) + commonConfigure(sharingController) + } + } else if let ckError = error as? CKError { + switch ckError.code { + case .unknownItem: + self.object.setShare(data: nil, in: self.persistentContainer) + default: + break + } + completion(nil) + } + } + } else { + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: object.ownerName!) + let shareID = CKRecord.ID(recordName: aShare.recordID.recordName, zoneID: zoneID) + CloudCore.config.container.sharedCloudDatabase.fetch(withRecordID: shareID) { (record, error) in + if let fetchedShare = record as? CKShare { + DispatchQueue.main.async { + let sharingController = UICloudSharingController(share: fetchedShare, container: CloudCore.config.container) + commonConfigure(sharingController) + } + } else { + completion(nil) + } + } + } + } else { + aShare = CKShare(rootRecord: aRecord) + aShare[CKShare.SystemFieldKey.title] = object.sharingTitle as CKRecordValue? + aShare[CKShare.SystemFieldKey.shareType] = object.sharingType as CKRecordValue? + + let sharingController = UICloudSharingController { (_, handler: + @escaping (CKShare?, CKContainer?, Error?) -> Void) in + + let modifyOp = CKModifyRecordsOperation(recordsToSave: [aRecord, aShare], recordIDsToDelete: nil) + modifyOp.savePolicy = .changedKeys + modifyOp.modifyRecordsCompletionBlock = { (records, recordIDs, error) in + if let share = records?.first as? CKShare { + self.object.setShare(data: aShare.encdodedSystemFields, in: self.persistentContainer) + + handler(share, CloudCore.config.container, error) + } else { + handler(nil, nil, error) + } + } + modifyOp.savePolicy = .changedKeys + CloudCore.config.container.privateCloudDatabase.add(modifyOp) + } + + commonConfigure(sharingController) + } + } + } + + public func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) { +// os_log(.debug, "failed to save share") + } + + public func itemTitle(for csc: UICloudSharingController) -> String? { + return object.sharingTitle + } + + public func itemThumbnailData(for csc: UICloudSharingController) -> Data? { + return object.sharingImage + } + + public func itemType(for csc: UICloudSharingController) -> String? { + return object.sharingType + } + + public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { + persistentContainer.performBackgroundTask { moc in + if let updatedObject = try? moc.existingObject(with: self.object.objectID) as? CloudCoreSharing { + if updatedObject.isOwnedByCurrentUser { + moc.name = CloudCore.config.pushContextName + updatedObject.shareRecordData = nil + } else { + moc.delete(updatedObject) + } + try? moc.save() + } + } + } + +} + +#endif diff --git a/Source/Extensions/UIViewController+CloudKit.swift b/Source/Extensions/UIViewController+CloudKit.swift new file mode 100644 index 00000000..a3bc2670 --- /dev/null +++ b/Source/Extensions/UIViewController+CloudKit.swift @@ -0,0 +1,54 @@ +// +// UIViewController+CloudKit.swift +// CloudCore +// +// Created by deeje cooley on 12/5/20. +// + +#if os(iOS) + +import UIKit +import CloudKit + +extension UIViewController { + + public func iCloudAvailable(completion: @escaping ((Bool) -> Void)) { + CloudCore.config.container.accountStatus { accountStatus, error in + DispatchQueue.main.async() { + var title: String? + var message: String? + + switch accountStatus { + case .noAccount: + title = "Sign in to iCloud and\nenable iCloud Drive" + message = "On the Home screen, launch Settings, tap iCloud, and enter your Apple ID. Turn iCloud Drive on. If you don't have an iCloud account, tap Create a new Apple ID." + + case .available: + completion(true) + + case .couldNotDetermine: + title = "iCloud Unavailable" + message = "Could not determine the status of your iCloud account" + + case .restricted: + title = "iCloud Restricted" + message = "You'll need permissions changed on your iCloud account" + + @unknown default: + break + } + + if let title = title, let message = message { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in })) + self.present(alert, animated: true) { + completion(false) + } + } + } + } + } + +} + +#endif diff --git a/Source/Protocols/CloudCoreType.swift b/Source/Protocols/CloudCoreType.swift new file mode 100644 index 00000000..50b0fad7 --- /dev/null +++ b/Source/Protocols/CloudCoreType.swift @@ -0,0 +1,20 @@ +// +// CloudCoreType.swift +// CloudCore +// +// Created by deeje cooley on 5/25/21. +// + +import CoreData + +/// thinking of a typesafe way to identify the required fields + +public protocol CloudCoreType where Self: NSManagedObject { + + var recordName: String? { get } + var ownerName: String? { get } + var publicRecordData: Data? { get } + var privateRecordData: Data? { get set } + +} + diff --git a/Source/Protocols/CloudKitSharing.swift b/Source/Protocols/CloudKitSharing.swift new file mode 100644 index 00000000..6011d1f1 --- /dev/null +++ b/Source/Protocols/CloudKitSharing.swift @@ -0,0 +1,16 @@ +// +// CloudKitSharing.swift +// CloudCore +// +// Created by deeje cooley on 5/25/21. +// + +import CoreData + +public protocol CloudKitSharing where Self: NSManagedObject { + + var sharingTitle: String? { get } + var sharingType: String? { get } + var sharingImage: Data? { get } + +} From 0aff6d7adc8d57ce40c5c4d4da773b6fd7b96d1c Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 27 May 2021 17:05:41 -0700 Subject: [PATCH 110/203] update Example with Sharing support --- .../project.pbxproj | 22 ++++- Example/Resources/Info.plist | 4 +- Example/Sources/AppDelegate.swift | 16 +++- .../Class/FRCTableViewDataSource.swift | 46 ++-------- .../Model.xcdatamodel/contents | 43 +++++----- .../Model/Organization+CloudCoreSharing.swift | 47 +++++++++++ .../Model/Organization+CoreDataClass.swift | 16 ++++ .../DetailViewController.swift | 48 ++++++++--- .../MasterViewController.swift | 84 ++++++++++++++----- 9 files changed, 230 insertions(+), 96 deletions(-) rename Example/{Resources => Sources/Model}/Model.xcdatamodeld/Model.xcdatamodel/contents (66%) create mode 100644 Example/Sources/Model/Organization+CloudCoreSharing.swift create mode 100644 Example/Sources/Model/Organization+CoreDataClass.swift diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index efb479d9..61fd0b22 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 579DFE0B2660506100B0A079 /* Organization+CloudCoreSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */; }; + 579DFE0D266050A000B0A079 /* Organization+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579DFE0C266050A000B0A079 /* Organization+CoreDataClass.swift */; }; B4532A37427BB629A3A47821 /* Pods_CloudCoreExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */; }; D97438161FE168D800650541 /* FRCTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97438151FE168D800650541 /* FRCTableViewDataSource.swift */; }; D974381D1FE16E6E00650541 /* ModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D974381C1FE16E6E00650541 /* ModelFactory.swift */; }; @@ -38,6 +40,8 @@ /* Begin PBXFileReference section */ 2AD0596598E464554C061BBB /* Pods-CloudCoreExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample.release.xcconfig"; sourceTree = ""; }; 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Organization+CloudCoreSharing.swift"; sourceTree = ""; }; + 579DFE0C266050A000B0A079 /* Organization+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Organization+CoreDataClass.swift"; sourceTree = ""; }; D97438151FE168D800650541 /* FRCTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRCTableViewDataSource.swift; sourceTree = ""; }; D974381C1FE16E6E00650541 /* ModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelFactory.swift; sourceTree = ""; }; D974381E1FE18ED100650541 /* NotificationsObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsObserver.swift; sourceTree = ""; }; @@ -69,6 +73,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 579DFE092660502600B0A079 /* Model */ = { + isa = PBXGroup; + children = ( + E2C3E3551E53299800A733BF /* Model.xcdatamodeld */, + 579DFE0C266050A000B0A079 /* Organization+CoreDataClass.swift */, + 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */, + ); + path = Model; + sourceTree = ""; + }; BFBADA7DFB65C4DA1FA75BBE /* Pods */ = { isa = PBXGroup; children = ( @@ -113,7 +127,6 @@ E2C3E3641E53299800A733BF /* Info.plist */, E2C3E3611E53299800A733BF /* LaunchScreen.storyboard */, E2C3E35C1E53299800A733BF /* Main.storyboard */, - E2C3E3551E53299800A733BF /* Model.xcdatamodeld */, ); path = Resources; sourceTree = ""; @@ -141,6 +154,7 @@ isa = PBXGroup; children = ( E2C3E3531E53299800A733BF /* AppDelegate.swift */, + 579DFE092660502600B0A079 /* Model */, D97438201FE1919300650541 /* View Controller */, D97438211FE191A500650541 /* Class */, D97438241FE19A0E00650541 /* View */, @@ -290,10 +304,12 @@ files = ( D974381F1FE18ED100650541 /* NotificationsObserver.swift in Sources */, D97438231FE199F500650541 /* EmployeeTableViewCell.swift in Sources */, + 579DFE0D266050A000B0A079 /* Organization+CoreDataClass.swift in Sources */, E2C3E3571E53299800A733BF /* Model.xcdatamodeld in Sources */, E2C3E3541E53299800A733BF /* AppDelegate.swift in Sources */, D974381D1FE16E6E00650541 /* ModelFactory.swift in Sources */, D97438161FE168D800650541 /* FRCTableViewDataSource.swift in Sources */, + 579DFE0B2660506100B0A079 /* Organization+CloudCoreSharing.swift in Sources */, E2C3E3591E53299800A733BF /* MasterViewController.swift in Sources */, E2C3E35B1E53299800A733BF /* DetailViewController.swift in Sources */, ); @@ -445,6 +461,8 @@ INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = com.deeje.sample.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; @@ -461,6 +479,8 @@ INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.2; + PRODUCT_BUNDLE_IDENTIFIER = com.deeje.sample.CloudCore; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; diff --git a/Example/Resources/Info.plist b/Example/Resources/Info.plist index 5cb06316..7a65846c 100644 --- a/Example/Resources/Info.plist +++ b/Example/Resources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1 + $(MARKETING_VERSION) CFBundleVersion 1 LSRequiresIPhoneOS @@ -48,5 +48,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + CKSharingSupported + diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index ae85c71b..c7829301 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -53,7 +53,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele }) } } - + + // User accepted a sharing link, pull the complete record + func application(_ application: UIApplication, + userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { + let acceptShareOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata]) + acceptShareOperation.qualityOfService = .userInteractive + acceptShareOperation.perShareCompletionBlock = { meta, share, error in + CloudCore.pull(rootRecordID: meta.rootRecordID, container: self.persistentContainer, error: nil) { } + } + acceptShareOperation.acceptSharesCompletionBlock = { error in + // N/A + } + CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation) + } + // MARK: - Default Apple initialization, you can skip that var window: UIWindow? diff --git a/Example/Sources/Class/FRCTableViewDataSource.swift b/Example/Sources/Class/FRCTableViewDataSource.swift index 10832bd7..762994fd 100644 --- a/Example/Sources/Class/FRCTableViewDataSource.swift +++ b/Example/Sources/Class/FRCTableViewDataSource.swift @@ -59,47 +59,11 @@ class FRCTableViewDataSource: NSObject // MARK: - NSFetchedResultsControllerDelegate - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - tableView?.beginUpdates() - } - - func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { - let sectionIndexSet = IndexSet(integer: sectionIndex) - - switch type { - case .insert: tableView?.insertSections(sectionIndexSet, with: .automatic) - case .delete: tableView?.deleteSections(sectionIndexSet, with: .automatic) - case .update: tableView?.reloadSections(sectionIndexSet, with: .automatic) - case .move: break - @unknown default: - break - } - } - - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - switch type { - case .insert: - guard let newIndexPath = newIndexPath else { break } - tableView?.insertRows(at: [newIndexPath], with: .automatic) - case .delete: - guard let indexPath = indexPath else { break } - tableView?.deleteRows(at: [indexPath], with: .automatic) - case .update: - guard let indexPath = indexPath else { break } - tableView?.reloadRows(at: [indexPath], with: .automatic) - case .move: - guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return } - tableView?.moveRow(at: indexPath, to: newIndexPath) - @unknown default: - break - } - } - - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - tableView?.endUpdates() - } - + // there are better ways to handle this + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + tableView?.reloadData() + } + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return nil } } - diff --git a/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents similarity index 66% rename from Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents rename to Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 2f4ba43a..04b20ab1 100644 --- a/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,15 +1,15 @@ - + - - - - - - - - - + + + + + + + + + @@ -18,16 +18,17 @@ - - - - - - - - - - + + + + + + + + + + + @@ -37,6 +38,6 @@ - + \ No newline at end of file diff --git a/Example/Sources/Model/Organization+CloudCoreSharing.swift b/Example/Sources/Model/Organization+CloudCoreSharing.swift new file mode 100644 index 00000000..5dc2522d --- /dev/null +++ b/Example/Sources/Model/Organization+CloudCoreSharing.swift @@ -0,0 +1,47 @@ +// +// Person+CloudKit.swift +// WTSDA +// +// Created by deeje cooley on 12/28/18. +// Copyright © 2018 deeje LLC. All rights reserved. +// + +import CoreData +import CloudKit +import UIKit +import CloudCore + +extension Organization: CloudCoreSharing { + + public var sharingTitle: String? { + return name + } + + public var sharingType: String? { + return "com.deeje.cloudcore.example.organization" + } + + public var sharingImage: Data? { + return nil + } + + /* + public var recordName: String? { + return uuid + } + + public var ownerName: String? { + return ownerUUID + } + + public var shareRecordData: Data? { + get { + return shareData + } + set { + shareData = newValue + } + } + */ + +} diff --git a/Example/Sources/Model/Organization+CoreDataClass.swift b/Example/Sources/Model/Organization+CoreDataClass.swift new file mode 100644 index 00000000..2c793db1 --- /dev/null +++ b/Example/Sources/Model/Organization+CoreDataClass.swift @@ -0,0 +1,16 @@ +// +// Person+CoreDataClass.swift +// +// +// Created by deeje cooley on 10/6/18. +// +// This file was automatically generated and then moved into the project. +// + +import UIKit +import CoreData + +@objc(Organization) +public class Organization: NSManagedObject { + +} diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift index dc0a3052..c8b0f182 100644 --- a/Example/Sources/View Controller/DetailViewController.swift +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -29,15 +29,24 @@ class DetailViewController: UITableViewController { tableView.dataSource = tableDataSource try! tableDataSource.performFetch() - let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add(_:))) - let renameButton = UIBarButtonItem(title: "Rename", style: .plain, target: self, action: #selector(rename(_:))) - navigationItem.setRightBarButtonItems([addButton, renameButton], animated: false) + guard let organization = try? self.context.existingObject(with: self.organizationID) as? CloudCoreSharing else { return } + + var buttons: [UIBarButtonItem] = [] + if organization.isOwnedByCurrentUser { + let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add(_:))) + buttons.append(addButton) + + let renameButton = UIBarButtonItem(title: "Rename", style: .plain, target: self, action: #selector(rename(_:))) + buttons.append(renameButton) + } + let shareButton = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector((share(_:)))) + buttons.append(shareButton) + + navigationItem.setRightBarButtonItems(buttons, animated: false) } @objc private func add(_ sender: UIBarButtonItem) { - persistentContainer.performBackgroundTask { (moc) in - moc.name = CloudCore.config.pushContextName - + persistentContainer.performBackgroundPushTask { (moc) in let employee = ModelFactory.insertEmployee(context: moc) let organization = try? moc.existingObject(with: self.organizationID) as? Organization employee.organization = organization @@ -48,9 +57,7 @@ class DetailViewController: UITableViewController { @objc private func rename(_ sender: UIBarButtonItem) { let newTitle = ModelFactory.newCompanyName() - persistentContainer.performBackgroundTask { (moc) in - moc.name = CloudCore.config.pushContextName - + persistentContainer.performBackgroundPushTask { (moc) in let organization = try? moc.existingObject(with: self.organizationID) as? Organization organization?.name = newTitle @@ -58,6 +65,26 @@ class DetailViewController: UITableViewController { } self.title = newTitle } + + @objc private func share(_ sender: UIBarButtonItem) { + iCloudAvailable { available in + guard available else { return } + + guard let organization = try? self.context.existingObject(with: self.organizationID) as? CloudCoreSharing else { return } + + let sharingController = CloudCoreSharingController(persistentContainer: persistentContainer, + object: organization) + sharingController.configureSharingController(permissions: [.allowReadOnly, .allowPrivate, .allowPublic]) { csc in + DispatchQueue.main.async() { + if let csc = csc { + csc.popoverPresentationController?.barButtonItem = sender + self.present(csc, animated:true, completion:nil) + } + } + } + } + + } } @@ -104,8 +131,7 @@ extension DetailViewController { let anObject = self?.tableDataSource.object(at: indexPath) let objectID = anObject?.objectID - persistentContainer.performBackgroundTask { (moc) in - moc.name = CloudCore.config.pushContextName + persistentContainer.performBackgroundPushTask { (moc) in if let objectToDelete = try? moc.existingObject(with: objectID!) { moc.delete(objectToDelete) try? moc.save() diff --git a/Example/Sources/View Controller/MasterViewController.swift b/Example/Sources/View Controller/MasterViewController.swift index cc15299d..523a2f3e 100644 --- a/Example/Sources/View Controller/MasterViewController.swift +++ b/Example/Sources/View Controller/MasterViewController.swift @@ -32,8 +32,7 @@ class MasterViewController: UITableViewController { } @IBAction func addButtonClicked(_ sender: UIBarButtonItem) { - persistentContainer.performBackgroundTask { (moc) in - moc.name = CloudCore.config.pushContextName + persistentContainer.performBackgroundPushTask { (moc) in ModelFactory.insertOrganizationWithEmployees(context: moc) try! moc.save() } @@ -88,27 +87,72 @@ extension MasterViewController { @available(iOS 11.0, *) override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let deleteTitle = NSLocalizedString("Delete", comment: "Delete action") - let deleteAction = UIContextualAction(style: .destructive, title: deleteTitle, - handler: { [weak self] action, view, completionHandler in - - let anObject = self?.tableDataSource.object(at: indexPath) - let objectID = anObject?.objectID - - persistentContainer.performBackgroundTask { (moc) in - moc.name = CloudCore.config.pushContextName - if let objectToDelete = try? moc.existingObject(with: objectID!) { - moc.delete(objectToDelete) - try? moc.save() - } - } - - completionHandler(true) - }) + var actions: [UIContextualAction] = [] - let configuration = UISwipeActionsConfiguration(actions: [deleteAction]) + let anObject = tableDataSource.object(at: indexPath) as Organization + if anObject.isOwnedByCurrentUser { + let deleteTitle = NSLocalizedString("Delete", comment: "Delete action") + let deleteAction = UIContextualAction(style: .destructive, title: deleteTitle) { [weak self] action, view, completionHandler in + self?.confirmDelete(objectID: anObject.objectID, completion: completionHandler) + } + actions.append(deleteAction) + } else { + let RemoveTitle = NSLocalizedString("Remove", comment: "Remove action") + let removeAction = UIContextualAction(style: .destructive, title: RemoveTitle) { [weak self] action, view, completionHandler in + self?.confirmRemove(objectID: anObject.objectID, completion: completionHandler) + } + actions.append(removeAction) + } + + let configuration = UISwipeActionsConfiguration(actions: actions) return configuration } + + func confirmDelete(objectID: NSManagedObjectID, completion: @escaping ((Bool) -> Void) ) { + let alert = UIAlertController(title: "Are you sure you want to delete?", message: "This will permanently delete this object from all devices", preferredStyle: .alert) + let confirm = UIAlertAction(title: "Delete", style: .destructive) { _ in + persistentContainer.performBackgroundPushTask { moc in + if let personEntity = try? moc.existingObject(with: objectID) { + moc.delete(personEntity) + try? moc.save() + } + completion(true) + } + } + alert.addAction(confirm) + let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in + completion(false) + } + alert.addAction(cancel) + self.present(alert, animated: true) + } + + func confirmRemove(objectID: NSManagedObjectID, completion: @escaping ((Bool) -> Void) ) { + let alert = UIAlertController(title: "Are you sure you want to remove?", message: "This will permanently remove this object from all your devices", preferredStyle: .alert) + let confirm = UIAlertAction(title: "Remove", style: .destructive) { _ in + guard let object = (try? self.context.existingObject(with: objectID)) as? Organization else { return } + + object.stopSharing { didStop in + if didStop { + persistentContainer.performBackgroundTask { moc in + if let deleteObject = try? moc.existingObject(with: objectID) { + moc.delete(deleteObject) + try? moc.save() + } + completion(true) + } + } else { + completion(false) + } + } + } + alert.addAction(confirm) + let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in + completion(false) + } + alert.addAction(cancel) + self.present(alert, animated: true) + } } From 470ab1d0d983589158f97fd5ae321194ec4b3a1e Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 27 May 2021 17:53:58 -0700 Subject: [PATCH 111/203] updated ReadMe to include info about Sharing --- README.md | 85 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 1024f360..6d982ed8 100755 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ * Parent-Child relationships can be defined for CloudKit Sharing * Respects Core Data options (cascade deletions, external storage). * Knows and manages CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. +* Available on iOS, iPadOS, and watchOS (tvOS hasn't been tested) +* Sharing can be extended to your NSManagedObject classes, and native SharingUI is implemented #### CloudCore vs iOS 13? @@ -23,7 +25,7 @@ At WWDC 2019, Apple announced support for NSPersistentCloudKitContainer in iOS 1 ###### NSPersistentCloudKitContainer * Simple to enable -* Private Database only, no Sharing or Public support +* Private or(?) Public Database only, no Sharing * Synchronizes All Records * No CloudKit Metadata (e.g. recordName, systemFields, owner) * Record-level Synchronization (entire objects are pushed) @@ -68,11 +70,11 @@ Current version of framework hasn't been deeply tested and may contain errors. I ## Quick start 1. Enable CloudKit capability for you application: + ![CloudKit capability](https://cloud.githubusercontent.com/assets/5610904/25092841/28305bc0-2398-11e7-9fbf-f94c619c264f.png) 2. For each entity type you want to sync, add this key: value pair to the UserInfo record of the entity: - - * `CloudCoreScopes`: `private` +* `CloudCoreScopes`: `private` 3. Also add 4 attributes to each entity: * `privateRecordData` attribute with `Binary` type @@ -84,7 +86,7 @@ Current version of framework hasn't been deeply tested and may contain errors. I * `privateRecordData` * `publicRecordData` -4. Make changes in your **AppDelegate.swift** file: +5. Make changes in your **AppDelegate.swift** file: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { @@ -107,38 +109,36 @@ func application(_ application: UIApplication, didReceiveRemoteNotification user }) } } - ``` -5. If you want to enable offline support, **enable NSPersistentHistoryTracking** when you initialize your Core Data stack +6. If you want to enable offline support, **enable NSPersistentHistoryTracking** when you initialize your Core Data stack ```swift lazy var persistentContainer: NSPersistentContainer = { - let container = NSPersistentContainer(name: "YourApp") + let container = NSPersistentContainer(name: "YourApp") - let storeDescription = container.persistentStoreDescriptions.first - storeDescription?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) + let storeDescription = container.persistentStoreDescriptions.first + storeDescription?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) - container.loadPersistentStores { storeDescription, error in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - } - } - return container + container.loadPersistentStores { storeDescription, error in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + } + } + return container }() ``` -6. To identify changes from your app that should be pushed, **save** from a background ManagedObjectContexts named `CloudCorePushContext` +7. To identify changes from your app that should be pushed, **save** from a background ManagedObjectContext named `CloudCorePushContext`, or use the convenience function performBackgroundPushTask ```swift -persistentContainer.performBackgroundTask { moc in - moc.name = CloudCore.config.pushContextName - // make changes to objects, properties, and relationships you want pushed via CloudCore - try? context.save() +persistentContainer.performBackgroundPushTask { moc in + // make changes to objects, properties, and relationships you want pushed via CloudCore + try? context.save() } ``` -7. Make first run of your application in a development environment, fill an example data in Core Data and wait until sync completes. CloudKit will create needed schemas automatically. +8. Make first run of your application in a development environment, fill an example data in Core Data and wait until sync completes. CloudKit will create needed schemas automatically. ## Service attributes CloudCore stores CloudKit information inside your managed objects, so you need to add attributes to your Core Data model for that. If required attributes are not found in an entity, that entity won't be synced. @@ -163,17 +163,43 @@ You can map your own attributes to the required service attributes. For each at ![Model editor User Info](https://cloud.githubusercontent.com/assets/5610904/24004400/52e0ff94-0a77-11e7-9dd9-e1e24a86add5.png) +When your *entities have relationships*, CloudCore will look for the following key:value pair in the UserInfo of your entities: + +`CloudCoreParent`: name of the to-one relationship property in your entity + ### 💡 Tips * I recommend to set the *Record Name* attribute as `Indexed`, to speed up updates in big databases. * *Record Data* attributes are used to store archived version of `CKRecord` with system fields only (like timestamps, tokens), so don't worry about size, no real data will be stored here. ## CloudKit Sharing -To enable CloudKit Sharing when your entities have relationships, CloudCore will look for the following key:value pair in the UserInfo of your entities: +CloudCore now has built-in support for CloudKit Sharing. There are several additional steps you must take to enable it in your application. -`CloudCoreParent`: name of the to-one relationship property in your entity +1. Add the CKSharingSupported key, with value true, to your info.plist + +2. Implement the UIApplicationDelegate function userDidAcceptCloudKitShare, something like… + +```swift +func application(_ application: UIApplication, + userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { + let acceptShareOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata]) + acceptShareOperation.qualityOfService = .userInteractive + acceptShareOperation.perShareCompletionBlock = { meta, share, error in + CloudCore.pull(rootRecordID: meta.rootRecordID, container: self.persistentContainer, error: nil) { } + } + acceptShareOperation.acceptSharesCompletionBlock = { error in + // N/A + } + CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation) +} +``` +Note that when a user accepts a share, the app does not receive a remote notification, and so it must specifically pull the shared record in. + +3. Use a CloudCoreSharingController to configure a UICloudSharingController for presentation + +4. When a user wants to delete an object, your app must distinguish between the owner and a sharer, and either delete the object or the share. ## Example application -You can find example application at [Example](/Example/) directory. +You can find example application at [Example](/Example/) directory, which has been updated to demonstrate sharing. **How to run it:** 1. Set Bundle Identifier. @@ -182,11 +208,12 @@ You can find example application at [Example](/Example/) directory. **How to use it:** * **+** button adds new object to local storage (that will be automatically synced to Cloud) -* **refresh** button calls `pull` to fetch data from Cloud. That is useful button for simulators because Simulator unable to receive push notifications +* **Share* button presents the CloudKit Sharing UI +* **refresh** button calls `pull` to fetch data from Cloud. That is only useful for simulators because Simulator unable to receive push notifications * Use [CloudKit dashboard](https://icloud.developer.apple.com/dashboard/) to make changes and see it at application, and make change in application and see ones in dashboard. Don't forget to refresh dashboard's page because it doesn't update data on-the-fly. ## Tests -CloudKit objects can't be mocked up, that's why I create 2 different types of tests: +CloudKit objects can't be mocked up, that's why there are 2 different types of tests: * `Tests/Unit` here I placed tests that can be performed without CloudKit connection. That tests are executed when you submit a Pull Request. * `Tests/CloudKit` here located "manual" tests, they are most important tests that can be run only in configured environment because they work with CloudKit and your Apple ID. @@ -194,7 +221,9 @@ CloudKit objects can't be mocked up, that's why I create 2 different types of te Nothing will be wrong with your account, tests use only private `CKDatabase` for application. **Please run these tests before opening pull requests.** - To run them you need to: + +To run them you need to: + 1. Change `TestableApp` bundle id. 2. Run in simulator or real device `TestableApp` target. 3. Configure iCloud on that device: Settings.app → iCloud → Login. @@ -202,8 +231,6 @@ CloudKit objects can't be mocked up, that's why I create 2 different types of te ## Roadmap -- [ ] Move beta to release status -- [ ] Add `CloudCore.disable` method - [ ] Add methods to clear local cache and remote database - [ ] Add error resolving for `limitExceeded` error (split saves by relationships). From c0e0241983735090ec92dd9d307f889a0f642833 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 4 Jun 2021 16:37:27 -0700 Subject: [PATCH 112/203] document windowScene(userDidAcceptCloudKitShare) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit if you’re using scenes, implement this, otherwise, implement the app delegate version --- README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d982ed8..38d9f506 100755 --- a/README.md +++ b/README.md @@ -176,7 +176,24 @@ CloudCore now has built-in support for CloudKit Sharing. There are several addi 1. Add the CKSharingSupported key, with value true, to your info.plist -2. Implement the UIApplicationDelegate function userDidAcceptCloudKitShare, something like… +2. Implement the appropriate delegate(… userDidAcceptCloudKitShare), something like… + +```swift +func windowScene(_ windowScene: UIWindowScene, + userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { + let acceptShareOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata]) + acceptShareOperation.qualityOfService = .userInteractive + acceptShareOperation.perShareCompletionBlock = { meta, share, error in + CloudCore.pull(rootRecordID: meta.rootRecordID, container: self.persistentContainer, error: nil) { } + } + acceptShareOperation.acceptSharesCompletionBlock = { error in + // N/A + } + CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation) +} +``` + +OR ```swift func application(_ application: UIApplication, @@ -192,7 +209,8 @@ func application(_ application: UIApplication, CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation) } ``` -Note that when a user accepts a share, the app does not receive a remote notification, and so it must specifically pull the shared record in. + +Note that when a user accepts a share, the app does not receive a remote notification of changes from iCloud, and so it must specifically pull the shared record in. 3. Use a CloudCoreSharingController to configure a UICloudSharingController for presentation From 289ebb08f7ee1bab03d868ddfd3662fc8e3486ce Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 17 Jun 2021 17:35:51 -0700 Subject: [PATCH 113/203] make Tokens atomic --- .../Classes/Pull/PullChangesOperation.swift | 48 ++++++++++--------- .../FetchRecordZoneChangesOperation.swift | 4 +- Source/Model/Tokens.swift | 23 ++++++++- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift index dcc59650..8b125ae5 100644 --- a/Source/Classes/Pull/PullChangesOperation.swift +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -41,7 +41,7 @@ public class PullChangesOperation: PullOperation { /// Performs the receiver’s non-concurrent task. override public func main() { - if self.isCancelled { return } + if isCancelled { return } #if TARGET_OS_IOS let app = UIApplication.shared @@ -58,14 +58,15 @@ public class PullChangesOperation: PullOperation { let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.name = CloudCore.config.pullContextName - for database in self.databases { + for database in databases { + let databaseChangeToken = tokens.token(for: database.databaseScope) + if database.databaseScope == .public { let changedRecordIDs: NSMutableSet = [] let deletedRecordIDs: NSMutableSet = [] - let previousToken = self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] - let fetchNotificationChanges = CKFetchNotificationChangesOperation(previousServerChangeToken: previousToken) + let fetchNotificationChanges = CKFetchNotificationChangesOperation(previousServerChangeToken: databaseChangeToken) fetchNotificationChanges.qualityOfService = .userInteractive - fetchNotificationChanges.notificationChangedBlock = { (innerNotification) in + fetchNotificationChanges.notificationChangedBlock = { innerNotification in if let innerQueryNotification = innerNotification as? CKQueryNotification { if innerQueryNotification.queryNotificationReason == .recordDeleted { deletedRecordIDs.add(innerQueryNotification.recordID!) @@ -75,17 +76,17 @@ public class PullChangesOperation: PullOperation { } } } - fetchNotificationChanges.fetchNotificationChangesCompletionBlock = { (changeToken, error) in + fetchNotificationChanges.fetchNotificationChangesCompletionBlock = { changeToken, error in let allChangedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] let fetchRecords = CKFetchRecordsOperation(recordIDs: allChangedRecordIDs) fetchRecords.database = CloudCore.config.container.publicCloudDatabase fetchRecords.qualityOfService = .userInteractive - fetchRecords.perRecordCompletionBlock = { (record, recordID, error) in + fetchRecords.perRecordCompletionBlock = { record, recordID, error in if error == nil { self.addConvertRecordOperation(record: record!, context: backgroundContext) } } - fetchRecords.fetchRecordsCompletionBlock = { (_, error) in + fetchRecords.fetchRecordsCompletionBlock = { _, error in self.processMissingReferences(context: backgroundContext) } let finished = BlockOperation { } @@ -98,27 +99,26 @@ public class PullChangesOperation: PullOperation { self.addDeleteRecordOperation(recordID: recordID, context: backgroundContext) } - self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken + self.tokens.setToken(changeToken, for: database.databaseScope) } let finished = BlockOperation { } finished.addDependency(fetchNotificationChanges) CloudCore.config.container.add(fetchNotificationChanges) - self.queue.addOperation(finished) + queue.addOperation(finished) } else { var changedZoneIDs = [CKRecordZone.ID]() var deletedZoneIDs = [CKRecordZone.ID]() - let databaseChangeToken = tokens.tokensByDatabaseScope[database.databaseScope.rawValue] let fetchDatabaseChanges = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) fetchDatabaseChanges.database = database fetchDatabaseChanges.qualityOfService = .userInteractive - fetchDatabaseChanges.recordZoneWithIDChangedBlock = { (recordZoneID) in + fetchDatabaseChanges.recordZoneWithIDChangedBlock = { recordZoneID in changedZoneIDs.append(recordZoneID) } - fetchDatabaseChanges.recordZoneWithIDWasDeletedBlock = { (recordZoneID) in + fetchDatabaseChanges.recordZoneWithIDWasDeletedBlock = { recordZoneID in deletedZoneIDs.append(recordZoneID) } - fetchDatabaseChanges.fetchDatabaseChangesCompletionBlock = { (changeToken, moreComing, error) in + fetchDatabaseChanges.fetchDatabaseChangesCompletionBlock = { changeToken, moreComing, error in // TODO: error handling? if changedZoneIDs.count > 0 { @@ -128,7 +128,7 @@ public class PullChangesOperation: PullOperation { self.deleteRecordsFromDeletedZones(recordZoneIDs: deletedZoneIDs) } - self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue] = changeToken + self.tokens.setToken(changeToken, for: database.databaseScope) } /* To improve performance overall, and on watchOS in particular @@ -143,11 +143,11 @@ public class PullChangesOperation: PullOperation { let finished = BlockOperation { } finished.addDependency(fetchDatabaseChanges) database.add(fetchDatabaseChanges) - self.queue.addOperation(finished) + queue.addOperation(finished) } } - self.queue.waitUntilAllOperationsAreFinished() + queue.waitUntilAllOperationsAreFinished() backgroundContext.performAndWait { do { @@ -166,7 +166,7 @@ public class PullChangesOperation: PullOperation { // Delete NSManagedObject with specified recordID Operation let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: recordID) deleteOperation.errorBlock = { self.errorBlock?($0) } - self.queue.addOperation(deleteOperation) + queue.addOperation(deleteOperation) } private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZone.ID], database: CKDatabase, context: NSManagedObjectContext) { @@ -194,7 +194,7 @@ public class PullChangesOperation: PullOperation { } private func deleteRecordsFromDeletedZones(recordZoneIDs: [CKRecordZone.ID]) { - persistentContainer.performBackgroundTask { (moc) in + persistentContainer.performBackgroundTask { moc in for entity in self.persistentContainer.managedObjectModel.entities { if let serviceAttributes = entity.serviceAttributeNames { for recordZoneID in recordZoneIDs { @@ -220,7 +220,7 @@ public class PullChangesOperation: PullOperation { } } - private func handle(recordZoneChangesError: Error, in zoneId: CKRecordZone.ID, database: CKDatabase, context: NSManagedObjectContext) { + private func handle(recordZoneChangesError: Error, in zoneID: CKRecordZone.ID, database: CKDatabase, context: NSManagedObjectContext) { guard let cloudError = recordZoneChangesError as? CKError else { errorBlock?(recordZoneChangesError) return @@ -237,9 +237,11 @@ public class PullChangesOperation: PullOperation { // Our token is expired, we need to refetch everything again case .changeTokenExpired: - tokens.tokensByRecordZoneID[zoneId] = nil - self.addRecordZoneChangesOperation(recordZoneIDs: [zoneId], database: database, context: context) - default: errorBlock?(cloudError) + tokens.setToken(nil, for: zoneID) + addRecordZoneChangesOperation(recordZoneIDs: [zoneID], database: database, context: context) + + default: + errorBlock?(cloudError) } } diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index 4a660e06..39ceded2 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -30,7 +30,7 @@ class FetchRecordZoneChangesOperation: Operation { var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneConfiguration]() for zoneID in recordZoneIDs { let options = CKFetchRecordZoneChangesOperation.ZoneConfiguration() - options.previousServerChangeToken = self.tokens.tokensByRecordZoneID[zoneID] + options.previousServerChangeToken = self.tokens.token(for: zoneID) optionsByRecordZoneID[zoneID] = options } self.optionsByRecordZoneID = optionsByRecordZoneID @@ -74,7 +74,7 @@ class FetchRecordZoneChangesOperation: Operation { self.recordWithIDWasDeletedBlock?(recordID) } fetchRecordZoneChanges.recordZoneFetchCompletionBlock = { zoneId, serverChangeToken, clientChangeTokenData, isMore, error in - self.tokens.tokensByRecordZoneID[zoneId] = serverChangeToken + self.tokens.setToken(serverChangeToken, for: zoneId) if let error = error { self.errorBlock?(zoneId, error) diff --git a/Source/Model/Tokens.swift b/Source/Model/Tokens.swift index e47f53b1..b172876b 100644 --- a/Source/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -19,14 +19,16 @@ import CloudKit open class Tokens: NSObject, NSSecureCoding { - var tokensByDatabaseScope = [Int: CKServerChangeToken]() - var tokensByRecordZoneID = [CKRecordZone.ID: CKServerChangeToken]() + private var tokensByDatabaseScope = [Int: CKServerChangeToken]() + private var tokensByRecordZoneID = [CKRecordZone.ID: CKServerChangeToken]() private struct ArchiverKey { static let tokensByDatabaseScope = "tokensByDatabaseScope" static let tokensByRecordZoneID = "tokensByRecordZoneID" } + private let queue = DispatchQueue(label: "com.deeje.CloudCore.Tokens") + public static var supportsSecureCoding: Bool { return true } @@ -45,6 +47,7 @@ open class Tokens: NSObject, NSSecureCoding { if let tokensData = UserDefaults.standard.data(forKey: CloudCore.config.userDefaultsKeyTokens) { do { let allowableClasses = [Tokens.classForKeyedUnarchiver(), + NSNumber.classForKeyedUnarchiver(), NSDictionary.classForKeyedUnarchiver(), CKRecordZone.ID.classForKeyedUnarchiver(), CKServerChangeToken.classForKeyedUnarchiver()] @@ -85,4 +88,20 @@ open class Tokens: NSObject, NSSecureCoding { aCoder.encode(tokensByRecordZoneID, forKey: ArchiverKey.tokensByRecordZoneID) } + func token(for scope: CKDatabase.Scope) -> CKServerChangeToken? { + return queue.sync { tokensByDatabaseScope[scope.rawValue] } + } + + func setToken(_ newToken: CKServerChangeToken?, for scope: CKDatabase.Scope) { + queue.sync { tokensByDatabaseScope[scope.rawValue] = newToken } + } + + func token(for zone: CKRecordZone.ID) -> CKServerChangeToken? { + return queue.sync { tokensByRecordZoneID[zone] } + } + + func setToken(_ newToken: CKServerChangeToken?, for zone: CKRecordZone.ID) { + queue.sync { tokensByRecordZoneID[zone] = newToken } + } + } From a533cf231c45fdbf2fa5c6f8cf6e9fca393f9298 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 17 Jun 2021 17:36:11 -0700 Subject: [PATCH 114/203] dont hide potential bugs behind try? --- Source/Classes/Push/CoreDataObserver.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index a151ec1e..f52baec2 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -231,12 +231,12 @@ class CoreDataObserver { container.performBackgroundTask { (moc) in let settings = UserDefaults.standard - var token: NSPersistentHistoryToken? = nil - if let data = settings.object(forKey: CloudCore.config.persistentHistoryTokenKey) as? Data { - token = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken - } - let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) do { + var token: NSPersistentHistoryToken? = nil + if let data = settings.object(forKey: CloudCore.config.persistentHistoryTokenKey) as? Data { + token = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken + } + let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult if let history = historyResult.result as? [NSPersistentHistoryTransaction] { @@ -245,9 +245,8 @@ class CoreDataObserver { let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) try moc.execute(deleteRequest) - if let data = try? NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) { - settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) - } + let data = try NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) + settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) } else { break } From e3e6bb1eb57046f9110c00080c9efe67e3c124ae Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 18 Jun 2021 14:19:48 -0700 Subject: [PATCH 115/203] =?UTF-8?q?don=E2=80=99t=20start=20uploading=20unt?= =?UTF-8?q?il=20zone=20is=20created?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/Classes/Push/CoreDataObserver.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index f52baec2..d9760421 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -293,7 +293,8 @@ class CoreDataObserver { // Upload all local data let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } - + uploadOperation.addDependency(createZoneOperation) + pushOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) case .operationCancelled: return default: delegate?.error(error: cloudError, module: .some(.pushToCloud)) From 1a0cbb7a6b9b3404cf2260bf5dcc70afc462c458 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 18 Jun 2021 14:20:52 -0700 Subject: [PATCH 116/203] (remove unnecessary () from async call site ) --- Source/Extensions/UIViewController+CloudKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Extensions/UIViewController+CloudKit.swift b/Source/Extensions/UIViewController+CloudKit.swift index a3bc2670..c92b359d 100644 --- a/Source/Extensions/UIViewController+CloudKit.swift +++ b/Source/Extensions/UIViewController+CloudKit.swift @@ -14,7 +14,7 @@ extension UIViewController { public func iCloudAvailable(completion: @escaping ((Bool) -> Void)) { CloudCore.config.container.accountStatus { accountStatus, error in - DispatchQueue.main.async() { + DispatchQueue.main.async { var title: String? var message: String? From b2927cfea6280d46abfab609a86b0475c676fe7f Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 18 Jun 2021 14:29:44 -0700 Subject: [PATCH 117/203] add PullChangesOp directly to queue --- Source/Classes/CloudCore.swift | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index f1748eaa..9098e9d9 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -150,18 +150,14 @@ open class CloudCore { return } - DispatchQueue.global(qos: .utility).async { - let errorProxy = ErrorBlockProxy(destination: error) - let operation = PullChangesOperation(from: [cloudDatabase], persistentContainer: container) - operation.errorBlock = { errorProxy.send(error: $0) } - operation.start() - - if errorProxy.wasError { - completion(PullResult.failed) - } else { - completion(PullResult.newData) - } - } + let errorProxy = ErrorBlockProxy(destination: error) + let pullChangesOp = PullChangesOperation(from: [cloudDatabase], persistentContainer: container) + pullChangesOp.errorBlock = { errorProxy.send(error: $0) } + pullChangesOp.completionBlock = { + completion(errorProxy.wasError ? PullResult.failed : PullResult.newData) + } + + queue.addOperation(pullChangesOp) } /** Fetch changes from all CloudKit databases and save it to Core Data From 532ab0241898406c2c2ab59da9f57aca09eda0b2 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 18 Jun 2021 14:30:28 -0700 Subject: [PATCH 118/203] save context on completing recordZoneChangesOp --- Source/Classes/Pull/PullChangesOperation.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift index 8b125ae5..1f190b93 100644 --- a/Source/Classes/Pull/PullChangesOperation.swift +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -188,6 +188,14 @@ public class PullChangesOperation: PullOperation { recordZoneChangesOperation.completionBlock = { self.processMissingReferences(context: context) + + context.performAndWait { + do { + try context.save() + } catch { + self.errorBlock?(error) + } + } } queue.addOperation(recordZoneChangesOperation) From b9b776e63b48bfb857065a2c04477797850df184 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 18 Jun 2021 14:40:22 -0700 Subject: [PATCH 119/203] eliminate ErrorBlocvkProxy --- Source/Classes/CloudCore.swift | 9 ++++++--- Source/Classes/ErrorBlockProxy.swift | 26 -------------------------- 2 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 Source/Classes/ErrorBlockProxy.swift diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 9098e9d9..f382a887 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -150,11 +150,14 @@ open class CloudCore { return } - let errorProxy = ErrorBlockProxy(destination: error) + var hadError = false let pullChangesOp = PullChangesOperation(from: [cloudDatabase], persistentContainer: container) - pullChangesOp.errorBlock = { errorProxy.send(error: $0) } + pullChangesOp.errorBlock = { + hadError = true + error?($0) + } pullChangesOp.completionBlock = { - completion(errorProxy.wasError ? PullResult.failed : PullResult.newData) + completion(hadError ? PullResult.failed : PullResult.newData) } queue.addOperation(pullChangesOp) diff --git a/Source/Classes/ErrorBlockProxy.swift b/Source/Classes/ErrorBlockProxy.swift deleted file mode 100644 index 454233cd..00000000 --- a/Source/Classes/ErrorBlockProxy.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ErrorBlockProxy.swift -// CloudCore -// -// Created by Vasily Ulianov on 12.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import Foundation - -/// Use that class to log if any errors were sent -class ErrorBlockProxy { - private(set) var wasError = false - var destination: ErrorBlock? - - init(destination: ErrorBlock?) { - self.destination = destination - } - - func send(error: Error?) { - if let error = error { - self.wasError = true - destination?(error) - } - } -} From ab505a90108d3bb92e0599ea56e376c12b8b3ae7 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 26 Jun 2021 17:00:58 -0700 Subject: [PATCH 120/203] use the context in processMissingReferences --- Source/Classes/Pull/PullOperation.swift | 54 +++++++++++++------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 7b93be3a..56810d04 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -42,36 +42,38 @@ public class PullOperation: Operation { internal func processMissingReferences(context: NSManagedObjectContext) { // iterate over all missing references and fix them, now are all NSManagedObjects created - for missingReferences in objectsWithMissingReferences { - for (object, references) in missingReferences { - guard let serviceAttributes = object.entity.serviceAttributeNames else { continue } - - for (attributeName, recordNames) in references { - for recordName in recordNames { - guard let relationship = object.entity.relationshipsByName[attributeName], let targetEntityName = relationship.destinationEntity?.name else { continue } - - // TODO: move to extension - let fetchRequest = NSFetchRequest(entityName: targetEntityName) - fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordName + " == %@" , recordName) - fetchRequest.fetchLimit = 1 - fetchRequest.includesPropertyValues = false - - do { - let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject + context.performAndWait { + for missingReferences in objectsWithMissingReferences { + for (object, references) in missingReferences { + guard let serviceAttributes = object.entity.serviceAttributeNames else { continue } + + for (attributeName, recordNames) in references { + for recordName in recordNames { + guard let relationship = object.entity.relationshipsByName[attributeName], let targetEntityName = relationship.destinationEntity?.name else { continue } - if let foundObject = foundObject { - if relationship.isToMany { - let set = object.value(forKey: attributeName) as? NSMutableSet ?? NSMutableSet() - set.add(foundObject) - object.setValue(set, forKey: attributeName) + // TODO: move to extension + let fetchRequest = NSFetchRequest(entityName: targetEntityName) + fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordName + " == %@" , recordName) + fetchRequest.fetchLimit = 1 + fetchRequest.includesPropertyValues = false + + do { + let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject + + if let foundObject = foundObject { + if relationship.isToMany { + let set = object.value(forKey: attributeName) as? NSMutableSet ?? NSMutableSet() + set.add(foundObject) + object.setValue(set, forKey: attributeName) + } else { + object.setValue(foundObject, forKey: attributeName) + } } else { - object.setValue(foundObject, forKey: attributeName) + print("warning: object not found " + recordName) } - } else { - print("warning: object not found " + recordName) + } catch { + self.errorBlock?(error) } - } catch { - self.errorBlock?(error) } } } From ec915550b613b428236f9b45ccef5ad7a248bf25 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 5 Jul 2021 15:34:40 -0700 Subject: [PATCH 121/203] add handlers for when share is saved or stopped --- Source/Classes/Sharing/CloudCoreSharingController.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Classes/Sharing/CloudCoreSharingController.swift b/Source/Classes/Sharing/CloudCoreSharingController.swift index 7168327e..9ca100d0 100644 --- a/Source/Classes/Sharing/CloudCoreSharingController.swift +++ b/Source/Classes/Sharing/CloudCoreSharingController.swift @@ -18,6 +18,9 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg let persistentContainer: NSPersistentContainer let object: CloudCoreSharing + public var didSaveShare: ((CKShare)->Void)? = nil + public var didStopSharing: (()->Void)? = nil + public init(persistentContainer: NSPersistentContainer, object: CloudCoreSharing) { self.persistentContainer = persistentContainer self.object = object @@ -111,6 +114,10 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg return object.sharingType } + public func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) { + didSaveShare?(csc.share!) + } + public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { persistentContainer.performBackgroundTask { moc in if let updatedObject = try? moc.existingObject(with: self.object.objectID) as? CloudCoreSharing { @@ -123,6 +130,7 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg try? moc.save() } } + didStopSharing?() } } From 8fe5d27e9463dc1ae4939b1c307c7bfac039b6c8 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 6 Jul 2021 13:35:42 -0700 Subject: [PATCH 122/203] DRY when fetching the share --- Source/Classes/Sharing/CloudCoreSharing.swift | 104 ++++++++++-------- .../Sharing/CloudCoreSharingController.swift | 77 ++++--------- 2 files changed, 80 insertions(+), 101 deletions(-) diff --git a/Source/Classes/Sharing/CloudCoreSharing.swift b/Source/Classes/Sharing/CloudCoreSharing.swift index fcc3700f..0c736c83 100644 --- a/Source/Classes/Sharing/CloudCoreSharing.swift +++ b/Source/Classes/Sharing/CloudCoreSharing.swift @@ -14,14 +14,15 @@ public typealias StopSharingCompletionBlock = (_ didStop: Bool) -> Void public protocol CloudCoreSharing: CloudKitSharing, CloudCoreType { var isOwnedByCurrentUser: Bool { get } + var isShared: Bool { get } var shareRecordData: Data? { get set } func fetchExistingShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) - func fetchShareRecord(in persistentContainer: NSPersistentContainer, completion: @escaping ((CKShare?, Error?) -> Void)) + func fetchShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) func fetchEditablePermissions(completion: @escaping FetchedEditablePermissionsCompletionBlock) func setShare(data: Data?, in persistentContainer: NSPersistentContainer) - func stopSharing(completion: @escaping StopSharingCompletionBlock) - + func stopSharing(in persistentContainer: NSPersistentContainer, completion: @escaping StopSharingCompletionBlock) + } extension CloudCoreSharing { @@ -32,20 +33,30 @@ extension CloudCoreSharing { } } + public var isShared: Bool { + get { + return shareRecordData != nil + } + } + public func fetchExistingShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) { if let shareData = shareRecordData { - let aShare = CKShare(archivedData: shareData)! + let shareForName = CKShare(archivedData: shareData)! + let database: CKDatabase + let shareID: CKRecord.ID - var database = CloudCore.config.container.sharedCloudDatabase - var ownerUUID = ownerName! - if ownerUUID == CKCurrentUserDefaultName { + if isOwnedByCurrentUser { database = CloudCore.config.container.privateCloudDatabase - ownerUUID = CloudCore.userRecordName()! + + shareID = shareForName.recordID + } else { + database = CloudCore.config.container.sharedCloudDatabase + + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerName!) + shareID = CKRecord.ID(recordName: shareForName.recordID.recordName, zoneID: zoneID) } - let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerUUID) - let shareID = CKRecord.ID(recordName: aShare.recordID.recordName, zoneID: zoneID) - database.fetch(withRecordID: shareID) { (record, error) in + database.fetch(withRecordID: shareID) { record, error in completion(record as? CKShare, error) } } else { @@ -53,34 +64,20 @@ extension CloudCoreSharing { } } - public func fetchShareRecord(in persistentContainer: NSPersistentContainer, completion: @escaping ((CKShare?, Error?) -> Void)) { + public func fetchShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) { + let aRecord = try! self.restoreRecordWithSystemFields(for: .private)! + let title = sharingTitle as CKRecordValue? + let type = sharingType as CKRecordValue? + fetchExistingShareRecord { share, error in - var createIt = false - if share == nil && error == nil { - createIt = true - } - if let ckError = error as? CKError, - ckError.code == .unknownItem { - createIt = true - } - if createIt, let aRecord = try? self.restoreRecordWithSystemFields(for: .private) { + if let share = share { + completion(share, nil) + } else { let newShare = CKShare(rootRecord: aRecord) - newShare[CKShare.SystemFieldKey.title] = self.sharingTitle as CKRecordValue? - newShare[CKShare.SystemFieldKey.shareType] = self.sharingType as CKRecordValue? + newShare[CKShare.SystemFieldKey.title] = title + newShare[CKShare.SystemFieldKey.shareType] = type - let modOp: CKModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [newShare, aRecord], recordIDsToDelete: nil) - modOp.savePolicy = .ifServerRecordUnchanged - modOp.modifyRecordsCompletionBlock = {records, recordIDs, error in - let savedShare = records?.first as? CKShare - if let savedShare = savedShare { - let shareData = savedShare.encdodedSystemFields - self.setShare(data: shareData, in: persistentContainer) - } - completion(savedShare, error) - } - CloudCore.config.container.privateCloudDatabase.add(modOp) - } else { - completion(share, error) + completion(newShare, nil) } } } @@ -117,22 +114,33 @@ extension CloudCoreSharing { } } - public func stopSharing(completion: @escaping StopSharingCompletionBlock) { - if isOwnedByCurrentUser { - completion(false) - - return - } - + public func stopSharing(in persistentContainer: NSPersistentContainer, completion: @escaping StopSharingCompletionBlock) { if let shareData = shareRecordData { - let sharedDB = CloudCore.config.container.sharedCloudDatabase + var database = CloudCore.config.container.sharedCloudDatabase + var ownerUUID = ownerName! + if ownerUUID == CKCurrentUserDefaultName { + database = CloudCore.config.container.privateCloudDatabase + ownerUUID = CloudCore.userRecordName()! + } + + let shareForName = CKShare(archivedData: shareData)! + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerUUID) + let shareID = CKRecord.ID(recordName: shareForName.recordID.recordName, zoneID: zoneID) - let aShare = CKShare(archivedData: shareData)! - let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerName!) - let shareID = CKRecord.ID(recordName: aShare.recordID.recordName, zoneID: zoneID) - sharedDB.delete(withRecordID: shareID) { recordID, error in + database.delete(withRecordID: shareID) { recordID, error in completion(error == nil) } + + if isOwnedByCurrentUser { + persistentContainer.performBackgroundPushTask { moc in + if let updatedObject = try? moc.existingObject(with: self.objectID) as? CloudCoreSharing { + updatedObject.shareRecordData = nil + try? moc.save() + } + } + } + } else { + completion(true) } } diff --git a/Source/Classes/Sharing/CloudCoreSharingController.swift b/Source/Classes/Sharing/CloudCoreSharingController.swift index 9ca100d0..886235ac 100644 --- a/Source/Classes/Sharing/CloudCoreSharingController.swift +++ b/Source/Classes/Sharing/CloudCoreSharingController.swift @@ -35,65 +35,36 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg completion(sharingController) } - if let aRecord = try? object.restoreRecordWithSystemFields(for: .private) { - let aShare: CKShare - if let shareData = object.shareRecordData { - aShare = CKShare(archivedData: shareData)! - if object.isOwnedByCurrentUser { - CloudCore.config.container.privateCloudDatabase.fetch(withRecordID: aShare.recordID) { (record, error) in - if let fetchedShare = record as? CKShare { - DispatchQueue.main.async { - let sharingController = UICloudSharingController(share: fetchedShare, container: CloudCore.config.container) - commonConfigure(sharingController) - } - } else if let ckError = error as? CKError { - switch ckError.code { - case .unknownItem: - self.object.setShare(data: nil, in: self.persistentContainer) - default: - break - } - completion(nil) - } - } + guard let aRecord = try! object.restoreRecordWithSystemFields(for: .private) else { completion(nil); return } + + object.fetchShareRecord { share, error in + guard error == nil, let share = share else { completion(nil); return } + + DispatchQueue.main.async { + if share.participants.count > 1 { + let sharingController = UICloudSharingController(share: share, container: CloudCore.config.container) + commonConfigure(sharingController) } else { - let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: object.ownerName!) - let shareID = CKRecord.ID(recordName: aShare.recordID.recordName, zoneID: zoneID) - CloudCore.config.container.sharedCloudDatabase.fetch(withRecordID: shareID) { (record, error) in - if let fetchedShare = record as? CKShare { - DispatchQueue.main.async { - let sharingController = UICloudSharingController(share: fetchedShare, container: CloudCore.config.container) - commonConfigure(sharingController) + let sharingController = UICloudSharingController { _, handler in + let modifyOp = CKModifyRecordsOperation(recordsToSave: [aRecord, share], recordIDsToDelete: nil) + modifyOp.savePolicy = .changedKeys + modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in + if let share = records?.first as? CKShare { + DispatchQueue.main.async { + self.object.setShare(data: share.encdodedSystemFields, in: self.persistentContainer) + } + + handler(share, CloudCore.config.container, error) + } else { + handler(nil, nil, error) } - } else { - completion(nil) } + modifyOp.savePolicy = .changedKeys + CloudCore.config.container.privateCloudDatabase.add(modifyOp) } - } - } else { - aShare = CKShare(rootRecord: aRecord) - aShare[CKShare.SystemFieldKey.title] = object.sharingTitle as CKRecordValue? - aShare[CKShare.SystemFieldKey.shareType] = object.sharingType as CKRecordValue? - - let sharingController = UICloudSharingController { (_, handler: - @escaping (CKShare?, CKContainer?, Error?) -> Void) in - let modifyOp = CKModifyRecordsOperation(recordsToSave: [aRecord, aShare], recordIDsToDelete: nil) - modifyOp.savePolicy = .changedKeys - modifyOp.modifyRecordsCompletionBlock = { (records, recordIDs, error) in - if let share = records?.first as? CKShare { - self.object.setShare(data: aShare.encdodedSystemFields, in: self.persistentContainer) - - handler(share, CloudCore.config.container, error) - } else { - handler(nil, nil, error) - } - } - modifyOp.savePolicy = .changedKeys - CloudCore.config.container.privateCloudDatabase.add(modifyOp) + commonConfigure(sharingController) } - - commonConfigure(sharingController) } } } From db60ccbe426aad63e656224cae363d12fa1c5032 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 6 Jul 2021 14:19:28 -0700 Subject: [PATCH 123/203] =?UTF-8?q?don=E2=80=99t=20try=20to=20upate=20obje?= =?UTF-8?q?ct=20when=20sharing=20stops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Classes/Sharing/CloudCoreSharingController.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Source/Classes/Sharing/CloudCoreSharingController.swift b/Source/Classes/Sharing/CloudCoreSharingController.swift index 886235ac..23b18cb3 100644 --- a/Source/Classes/Sharing/CloudCoreSharingController.swift +++ b/Source/Classes/Sharing/CloudCoreSharingController.swift @@ -90,17 +90,6 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg } public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { - persistentContainer.performBackgroundTask { moc in - if let updatedObject = try? moc.existingObject(with: self.object.objectID) as? CloudCoreSharing { - if updatedObject.isOwnedByCurrentUser { - moc.name = CloudCore.config.pushContextName - updatedObject.shareRecordData = nil - } else { - moc.delete(updatedObject) - } - try? moc.save() - } - } didStopSharing?() } From cc0283d8ad45917c10c819c387ff4d416395e7b0 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 6 Jul 2021 14:20:10 -0700 Subject: [PATCH 124/203] update example when displaying sharing UI --- .../View Controller/DetailViewController.swift | 17 +++++++++-------- .../View Controller/MasterViewController.swift | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift index c8b0f182..53082eb4 100644 --- a/Example/Sources/View Controller/DetailViewController.swift +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -17,6 +17,8 @@ class DetailViewController: UITableViewController { private var tableDataSource: DetailTableDataSource! + private var sharingController: CloudCoreSharingController! + override func viewDidLoad() { super.viewDidLoad() @@ -72,18 +74,17 @@ class DetailViewController: UITableViewController { guard let organization = try? self.context.existingObject(with: self.organizationID) as? CloudCoreSharing else { return } - let sharingController = CloudCoreSharingController(persistentContainer: persistentContainer, + if self.sharingController == nil { + self.sharingController = CloudCoreSharingController(persistentContainer: persistentContainer, object: organization) - sharingController.configureSharingController(permissions: [.allowReadOnly, .allowPrivate, .allowPublic]) { csc in - DispatchQueue.main.async() { - if let csc = csc { - csc.popoverPresentationController?.barButtonItem = sender - self.present(csc, animated:true, completion:nil) - } + } + self.sharingController.configureSharingController(permissions: [.allowReadOnly, .allowPrivate, .allowPublic]) { csc in + if let csc = csc { + csc.popoverPresentationController?.barButtonItem = sender + self.present(csc, animated:true, completion:nil) } } } - } } diff --git a/Example/Sources/View Controller/MasterViewController.swift b/Example/Sources/View Controller/MasterViewController.swift index 523a2f3e..78500c75 100644 --- a/Example/Sources/View Controller/MasterViewController.swift +++ b/Example/Sources/View Controller/MasterViewController.swift @@ -132,7 +132,7 @@ extension MasterViewController { let confirm = UIAlertAction(title: "Remove", style: .destructive) { _ in guard let object = (try? self.context.existingObject(with: objectID)) as? Organization else { return } - object.stopSharing { didStop in + object.stopSharing(in: persistentContainer) { didStop in if didStop { persistentContainer.performBackgroundTask { moc in if let deleteObject = try? moc.existingObject(with: objectID) { From c649c7b744720539f73cb9245e9d8355f06a51d5 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 6 Jul 2021 14:40:46 -0700 Subject: [PATCH 125/203] DRY, call shareDatabaseAndRecordID instead --- Source/Classes/Sharing/CloudCoreSharing.swift | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/Source/Classes/Sharing/CloudCoreSharing.swift b/Source/Classes/Sharing/CloudCoreSharing.swift index 0c736c83..ba3e6af9 100644 --- a/Source/Classes/Sharing/CloudCoreSharing.swift +++ b/Source/Classes/Sharing/CloudCoreSharing.swift @@ -39,22 +39,28 @@ extension CloudCoreSharing { } } + func shareDatabaseAndRecordID(from shareData: Data) -> (CKDatabase, CKRecord.ID) { + let shareForName = CKShare(archivedData: shareData)! + let database: CKDatabase + let shareID: CKRecord.ID + + if isOwnedByCurrentUser { + database = CloudCore.config.container.privateCloudDatabase + + shareID = shareForName.recordID + } else { + database = CloudCore.config.container.sharedCloudDatabase + + let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerName!) + shareID = CKRecord.ID(recordName: shareForName.recordID.recordName, zoneID: zoneID) + } + + return (database, shareID) + } + public func fetchExistingShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) { if let shareData = shareRecordData { - let shareForName = CKShare(archivedData: shareData)! - let database: CKDatabase - let shareID: CKRecord.ID - - if isOwnedByCurrentUser { - database = CloudCore.config.container.privateCloudDatabase - - shareID = shareForName.recordID - } else { - database = CloudCore.config.container.sharedCloudDatabase - - let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerName!) - shareID = CKRecord.ID(recordName: shareForName.recordID.recordName, zoneID: zoneID) - } + let (database, shareID) = shareDatabaseAndRecordID(from: shareData) database.fetch(withRecordID: shareID) { record, error in completion(record as? CKShare, error) @@ -116,16 +122,7 @@ extension CloudCoreSharing { public func stopSharing(in persistentContainer: NSPersistentContainer, completion: @escaping StopSharingCompletionBlock) { if let shareData = shareRecordData { - var database = CloudCore.config.container.sharedCloudDatabase - var ownerUUID = ownerName! - if ownerUUID == CKCurrentUserDefaultName { - database = CloudCore.config.container.privateCloudDatabase - ownerUUID = CloudCore.userRecordName()! - } - - let shareForName = CKShare(archivedData: shareData)! - let zoneID = CKRecordZone.ID(zoneName: CloudCore.config.zoneName, ownerName: ownerUUID) - let shareID = CKRecord.ID(recordName: shareForName.recordID.recordName, zoneID: zoneID) + let (database, shareID) = shareDatabaseAndRecordID(from: shareData) database.delete(withRecordID: shareID) { recordID, error in completion(error == nil) From 21859e851430418e27ed3d943fae73856f4dc4ae Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 7 Jul 2021 13:01:08 -0700 Subject: [PATCH 126/203] call setShareRecore on start/stop sharing --- Source/Classes/Sharing/CloudCoreSharing.swift | 15 +++++------ .../Sharing/CloudCoreSharingController.swift | 25 +++++++++++-------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Source/Classes/Sharing/CloudCoreSharing.swift b/Source/Classes/Sharing/CloudCoreSharing.swift index ba3e6af9..c28a9db0 100644 --- a/Source/Classes/Sharing/CloudCoreSharing.swift +++ b/Source/Classes/Sharing/CloudCoreSharing.swift @@ -20,7 +20,7 @@ public protocol CloudCoreSharing: CloudKitSharing, CloudCoreType { func fetchExistingShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) func fetchShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) func fetchEditablePermissions(completion: @escaping FetchedEditablePermissionsCompletionBlock) - func setShare(data: Data?, in persistentContainer: NSPersistentContainer) + func setShareRecord(share: CKShare?, in persistentContainer: NSPersistentContainer) func stopSharing(in persistentContainer: NSPersistentContainer, completion: @escaping StopSharingCompletionBlock) } @@ -59,6 +59,8 @@ extension CloudCoreSharing { } public func fetchExistingShareRecord(completion: @escaping ((CKShare?, Error?) -> Void)) { + managedObjectContext?.refresh(self, mergeChanges: true) + if let shareData = shareRecordData { let (database, shareID) = shareDatabaseAndRecordID(from: shareData) @@ -111,10 +113,10 @@ extension CloudCoreSharing { } } - public func setShare(data: Data?, in persistentContainer: NSPersistentContainer) { + public func setShareRecord(share: CKShare?, in persistentContainer: NSPersistentContainer) { persistentContainer.performBackgroundPushTask { moc in if let updatedObject = try? moc.existingObject(with: self.objectID) as? CloudCoreSharing { - updatedObject.shareRecordData = data + updatedObject.shareRecordData = share?.encdodedSystemFields try? moc.save() } } @@ -129,12 +131,7 @@ extension CloudCoreSharing { } if isOwnedByCurrentUser { - persistentContainer.performBackgroundPushTask { moc in - if let updatedObject = try? moc.existingObject(with: self.objectID) as? CloudCoreSharing { - updatedObject.shareRecordData = nil - try? moc.save() - } - } + setShareRecord(share: nil, in: persistentContainer) } } else { completion(true) diff --git a/Source/Classes/Sharing/CloudCoreSharingController.swift b/Source/Classes/Sharing/CloudCoreSharingController.swift index 23b18cb3..a5dc0072 100644 --- a/Source/Classes/Sharing/CloudCoreSharingController.swift +++ b/Source/Classes/Sharing/CloudCoreSharingController.swift @@ -18,8 +18,9 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg let persistentContainer: NSPersistentContainer let object: CloudCoreSharing - public var didSaveShare: ((CKShare)->Void)? = nil - public var didStopSharing: (()->Void)? = nil + public var didSaveShare: ((CKShare)->Void)? + public var didStopSharing: (()->Void)? + public var didError: ((Error)->Void)? public init(persistentContainer: NSPersistentContainer, object: CloudCoreSharing) { self.persistentContainer = persistentContainer @@ -50,10 +51,6 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg modifyOp.savePolicy = .changedKeys modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in if let share = records?.first as? CKShare { - DispatchQueue.main.async { - self.object.setShare(data: share.encdodedSystemFields, in: self.persistentContainer) - } - handler(share, CloudCore.config.container, error) } else { handler(nil, nil, error) @@ -69,10 +66,6 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg } } - public func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) { -// os_log(.debug, "failed to save share") - } - public func itemTitle(for csc: UICloudSharingController) -> String? { return object.sharingTitle } @@ -86,13 +79,25 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg } public func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) { + if object.isOwnedByCurrentUser && object.shareRecordData == nil { + object.setShareRecord(share: csc.share, in: persistentContainer) + } + didSaveShare?(csc.share!) } public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { + if object.isOwnedByCurrentUser { + object.setShareRecord(share: nil, in: persistentContainer) + } + didStopSharing?() } + public func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) { + didError?(error) + } + } #endif From bd70c25df995490f6b43dffa850df26d2a011f68 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 14 Jul 2021 16:01:30 -0700 Subject: [PATCH 127/203] =?UTF-8?q?UIViewController.=20iCloudAvailable(wit?= =?UTF-8?q?hPrompt=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sometimes you only want to show an alert once per launch or once per install or not at all --- Source/Extensions/UIViewController+CloudKit.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/Extensions/UIViewController+CloudKit.swift b/Source/Extensions/UIViewController+CloudKit.swift index c92b359d..825bf63b 100644 --- a/Source/Extensions/UIViewController+CloudKit.swift +++ b/Source/Extensions/UIViewController+CloudKit.swift @@ -12,19 +12,21 @@ import CloudKit extension UIViewController { - public func iCloudAvailable(completion: @escaping ((Bool) -> Void)) { + public func iCloudAvailable(withPrompt: Bool = true, completion: @escaping ((Bool) -> Void)) { CloudCore.config.container.accountStatus { accountStatus, error in DispatchQueue.main.async { + var available = false + var title: String? var message: String? switch accountStatus { case .noAccount: title = "Sign in to iCloud and\nenable iCloud Drive" - message = "On the Home screen, launch Settings, tap iCloud, and enter your Apple ID. Turn iCloud Drive on. If you don't have an iCloud account, tap Create a new Apple ID." + message = "Go to Settings and sign into your iPhone. Under iCloud, enable iCloud Drive." case .available: - completion(true) + available = true case .couldNotDetermine: title = "iCloud Unavailable" @@ -38,12 +40,14 @@ extension UIViewController { break } - if let title = title, let message = message { + if withPrompt, let title = title, let message = message { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in })) self.present(alert, animated: true) { - completion(false) + completion(available) } + } else { + completion(available) } } } From 214de8abc77489efd01daced16b3dcc15ff7822c Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 15 Jul 2021 10:21:50 -0700 Subject: [PATCH 128/203] when the zone is first created, do an initial pull this also has the effect of calling the delegate as expected --- Source/Classes/CloudCore.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index f382a887..80c38f17 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -237,8 +237,17 @@ open class CloudCore { // Zone wasn't found, we need to create it self.queue.cancelAllOperations() let setupOperation = SetupOperation(container: container, uploadAllData: !(coreDataObserver?.usePersistentHistoryForPush)!) - self.queue.addOperation(setupOperation) + // for completeness, pull again + let pullOperation = PullChangesOperation(persistentContainer: container) + pullOperation.errorBlock = { + self.delegate?.error(error: $0, module: .some(.pullFromCloud)) + } + pullOperation.addDependency(setupOperation) + + self.queue.addOperation(setupOperation) + self.queue.addOperation(pullOperation) + return } } From d7ba6ba7239cb41a3451187af6ec4089ba03c4e1 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 23 Jul 2021 12:27:16 -0700 Subject: [PATCH 129/203] (remove an duplicate line of code) --- Source/Classes/Sharing/CloudCoreSharingController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Classes/Sharing/CloudCoreSharingController.swift b/Source/Classes/Sharing/CloudCoreSharingController.swift index a5dc0072..caa3f81d 100644 --- a/Source/Classes/Sharing/CloudCoreSharingController.swift +++ b/Source/Classes/Sharing/CloudCoreSharingController.swift @@ -56,7 +56,6 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg handler(nil, nil, error) } } - modifyOp.savePolicy = .changedKeys CloudCore.config.container.privateCloudDatabase.add(modifyOp) } From b8fbf6257996e2924e76d8c5e68c63ec310cb102 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 23 Jul 2021 12:28:23 -0700 Subject: [PATCH 130/203] backgroundPushTask is always merge PropertyObjectTrump --- Source/Extensions/NSManagedContainer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Extensions/NSManagedContainer.swift b/Source/Extensions/NSManagedContainer.swift index a78e4629..cff87cfe 100644 --- a/Source/Extensions/NSManagedContainer.swift +++ b/Source/Extensions/NSManagedContainer.swift @@ -12,6 +12,7 @@ extension NSPersistentContainer { public func performBackgroundPushTask(_ block: @escaping (NSManagedObjectContext) -> Void) { performBackgroundTask { moc in moc.name = CloudCore.config.pushContextName + moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy block(moc) } } From 52f131ae2234237bd8248b6f9a611429b53f8dab Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 23 Jul 2021 12:29:29 -0700 Subject: [PATCH 131/203] all pull operations should be serialized in case we get called to sync while we're still syncing, eliminates potential duplicates --- Source/Classes/CloudCore.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 80c38f17..02e9e2f4 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -78,7 +78,11 @@ open class CloudCore { public static var userRecordID: CKRecord.ID? = nil - static private let queue = OperationQueue() + static private let queue: OperationQueue = { + let q = OperationQueue() + q.maxConcurrentOperationCount = 1 + return q + }() // MARK: - Methods @@ -104,8 +108,6 @@ open class CloudCore { pullOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } - - pullOperation.addDependency(subscribeOperation) queue.addOperation(subscribeOperation) queue.addOperation(pullOperation) @@ -236,6 +238,7 @@ open class CloudCore { if case .zoneNotFound = subError.code { // Zone wasn't found, we need to create it self.queue.cancelAllOperations() + let setupOperation = SetupOperation(container: container, uploadAllData: !(coreDataObserver?.usePersistentHistoryForPush)!) // for completeness, pull again @@ -243,7 +246,6 @@ open class CloudCore { pullOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } - pullOperation.addDependency(setupOperation) self.queue.addOperation(setupOperation) self.queue.addOperation(pullOperation) From 01a8b4f420b0456ca9fc0fc239e8a2e06b200404 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 29 Jul 2021 15:30:22 -0700 Subject: [PATCH 132/203] bumping podspec version to 4.0 --- CloudCore.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index a4c4113d..0cf8930f 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "3.1" + s.version = "4.0" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } From 66ebbd717f43c8a218707277cd15d1c9025bbaad Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 29 Jul 2021 15:41:24 -0700 Subject: [PATCH 133/203] update license & readme --- LICENSE.md | 2 +- README.md | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 3c778128..9b64f6b5 100755 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 deeje cooley +Copyright (c) 2021 deeje cooley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 38d9f506..546b0ee9 100755 --- a/README.md +++ b/README.md @@ -16,23 +16,22 @@ * Parent-Child relationships can be defined for CloudKit Sharing * Respects Core Data options (cascade deletions, external storage). * Knows and manages CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. -* Available on iOS, iPadOS, and watchOS (tvOS hasn't been tested) +* Available on iOS and iPadOS (watchOS and tvOS haven't been tested) * Sharing can be extended to your NSManagedObject classes, and native SharingUI is implemented -#### CloudCore vs iOS 13? +#### CloudCore vs NSPersistentCloudKitContainer? -At WWDC 2019, Apple announced support for NSPersistentCloudKitContainer in iOS 13, which provides native support for Core Data <-> CloudKit synchronization. Here are some initial thoughts on the differences between these two approaches. +NSPersistentCloudKitContainer provides native support for Core Data <-> CloudKit synchronization. Here are some thoughts on the differences between these two approaches. ###### NSPersistentCloudKitContainer * Simple to enable -* Private or(?) Public Database only, no Sharing +* Support for Private, Shared, and Public databases * Synchronizes All Records * No CloudKit Metadata (e.g. recordName, systemFields, owner) * Record-level Synchronization (entire objects are pushed) * Offline Synchronization is opaque, but doesn't appear to require NSPersistentHistoryTracking * All Core Data names are preceeded with "CD_" in CloudKit * Core Data Relationships are mapped thru CDMR records in CloudKit -* Uses a specific custom zone in the Private Database ###### CloudCore * Support requires specific configuration in the Core Data Model @@ -66,7 +65,7 @@ pod 'CloudCore' ``` ## How to help? -Current version of framework hasn't been deeply tested and may contain errors. If you can test framework, I will be very glad. If you found an error, please post [an issue](https://github.com/deeje/CloudCore/issues). +What would you like to see improved? ## Quick start 1. Enable CloudKit capability for you application: From 15598fbd1459d73ce15f9cc39f523d3b50aab5de Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 4 Aug 2021 17:16:09 -0700 Subject: [PATCH 134/203] set state of AsyncOp in its own thread NOT in its parentContext --- .../Classes/Pull/SubOperations/RecordToCoreDataOperation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index c3dc8282..7af88ee3 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -53,8 +53,8 @@ public class RecordToCoreDataOperation: AsynchronousOperation { self.errorBlock?(error) } - self.state = .finished } + self.state = .finished } /// Create or update existing NSManagedObject from CKRecord From 3fb1290995d91a61095f26350b3c31458893bf81 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 5 Aug 2021 09:50:50 -0700 Subject: [PATCH 135/203] still trying to resolve rando crash re missing refs --- Source/Classes/Pull/PullOperation.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 56810d04..1bd52bd3 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -35,7 +35,9 @@ public class PullOperation: Operation { let convertOperation = RecordToCoreDataOperation(parentContext: context, record: record) convertOperation.errorBlock = { self.errorBlock?($0) } convertOperation.completionBlock = { - self.objectsWithMissingReferences.append(convertOperation.missingObjectsPerEntities) + context.performAndWait { + self.objectsWithMissingReferences.append(convertOperation.missingObjectsPerEntities) + } } self.queue.addOperation(convertOperation) } From adebd430fcf42f0875d550ec6bc5145110e53b5d Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 12 Jan 2022 16:13:22 -0800 Subject: [PATCH 136/203] align podspec targets with project targets --- CloudCore.podspec | 8 +++++--- CloudCore.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 0cf8930f..7aab3e4b 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "4.0" + s.version = "4.0.1" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } @@ -11,14 +11,16 @@ Pod::Spec.new do |s| } s.ios.deployment_target = '13.0' - s.osx.deployment_target = '10.15' - s.tvos.deployment_target = '10.0' + s.osx.deployment_target = '11.0' + s.tvos.deployment_target = '12.0' s.watchos.deployment_target = '6.0' s.source_files = 'Source/**/*.swift' s.ios.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.osx.frameworks = 'Foundation', 'CloudKit', 'CoreData' + s.tvos.frameworks = 'Foundation', 'CloudKit', 'CoreData' + s.watchos.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.swift_versions = [5.1] s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 1a10e3b1..736b6d09 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -864,7 +864,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; - TVOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WATCHOS_DEPLOYMENT_TARGET = 6.0; @@ -919,7 +919,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; - TVOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; From 8d5f56c7ee84e7f17684c00e99cbcc97997fedc8 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 26 Jan 2022 12:55:36 -0800 Subject: [PATCH 137/203] fix for CloudKit error 3, not online when using cellular qualityOfService = .userInitiated via https://stackoverflow.com/questions/34189458/cloudkit-ckdatabaseoperation-not-working-on-cellular --- README.md | 4 ++-- .../FetchPublicSubscriptionsOperation.swift | 2 +- Source/Classes/Pull/PullChangesOperation.swift | 8 ++++---- Source/Classes/Pull/PullOperation.swift | 2 +- Source/Classes/Pull/PullRecordOperation.swift | 2 +- .../SubOperations/FetchRecordZoneChangesOperation.swift | 4 ++-- .../Pull/SubOperations/PurgeLocalDatabaseOperation.swift | 2 +- Source/Classes/Push/PushOperationQueue.swift | 2 +- Source/Classes/Setup/CreateCloudCoreZoneOperation.swift | 3 ++- Source/Classes/Setup/PushAllLocalDataOperation.swift | 2 +- Source/Classes/Setup/SetupOperation.swift | 2 +- Source/Classes/Setup/SubscribeOperation.swift | 6 +++--- 12 files changed, 20 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 546b0ee9..d5c06f5c 100755 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ CloudCore now has built-in support for CloudKit Sharing. There are several addi func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { let acceptShareOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata]) - acceptShareOperation.qualityOfService = .userInteractive + acceptShareOperation.qualityOfService = .userInitiated acceptShareOperation.perShareCompletionBlock = { meta, share, error in CloudCore.pull(rootRecordID: meta.rootRecordID, container: self.persistentContainer, error: nil) { } } @@ -198,7 +198,7 @@ OR func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { let acceptShareOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata]) - acceptShareOperation.qualityOfService = .userInteractive + acceptShareOperation.qualityOfService = .userInitiated acceptShareOperation.perShareCompletionBlock = { meta, share, error in CloudCore.pull(rootRecordID: meta.rootRecordID, container: self.persistentContainer, error: nil) { } } diff --git a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift index ebe055d1..e7767ef6 100644 --- a/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift +++ b/Source/Classes/Pull/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift @@ -20,7 +20,7 @@ class FetchPublicSubscriptionsOperation: AsynchronousOperation { super.init() name = "FetchPublicSubscriptionsOperation" - qualityOfService = .userInteractive + qualityOfService = .userInitiated } override func main() { diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift index 1f190b93..a4b8cb3c 100644 --- a/Source/Classes/Pull/PullChangesOperation.swift +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -65,7 +65,7 @@ public class PullChangesOperation: PullOperation { let changedRecordIDs: NSMutableSet = [] let deletedRecordIDs: NSMutableSet = [] let fetchNotificationChanges = CKFetchNotificationChangesOperation(previousServerChangeToken: databaseChangeToken) - fetchNotificationChanges.qualityOfService = .userInteractive + fetchNotificationChanges.qualityOfService = .userInitiated fetchNotificationChanges.notificationChangedBlock = { innerNotification in if let innerQueryNotification = innerNotification as? CKQueryNotification { if innerQueryNotification.queryNotificationReason == .recordDeleted { @@ -80,7 +80,7 @@ public class PullChangesOperation: PullOperation { let allChangedRecordIDs = changedRecordIDs.allObjects as! [CKRecord.ID] let fetchRecords = CKFetchRecordsOperation(recordIDs: allChangedRecordIDs) fetchRecords.database = CloudCore.config.container.publicCloudDatabase - fetchRecords.qualityOfService = .userInteractive + fetchRecords.qualityOfService = .userInitiated fetchRecords.perRecordCompletionBlock = { record, recordID, error in if error == nil { self.addConvertRecordOperation(record: record!, context: backgroundContext) @@ -111,7 +111,7 @@ public class PullChangesOperation: PullOperation { let fetchDatabaseChanges = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken) fetchDatabaseChanges.database = database - fetchDatabaseChanges.qualityOfService = .userInteractive + fetchDatabaseChanges.qualityOfService = .userInitiated fetchDatabaseChanges.recordZoneWithIDChangedBlock = { recordZoneID in changedZoneIDs.append(recordZoneID) } @@ -173,7 +173,7 @@ public class PullChangesOperation: PullOperation { if recordZoneIDs.isEmpty { return } let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) - recordZoneChangesOperation.qualityOfService = .userInteractive + recordZoneChangesOperation.qualityOfService = .userInitiated recordZoneChangesOperation.recordChangedBlock = { self.addConvertRecordOperation(record: $0, context: context) } diff --git a/Source/Classes/Pull/PullOperation.swift b/Source/Classes/Pull/PullOperation.swift index 1bd52bd3..063d59bf 100644 --- a/Source/Classes/Pull/PullOperation.swift +++ b/Source/Classes/Pull/PullOperation.swift @@ -24,7 +24,7 @@ public class PullOperation: Operation { super.init() - qualityOfService = .userInteractive + qualityOfService = .userInitiated queue.name = "PullQueue" queue.maxConcurrentOperationCount = 1 diff --git a/Source/Classes/Pull/PullRecordOperation.swift b/Source/Classes/Pull/PullRecordOperation.swift index 2617f042..fae1af74 100644 --- a/Source/Classes/Pull/PullRecordOperation.swift +++ b/Source/Classes/Pull/PullRecordOperation.swift @@ -63,7 +63,7 @@ public class PullRecordOperation: PullOperation { private func addFetchRecordsOp(recordIDs: [CKRecord.ID], backgroundContext: NSManagedObjectContext) { let fetchRecords = CKFetchRecordsOperation(recordIDs: recordIDs) fetchRecords.database = database - fetchRecords.qualityOfService = .userInteractive + fetchRecords.qualityOfService = .userInitiated fetchRecords.perRecordCompletionBlock = { record, recordID, error in if let record = record { self.fetchedRecordIDs.append(recordID!) diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index 39ceded2..3fdd60dd 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -38,7 +38,7 @@ class FetchRecordZoneChangesOperation: Operation { super.init() name = "FetchRecordZoneChangesOperation" - qualityOfService = .userInteractive + qualityOfService = .userInitiated } override func main() { @@ -90,7 +90,7 @@ class FetchRecordZoneChangesOperation: Operation { } fetchRecordZoneChanges.database = self.database - fetchRecordZoneChanges.qualityOfService = .userInteractive + fetchRecordZoneChanges.qualityOfService = .userInitiated return fetchRecordZoneChanges } diff --git a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift index cb04c79e..92cf3b24 100644 --- a/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift +++ b/Source/Classes/Pull/SubOperations/PurgeLocalDatabaseOperation.swift @@ -21,7 +21,7 @@ class PurgeLocalDatabaseOperation: Operation { super.init() name = "PurgeLocalDatabaseOperation" - qualityOfService = .userInteractive + qualityOfService = .userInitiated } override func main() { diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index 14594a0c..b8a3083f 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -49,7 +49,7 @@ class PushOperationQueue: OperationQueue { let modifyRecords = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) modifyRecords.database = database modifyRecords.savePolicy = .changedKeys - modifyRecords.qualityOfService = .userInteractive + modifyRecords.qualityOfService = .userInitiated modifyRecords.perRecordCompletionBlock = { record, error in if let error = error { diff --git a/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift b/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift index 374caf70..00285a86 100644 --- a/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift +++ b/Source/Classes/Setup/CreateCloudCoreZoneOperation.swift @@ -18,7 +18,7 @@ class CreateCloudCoreZoneOperation: AsynchronousOperation { super.init() name = "CreateCloudCoreZoneOperation" - qualityOfService = .userInteractive + qualityOfService = .userInitiated } override func main() { @@ -26,6 +26,7 @@ class CreateCloudCoreZoneOperation: AsynchronousOperation { let cloudCoreZone = CKRecordZone(zoneName: CloudCore.config.zoneName) let recordZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: [cloudCoreZone], recordZoneIDsToDelete: nil) + recordZoneOperation.qualityOfService = .userInitiated recordZoneOperation.modifyRecordZonesCompletionBlock = { if let error = $2 { self.errorBlock?(error) diff --git a/Source/Classes/Setup/PushAllLocalDataOperation.swift b/Source/Classes/Setup/PushAllLocalDataOperation.swift index 2ca681af..36600624 100644 --- a/Source/Classes/Setup/PushAllLocalDataOperation.swift +++ b/Source/Classes/Setup/PushAllLocalDataOperation.swift @@ -31,7 +31,7 @@ class PushAllLocalDataOperation: Operation { super.init() name = "PushAllLocalDataOperation" - qualityOfService = .userInteractive + qualityOfService = .userInitiated } override func main() { diff --git a/Source/Classes/Setup/SetupOperation.swift b/Source/Classes/Setup/SetupOperation.swift index f65f2e6c..03e7b477 100644 --- a/Source/Classes/Setup/SetupOperation.swift +++ b/Source/Classes/Setup/SetupOperation.swift @@ -32,7 +32,7 @@ class SetupOperation: Operation { super.init() name = "SetupOperation" - qualityOfService = .userInteractive + qualityOfService = .userInitiated } private let queue = OperationQueue() diff --git a/Source/Classes/Setup/SubscribeOperation.swift b/Source/Classes/Setup/SubscribeOperation.swift index 1b2ddedd..ef625fbf 100644 --- a/Source/Classes/Setup/SubscribeOperation.swift +++ b/Source/Classes/Setup/SubscribeOperation.swift @@ -19,7 +19,7 @@ class SubscribeOperation: AsynchronousOperation { super.init() name = "SubscribeOperation" - qualityOfService = .userInteractive + qualityOfService = .userInitiated } override func main() { @@ -75,7 +75,7 @@ class SubscribeOperation: AsynchronousOperation { } } - modifySubscriptions.qualityOfService = .userInteractive + modifySubscriptions.qualityOfService = .userInitiated return modifySubscriptions } @@ -89,7 +89,7 @@ class SubscribeOperation: AsynchronousOperation { operationToCancel.cancel() } } - fetchSubscriptions.qualityOfService = .userInteractive + fetchSubscriptions.qualityOfService = .userInitiated return fetchSubscriptions } From fde7deea14b0707fe2767ca6a42ed628013a2aef Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 26 Jan 2022 14:01:37 -0800 Subject: [PATCH 138/203] more qualityOfService = .userInitiated --- .../Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift | 2 ++ Source/Classes/Sharing/CloudCoreSharingController.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift index 6e399c61..f69bdb3b 100644 --- a/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift +++ b/Source/Classes/Pull/PublicSubscriptions/PublicDatabaseSubscriptions.swift @@ -44,6 +44,7 @@ public class PublicDatabaseSubscriptions { let config = CKOperation.Configuration() config.timeoutIntervalForResource = 20 + config.qualityOfService = .userInitiated modifySubscriptions.configuration = config CloudCore.config.container.publicCloudDatabase.add(modifySubscriptions) @@ -67,6 +68,7 @@ public class PublicDatabaseSubscriptions { let config = CKOperation.Configuration() config.timeoutIntervalForResource = 20 + config.qualityOfService = .userInitiated modifySubscription.configuration = config CloudCore.config.container.publicCloudDatabase.add(modifySubscription) diff --git a/Source/Classes/Sharing/CloudCoreSharingController.swift b/Source/Classes/Sharing/CloudCoreSharingController.swift index caa3f81d..39349f01 100644 --- a/Source/Classes/Sharing/CloudCoreSharingController.swift +++ b/Source/Classes/Sharing/CloudCoreSharingController.swift @@ -49,6 +49,7 @@ public class CloudCoreSharingController: NSObject, UICloudSharingControllerDeleg let sharingController = UICloudSharingController { _, handler in let modifyOp = CKModifyRecordsOperation(recordsToSave: [aRecord, share], recordIDsToDelete: nil) modifyOp.savePolicy = .changedKeys + modifyOp.qualityOfService = .userInitiated modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in if let share = records?.first as? CKShare { handler(share, CloudCore.config.container, error) From f50d8d77c27e637a983012510a390762d5ab7faa Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 26 Jan 2022 14:11:11 -0800 Subject: [PATCH 139/203] CloudCore.perform { configuredcontainer in } --- Source/Classes/CloudCore.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 02e9e2f4..12e53516 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -256,5 +256,21 @@ open class CloudCore { delegate?.error(error: subscriptionError, module: nil) } - + + static public func perform(completion: ((CKContainer) -> Void)) { + let container = config.container + + if #available(iOSApplicationExtension 15.0, *) { + let ckConfig = CKOperation.Configuration() +// ckConfig.container = container + ckConfig.qualityOfService = .userInitiated + ckConfig.allowsCellularAccess = true + container.configuredWith(configuration: ckConfig, group: nil) { configuredContainer in + completion(configuredContainer) + } + } else { + completion(container) + } + } + } From 1336c13fbdfab74a1c73fb445d42b6137cca2056 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 19 Mar 2022 12:03:54 -0700 Subject: [PATCH 140/203] fix use of #availability --- Source/Classes/CloudCore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 12e53516..f17ef182 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -260,7 +260,7 @@ open class CloudCore { static public func perform(completion: ((CKContainer) -> Void)) { let container = config.container - if #available(iOSApplicationExtension 15.0, *) { + if #available(iOS 15.0, *) { let ckConfig = CKOperation.Configuration() // ckConfig.container = container ckConfig.qualityOfService = .userInitiated From 271b9537757b9f674f05d4e9e93ebad52b62affe Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 19 Mar 2022 12:04:25 -0700 Subject: [PATCH 141/203] explicitly import CloudKit in Example AppDelegate --- Example/Sources/AppDelegate.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index c7829301..26460f9f 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -8,6 +8,7 @@ import UIKit import CoreData +import CloudKit import CloudCore import Connectivity From ba0187933f229a58fdcc7d116b8e26fab02aa5a8 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 19 Mar 2022 12:06:47 -0700 Subject: [PATCH 142/203] bump podspec s.version = 4.0.2 --- CloudCore.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 7aab3e4b..a59a72c0 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "4.0.1" + s.version = "4.0.2" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } From bc837c0d148c6f81cf0cd5d6e348da0cace43252 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 3 Apr 2022 16:48:54 -0700 Subject: [PATCH 143/203] read from record.encryptedValues note that record.allKeys() includes record.encryptedValues.allKeys() --- .../RecordToCoreDataOperation.swift | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 7af88ee3..1d489db9 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -89,10 +89,9 @@ public class RecordToCoreDataOperation: AsynchronousOperation { /// - entityName: entity name of `object` /// - recordDataAttributeName: attribute name containing recordData private func fill(object: NSManagedObject, entityName: String, serviceAttributeNames: ServiceAttributeNames, context: NSManagedObjectContext) throws { - for key in record.allKeys() { - let recordValue = record.value(forKey: key) - - let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) + + func storeValue(_ recordValue: Any?, for key: String) { + let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) if let coreDataValue = try? ckAttribute.makeCoreDataValue() { if let cdAttribute = object.entity.attributesByName[key], cdAttribute.attributeType == .transformableAttributeType, let data = coreDataValue as? Data { @@ -111,8 +110,28 @@ public class RecordToCoreDataOperation: AsynchronousOperation { missingObjectsPerEntities[object] = ckAttribute.notFoundRecordNamesForAttribute } } - } - + } + + if #available(iOS 15, *) { + let allKeys = record.allKeys() + let encryptedKeys = record.encryptedValues.allKeys() + + for key in allKeys { + let recordValue: Any? + if encryptedKeys.contains(key) { + recordValue = record.encryptedValues[key] + } else { + recordValue = record.value(forKey: key) + } + storeValue(recordValue, for: key) + } + } else { + for key in record.allKeys() { + let recordValue = record.value(forKey: key) + storeValue(recordValue, for: key) + } + } + // Set system headers object.setValue(record.recordID.recordName, forKey: serviceAttributeNames.recordName) object.setValue(record.recordID.zoneID.ownerName, forKey: serviceAttributeNames.ownerName) From 6d42cb106b96877eaf2f300943e161fe66a38971 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 3 Apr 2022 16:49:41 -0700 Subject: [PATCH 144/203] if allowsCloudEncryption, store in encryptedValues --- .../Push/ObjectToRecord/ObjectToRecordOperation.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index ac0ae404..981a9f6e 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -79,7 +79,15 @@ class ObjectToRecordOperation: Operation { if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) { let recordValue = try attribute.makeRecordValue() - record.setValue(recordValue, forKey: attributeName) + if #available(iOS 15, *) { + if attribute.description.allowsCloudEncryption { + record.encryptedValues[attributeName] = (recordValue as! __CKRecordObjCValue) + } else { + record.setValue(recordValue, forKey: attributeName) + } + } else { + record.setValue(recordValue, forKey: attributeName) + } } else if let relationship = CoreDataRelationship(scope: scope, value: value, relationshipName: attributeName, entity: managedObject.entity) { let references = try relationship.makeRecordValue() record.setValue(references, forKey: attributeName) From e3562b32086925da7382daeb8374e733e6e90370 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 3 Apr 2022 16:50:04 -0700 Subject: [PATCH 145/203] update Example app to support encrypted fields --- Example/Sources/Class/ModelFactory.swift | 7 ++++++- .../Model/Model.xcdatamodeld/Model.xcdatamodel/contents | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Example/Sources/Class/ModelFactory.swift b/Example/Sources/Class/ModelFactory.swift index 2fd21c59..55838228 100644 --- a/Example/Sources/Class/ModelFactory.swift +++ b/Example/Sources/Class/ModelFactory.swift @@ -37,7 +37,12 @@ class ModelFactory { org.name = faker.company.name() org.bs = faker.company.bs() org.founded = Date(timeIntervalSince1970: faker.number.randomDouble(min: 1292250324, max: 1513175137)) - + + org.secretString = "This is a secret" + org.secretInteger = 42 + org.secretDouble = 3.14 + org.secretBoolean = true + return org } diff --git a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 04b20ab1..b6f4db0a 100644 --- a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -26,6 +26,10 @@ + + + + @@ -38,6 +42,6 @@ - + \ No newline at end of file From 6828049372ace4c9e06942956ab1c15b9dce645f Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 3 Apr 2022 16:50:27 -0700 Subject: [PATCH 146/203] (update sharing type to match bundleID) --- Example/Sources/Model/Organization+CloudCoreSharing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/Sources/Model/Organization+CloudCoreSharing.swift b/Example/Sources/Model/Organization+CloudCoreSharing.swift index 5dc2522d..300c490b 100644 --- a/Example/Sources/Model/Organization+CloudCoreSharing.swift +++ b/Example/Sources/Model/Organization+CloudCoreSharing.swift @@ -18,7 +18,7 @@ extension Organization: CloudCoreSharing { } public var sharingType: String? { - return "com.deeje.cloudcore.example.organization" + return "com.deeje.sample.CloudCore.organization" } public var sharingImage: Data? { From 08c8a6e830f3c082f5f855de4438481dc5f28cca Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 3 Apr 2022 18:05:11 -0700 Subject: [PATCH 147/203] handle CKErrorUserDidResetEncryptedData --- Source/Classes/Push/CoreDataObserver.swift | 21 ++++++++-- .../Setup/DeleteCloudCoreZoneOperation.swift | 41 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 Source/Classes/Setup/DeleteCloudCoreZoneOperation.swift diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index d9760421..a0a8bfd6 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -276,26 +276,41 @@ class CoreDataObserver { case .zoneNotFound: pushOperationQueue.cancelAllOperations() + var resetZoneOperations: [Operation] = [] + + var deleteZoneOperation: Operation? = nil + if let _ = cloudError.userInfo["CKErrorUserDidResetEncryptedDataKey"] { + // per https://developer.apple.com/documentation/cloudkit/encrypting_user_data + let deleteOp = DeleteCloudCoreZoneOperation() + resetZoneOperations.append(deleteOp) + + deleteZoneOperation = deleteOp + } + // Create CloudCore Zone let createZoneOperation = CreateCloudCoreZoneOperation() createZoneOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) self.pushOperationQueue.cancelAllOperations() } + if let deleteZoneOperation = deleteZoneOperation { + createZoneOperation.addDependency(deleteZoneOperation) + } + resetZoneOperations.append(createZoneOperation) // Subscribe operation let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } subscribeOperation.addDependency(createZoneOperation) + resetZoneOperations.append(subscribeOperation) - pushOperationQueue.addOperation(subscribeOperation) - // Upload all local data let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } uploadOperation.addDependency(createZoneOperation) + resetZoneOperations.append(uploadOperation) - pushOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) + pushOperationQueue.addOperations(resetZoneOperations, waitUntilFinished: true) case .operationCancelled: return default: delegate?.error(error: cloudError, module: .some(.pushToCloud)) } diff --git a/Source/Classes/Setup/DeleteCloudCoreZoneOperation.swift b/Source/Classes/Setup/DeleteCloudCoreZoneOperation.swift new file mode 100644 index 00000000..2962c8e9 --- /dev/null +++ b/Source/Classes/Setup/DeleteCloudCoreZoneOperation.swift @@ -0,0 +1,41 @@ +// +// DeleteCloudCoreZoneOperation.swift +// CloudCore +// +// Created by deeje cooley on 4/3/22. +// + +import Foundation +import CloudKit + +class DeleteCloudCoreZoneOperation: AsynchronousOperation { + + var errorBlock: ErrorBlock? + private var deleteZoneOperation: CKModifyRecordZonesOperation? + + public override init() { + super.init() + + name = "CreateCloudCoreZoneOperation" + qualityOfService = .userInitiated + } + + override func main() { + super.main() + + let cloudCoreZone = CKRecordZone(zoneName: CloudCore.config.zoneName) + let recordZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: nil, recordZoneIDsToDelete: [cloudCoreZone.zoneID]) + recordZoneOperation.qualityOfService = .userInitiated + recordZoneOperation.modifyRecordZonesCompletionBlock = { + if let error = $2 { + self.errorBlock?(error) + } + + self.state = .finished + } + + CloudCore.config.container.privateCloudDatabase.add(recordZoneOperation) + self.deleteZoneOperation = recordZoneOperation + } + +} From eedd56444078613432f8d2fe64d60ef6f7f5a991 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 4 Apr 2022 12:45:22 -0700 Subject: [PATCH 148/203] another URL re CKErrorUserDidResetEncryptedDataKey --- Source/Classes/Push/CoreDataObserver.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index a0a8bfd6..b4d53205 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -281,6 +281,8 @@ class CoreDataObserver { var deleteZoneOperation: Operation? = nil if let _ = cloudError.userInfo["CKErrorUserDidResetEncryptedDataKey"] { // per https://developer.apple.com/documentation/cloudkit/encrypting_user_data + // see also https://github.com/apple/cloudkit-sample-encryption + let deleteOp = DeleteCloudCoreZoneOperation() resetZoneOperations.append(deleteOp) From eaa9db9e65970ed693a487db8fbfdced510ea611 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 12 Apr 2022 15:29:36 -0700 Subject: [PATCH 149/203] update project --- CloudCore.xcodeproj/project.pbxproj | 12 +++---- .../Classes/ErrorBlockProxyTests.swift | 32 ------------------- 2 files changed, 4 insertions(+), 40 deletions(-) delete mode 100644 Tests/CloudCoreTests/Classes/ErrorBlockProxyTests.swift diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 8e2f806a..e19f1784 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 570D8D23280631F900E6836A /* DeleteCloudCoreZoneOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570D8D22280631F900E6836A /* DeleteCloudCoreZoneOperation.swift */; }; 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57505AAF21A7591500D9CF8F /* PullResult.swift */; }; 575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */; }; 575ADF472655AB7C0050D693 /* PullChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */; }; @@ -42,11 +43,9 @@ E22A53DA1E4A8743009286C0 /* CloudKitAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */; }; E22C40461E42956C009469A1 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22C40451E42956C009469A1 /* CoreDataObserver.swift */; }; E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478B1E48A404004310F9 /* PushOperationQueue.swift */; }; - E247EF8D1E67775500EBD75E /* ErrorBlockProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */; }; E247EF971E67873E00EBD75E /* DeleteFromCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */; }; E247EF9A1E678EAC00EBD75E /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF981E678EA200EBD75E /* CustomFunctions.swift */; }; E24F44A61E4595B900F78819 /* CoreDataRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A51E4595B900F78819 /* CoreDataRelationship.swift */; }; - E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */; }; E28F0B931E671E7400BF532A /* CKRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0B911E671E6500BF532A /* CKRecordTests.swift */; }; E28F0BA21E67260900BF532A /* NSEntityDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0BA01E6725E700BF532A /* NSEntityDescriptionTests.swift */; }; E28F0BA31E67280100BF532A /* NSManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */; }; @@ -90,6 +89,7 @@ /* Begin PBXFileReference section */ 245F765CC7CBF0507158B4A9 /* Pods_CloudCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 570D8D22280631F900E6836A /* DeleteCloudCoreZoneOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteCloudCoreZoneOperation.swift; sourceTree = ""; }; 57505AAF21A7591500D9CF8F /* PullResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullResult.swift; sourceTree = ""; }; 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullRecordOperation.swift; sourceTree = ""; }; 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullChangesOperation.swift; sourceTree = ""; }; @@ -131,12 +131,10 @@ E22C40441E4291FB009469A1 /* CloudCore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CloudCore.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; E22C40451E42956C009469A1 /* CoreDataObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = ""; }; E23C478B1E48A404004310F9 /* PushOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushOperationQueue.swift; sourceTree = ""; }; - E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxyTests.swift; sourceTree = ""; }; E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteFromCoreDataOperationTests.swift; sourceTree = ""; }; E247EF981E678EA200EBD75E /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctions.swift; sourceTree = ""; }; E24F44A51E4595B900F78819 /* CoreDataRelationship.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataRelationship.swift; sourceTree = ""; }; E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectTests.swift; sourceTree = ""; }; - E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxy.swift; sourceTree = ""; }; E277DB061E7726FB00DC334A /* PublicDatabaseSubscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicDatabaseSubscriptions.swift; sourceTree = ""; }; E277DB0C1E77F96400DC334A /* FetchPublicSubscriptionsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPublicSubscriptionsOperation.swift; sourceTree = ""; }; E28F0B911E671E6500BF532A /* CKRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecordTests.swift; sourceTree = ""; }; @@ -253,6 +251,7 @@ D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */, D985DEAA1FE0335800236870 /* PushAllLocalDataOperation.swift */, D985DEA71FE0292000236870 /* SubscribeOperation.swift */, + 570D8D22280631F900E6836A /* DeleteCloudCoreZoneOperation.swift */, ); path = Setup; sourceTree = ""; @@ -320,7 +319,6 @@ E2075FF31E4BB70D00E31F1F /* Pull */, E2075FF21E4BB6F700E31F1F /* Push */, E200D44C1E48E13200B707D4 /* CloudCore.swift */, - E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */, ); path = Classes; sourceTree = ""; @@ -369,7 +367,6 @@ children = ( E247EF8E1E677D1400EBD75E /* Fetch */, E29D11771E69808800E3DCBF /* Upload */, - E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */, ); path = Classes; sourceTree = ""; @@ -699,6 +696,7 @@ E2E4D8411E76D5A600550CBE /* PullOperation.swift in Sources */, E2C02A141E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift in Sources */, E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */, + 570D8D23280631F900E6836A /* DeleteCloudCoreZoneOperation.swift in Sources */, E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */, E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */, D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */, @@ -715,7 +713,6 @@ E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */, 575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */, E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */, - E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */, D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */, D985DEAB1FE0335800236870 /* PushAllLocalDataOperation.swift in Sources */, E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */, @@ -770,7 +767,6 @@ E29D117D1E69A47700E3DCBF /* CoreDataRelationshipTests.swift in Sources */, D9B3C7391FCF0C9E00CDB7FF /* CorrectObject.swift in Sources */, E247EF9A1E678EAC00EBD75E /* CustomFunctions.swift in Sources */, - E247EF8D1E67775500EBD75E /* ErrorBlockProxyTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests/CloudCoreTests/Classes/ErrorBlockProxyTests.swift b/Tests/CloudCoreTests/Classes/ErrorBlockProxyTests.swift deleted file mode 100644 index 1092c7a7..00000000 --- a/Tests/CloudCoreTests/Classes/ErrorBlockProxyTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ErrorBlockProxyTests.swift -// CloudCore -// -// Created by Vasily Ulianov on 02.03.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import XCTest - -@testable import CloudCore - -class ErrorBlockProxyTests: XCTestCase { - func testProxy() { - var isErrorReceived = false - let errorBlock: ErrorBlock = { _ in - isErrorReceived = true - } - - let proxy = ErrorBlockProxy(destination: errorBlock) - - // Check null error - proxy.send(error: nil) - XCTAssertFalse(proxy.wasError) - XCTAssertFalse(isErrorReceived) - - // Check that proxy in proxifing - proxy.send(error: CloudCoreError.custom("test")) - XCTAssertTrue(proxy.wasError) - XCTAssertTrue(isErrorReceived) - } -} From f657c8bc2cd7102663a4f3b0f72edf0d8f92c81a Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 12 Apr 2022 15:30:47 -0700 Subject: [PATCH 150/203] =?UTF-8?q?Update=20ReadMe=20to=20include=20'Allow?= =?UTF-8?q?s=20Cloud=20Encryption=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d5c06f5c..ce2db131 100755 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ * **public database** push is supported * Parent-Child relationships can be defined for CloudKit Sharing * Respects Core Data options (cascade deletions, external storage). +* Support for 'Allows Cloud Encryption' for attributes in Core Data with automatic encoding to and from encryptedValues[] in CloudKit. * Knows and manages CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. * Available on iOS and iPadOS (watchOS and tvOS haven't been tested) * Sharing can be extended to your NSManagedObject classes, and native SharingUI is implemented From eb611c7bce33886e2085310c0c053d9a50d2906e Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 12 Apr 2022 15:31:49 -0700 Subject: [PATCH 151/203] update podspec version to 4.1 --- CloudCore.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index a59a72c0..287e3d38 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "4.0.2" + s.version = "4.1" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } From f975be5ffc78316dd7b2323450fac6dea1f7f81a Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 13 Apr 2022 10:50:44 -0700 Subject: [PATCH 152/203] proper #available for encryption support --- Source/Classes/CloudCore.swift | 2 +- .../Classes/Pull/SubOperations/RecordToCoreDataOperation.swift | 2 +- .../Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index f17ef182..048d1424 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -260,7 +260,7 @@ open class CloudCore { static public func perform(completion: ((CKContainer) -> Void)) { let container = config.container - if #available(iOS 15.0, *) { + if #available(iOS 15.0, watchOS 8.0, tvOS 15.0, macOS 12.0, *) { let ckConfig = CKOperation.Configuration() // ckConfig.container = container ckConfig.qualityOfService = .userInitiated diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 1d489db9..1ecce624 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -112,7 +112,7 @@ public class RecordToCoreDataOperation: AsynchronousOperation { } } - if #available(iOS 15, *) { + if #available(iOS 15.0, watchOS 8.0, tvOS 15.0, macOS 12.0, *) { let allKeys = record.allKeys() let encryptedKeys = record.encryptedValues.allKeys() diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 981a9f6e..d0ccdc2d 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -79,7 +79,7 @@ class ObjectToRecordOperation: Operation { if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) { let recordValue = try attribute.makeRecordValue() - if #available(iOS 15, *) { + if #available(iOS 15.0, watchOS 8.0, tvOS 15.0, macOS 12.0, *) { if attribute.description.allowsCloudEncryption { record.encryptedValues[attributeName] = (recordValue as! __CKRecordObjCValue) } else { From 3f5d57572e98c9992b6a91e10f603d4ec2588315 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 13 Apr 2022 10:51:06 -0700 Subject: [PATCH 153/203] clean up deprecated CKRecord_Reference_Action --- Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift b/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift index 729a5bd8..16f04512 100644 --- a/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift +++ b/Source/Classes/Push/ObjectToRecord/CoreDataRelationship.swift @@ -65,7 +65,7 @@ class CoreDataRelationship { } private func makeReference(from managedObject: NSManagedObject) throws -> CKRecord.Reference? { - let action: CKRecord_Reference_Action + let action: CKRecord.ReferenceAction if case .some(NSDeleteRule.cascadeDeleteRule) = description.inverseRelationship?.deleteRule { action = .deleteSelf } else { From f3a7c2f7b6aa055aa8d919818b70cc667ec73a8d Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 13 Apr 2022 11:00:15 -0700 Subject: [PATCH 154/203] update podspec version to 4.1.1 --- CloudCore.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 287e3d38..f02fcb01 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "4.1" + s.version = "4.1.1" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } From 3fde4c1b8d33c40b1805f6e0d98377a6487387d2 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 15 Apr 2022 13:28:46 -0700 Subject: [PATCH 155/203] masked attributes use desiredKeys in fetch, skip maskedUpload in modify --- .../Classes/Pull/PullChangesOperation.swift | 7 +- Source/Classes/Pull/PullRecordOperation.swift | 1 + .../FetchRecordZoneChangesOperation.swift | 8 ++- .../RecordToCoreDataOperation.swift | 2 + .../ObjectToRecordOperation.swift | 2 +- Source/Extensions/NSEntityDescription.swift | 64 +++++++++++++------ Source/Extensions/NSManagedObjectModel.swift | 12 ++++ Source/Model/ServiceAttributeName.swift | 19 +++++- 8 files changed, 89 insertions(+), 26 deletions(-) diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift index a4b8cb3c..91814515 100644 --- a/Source/Classes/Pull/PullChangesOperation.swift +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -172,7 +172,12 @@ public class PullChangesOperation: PullOperation { private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZone.ID], database: CKDatabase, context: NSManagedObjectContext) { if recordZoneIDs.isEmpty { return } - let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) + let desiredKeys = context.persistentStoreCoordinator?.managedObjectModel.desiredKeys + + let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, + recordZoneIDs: recordZoneIDs, + tokens: tokens, + desiredKeys: desiredKeys) recordZoneChangesOperation.qualityOfService = .userInitiated recordZoneChangesOperation.recordChangedBlock = { self.addConvertRecordOperation(record: $0, context: context) diff --git a/Source/Classes/Pull/PullRecordOperation.swift b/Source/Classes/Pull/PullRecordOperation.swift index fae1af74..33e8cced 100644 --- a/Source/Classes/Pull/PullRecordOperation.swift +++ b/Source/Classes/Pull/PullRecordOperation.swift @@ -64,6 +64,7 @@ public class PullRecordOperation: PullOperation { let fetchRecords = CKFetchRecordsOperation(recordIDs: recordIDs) fetchRecords.database = database fetchRecords.qualityOfService = .userInitiated + fetchRecords.desiredKeys = persistentContainer.managedObjectModel.desiredKeys fetchRecords.perRecordCompletionBlock = { record, recordID, error in if let record = record { self.fetchedRecordIDs.append(recordID!) diff --git a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift index 3fdd60dd..1109f6d1 100644 --- a/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Pull/SubOperations/FetchRecordZoneChangesOperation.swift @@ -22,7 +22,7 @@ class FetchRecordZoneChangesOperation: Operation { private let optionsByRecordZoneID: [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneConfiguration] private let fetchQueue = OperationQueue() - init(from database: CKDatabase, recordZoneIDs: [CKRecordZone.ID], tokens: Tokens) { + init(from database: CKDatabase, recordZoneIDs: [CKRecordZone.ID], tokens: Tokens, desiredKeys: [String]? = nil) { self.tokens = tokens self.database = database self.recordZoneIDs = recordZoneIDs @@ -32,6 +32,7 @@ class FetchRecordZoneChangesOperation: Operation { let options = CKFetchRecordZoneChangesOperation.ZoneConfiguration() options.previousServerChangeToken = self.tokens.token(for: zoneID) optionsByRecordZoneID[zoneID] = options + options.desiredKeys = desiredKeys } self.optionsByRecordZoneID = optionsByRecordZoneID @@ -73,6 +74,11 @@ class FetchRecordZoneChangesOperation: Operation { fetchRecordZoneChanges.recordWithIDWasDeletedBlock = { recordID, _ in self.recordWithIDWasDeletedBlock?(recordID) } + /* + fetchRecordZoneChanges.recordZoneChangeTokensUpdatedBlock = { zoneId, serverChangeToken, _ in + self.tokens.setToken(serverChangeToken, for: zoneId) + } + */ fetchRecordZoneChanges.recordZoneFetchCompletionBlock = { zoneId, serverChangeToken, clientChangeTokenData, isMore, error in self.tokens.setToken(serverChangeToken, for: zoneId) diff --git a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift index 1ecce624..25c73e74 100644 --- a/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Pull/SubOperations/RecordToCoreDataOperation.swift @@ -91,6 +91,8 @@ public class RecordToCoreDataOperation: AsynchronousOperation { private func fill(object: NSManagedObject, entityName: String, serviceAttributeNames: ServiceAttributeNames, context: NSManagedObjectContext) throws { func storeValue(_ recordValue: Any?, for key: String) { + if serviceAttributeNames.maskedDownload.contains(key) { return } + let ckAttribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) if let coreDataValue = try? ckAttribute.makeCoreDataValue() { if let cdAttribute = object.entity.attributesByName[key], cdAttribute.attributeType == .transformableAttributeType, diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index d0ccdc2d..20556a6a 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -75,7 +75,7 @@ class ObjectToRecordOperation: Operation { let changedValues = managedObject.committedValues(forKeys: changedAttributes) for (attributeName, value) in changedValues { - if serviceAttributeNames.contains(attributeName) { continue } + if serviceAttributeNames.isMaskedUpload(attributeName) { continue } if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) { let recordValue = try attribute.makeRecordValue() diff --git a/Source/Extensions/NSEntityDescription.swift b/Source/Extensions/NSEntityDescription.swift index 5dff9016..48b93112 100644 --- a/Source/Extensions/NSEntityDescription.swift +++ b/Source/Extensions/NSEntityDescription.swift @@ -68,27 +68,46 @@ extension NSEntityDescription { } } + let relationshipNames = relationshipsByName.map { $0.key } + return ServiceAttributeNames(entityName: entityName, scopes: attributeNamesFromUserInfo.scopes, recordName: recordNameAttribute, ownerName: ownerNameAttribute, privateRecordData: privateRecordDataAttribute, - publicRecordData: publicRecordDataAttribute) + publicRecordData: publicRecordDataAttribute, + allAttributeNames: attributeNamesFromUserInfo.allAttributeNames, + allRelationshipNames: relationshipNames, + maskedUpload: attributeNamesFromUserInfo.maskedUpload, + maskedDownload: attributeNamesFromUserInfo.maskedDownload) } /// Parse data from User Info dictionary - private func parseAttributeNamesFromUserInfo() -> (scopes: [CKDatabase.Scope], recordName: String?, ownerName: String?, privateRecordData: String?, publicRecordData: String?) { + private func parseAttributeNamesFromUserInfo() -> (scopes: [CKDatabase.Scope], + recordName: String?, + ownerName: String?, + privateRecordData: String?, + publicRecordData: String?, + allAttributeNames: [String], + maskedUpload: [String], + maskedDownload: [String]) { var scopes: [CKDatabase.Scope] = [] var recordNameAttribute: String? var ownerNameAttribute: String? var privateRecordDataAttribute: String? var publicRecordDataAttribute: String? + var allAttributeNames: [String] = [] + var maskedUpload: [String] = [] + var maskedDownload: [String] = [] func parse(_ attributeName: String, _ userInfo: [AnyHashable: Any]) { + allAttributeNames.append(attributeName) + for (key, value) in userInfo { - guard let key = key as? String, - let value = value as? String else { continue } - + guard let key = key as? String, let value = value as? String else { + continue + } + if key == ServiceAttributeNames.keyType { switch value { case ServiceAttributeNames.valueRecordName: recordNameAttribute = attributeName @@ -97,33 +116,36 @@ extension NSEntityDescription { case ServiceAttributeNames.valuePublicRecordData: publicRecordDataAttribute = attributeName default: continue } - } else if key == ServiceAttributeNames.keyScopes { - let scopeStrings = value.components(separatedBy: ",") - for scopeString in scopeStrings { - switch scopeString { - case "public": - scopes.append(.public) - case "private": - scopes.append(.private) - default: - break - } + + allAttributeNames.removeLast() + } else if key == ServiceAttributeNames.keyMasks { + let maskStrings = value.components(separatedBy: ",") + if maskStrings.contains("upload") { + maskedUpload.append(attributeName) + } + if maskStrings.contains("download") { + maskedDownload.append(attributeName) } } } } - if let userInfo = self.userInfo { - parse("", userInfo) + if let userInfo = self.userInfo, let scopesString = userInfo[ServiceAttributeNames.keyScopes] as? String { + let scopeComponents = scopesString.components(separatedBy: ",") + if scopeComponents.contains("public") { + scopes.append(.public) + } + if scopeComponents.contains("private") { + scopes.append(.private) + } } - // In attribute - for (attributeName, attributeDescription) in self.attributesByName { + for (attributeName, attributeDescription) in attributesByName { guard let userInfo = attributeDescription.userInfo else { continue } parse(attributeName, userInfo) } - return (scopes, recordNameAttribute, ownerNameAttribute, privateRecordDataAttribute, publicRecordDataAttribute) + return (scopes, recordNameAttribute, ownerNameAttribute, privateRecordDataAttribute, publicRecordDataAttribute, allAttributeNames, maskedUpload, maskedDownload) } } diff --git a/Source/Extensions/NSManagedObjectModel.swift b/Source/Extensions/NSManagedObjectModel.swift index 49bbbdbc..2d74009f 100644 --- a/Source/Extensions/NSManagedObjectModel.swift +++ b/Source/Extensions/NSManagedObjectModel.swift @@ -21,5 +21,17 @@ extension NSManagedObjectModel { return cloudCoreEntities } + + var desiredKeys: [String] { + var keys: Set = [] + + for entity in self.entities { + if let desired = entity.serviceAttributeNames?.desiredKeys() { + desired.forEach { keys.insert($0) } + } + } + + return keys.map { $0 } + } } diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index 2792858f..d1c53b10 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -14,6 +14,7 @@ struct ServiceAttributeNames { static let keyType = "CloudCoreType" static let keyScopes = "CloudCoreScopes" static let keyParent = "CloudCoreParent" + static let keyMasks = "CloudCoreMasks" static let valueRecordName = "recordName" static let valueOwnerName = "ownerName" @@ -29,12 +30,26 @@ struct ServiceAttributeNames { let privateRecordData: String let publicRecordData: String - func contains(_ attributeName: String) -> Bool { + let allAttributeNames: [String] + let allRelationshipNames: [String] + let maskedUpload: [String] + let maskedDownload: [String] + + func isMaskedUpload(_ attributeName: String) -> Bool { switch attributeName { case recordName, ownerName, privateRecordData, publicRecordData: return true default: - return false + return maskedUpload.contains(attributeName) } } + + func desiredKeys() -> [String] { + var keys = allAttributeNames.filter { !maskedDownload.contains($0) } + + keys.append(contentsOf: allRelationshipNames) + + return keys + } + } From 30204860a240e8b66aa151fc00d4d20787a6fd98 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 15 Apr 2022 13:28:58 -0700 Subject: [PATCH 156/203] update version to 5.0.0 --- CloudCore.podspec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index f02fcb01..88afae75 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "4.1.1" + s.version = "5.0.0" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } @@ -23,5 +23,4 @@ Pod::Spec.new do |s| s.watchos.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.swift_versions = [5.1] - s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' end From f90e5ffc1cbc72c0638ad17ef9aa655841a687cd Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 15 Apr 2022 13:30:40 -0700 Subject: [PATCH 157/203] updating ReadMe with Scope, Maskable Attributes --- README.md | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ce2db131..2d57f4b6 100755 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ ![Status](https://img.shields.io/badge/status-beta-orange.svg) ![Swift](https://img.shields.io/badge/swift-5.0-orange.svg) -**CloudCore** is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. +**CloudCore** is an advanced sync engine for CloudKit and Core Data. #### Features -* Leveraging **NSPersistentHistory**, local changes are pushed to CloudKit when online +* Leveraging **NSPersistentHistory**, local changes are pushed to CloudKit when online. Never lose a change again. * Pull manually or on CloudKit **remote notifications**. * **Differential sync**, only changed object and values are uploaded and downloaded. * Core Data relationships are preserved @@ -19,6 +19,7 @@ * Knows and manages CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. * Available on iOS and iPadOS (watchOS and tvOS haven't been tested) * Sharing can be extended to your NSManagedObject classes, and native SharingUI is implemented +* Maskable Attributes allows you to control which attributes are ignored during upload and/or download. #### CloudCore vs NSPersistentCloudKitContainer? @@ -33,6 +34,7 @@ NSPersistentCloudKitContainer provides native support for Core Data <-> CloudKit * Offline Synchronization is opaque, but doesn't appear to require NSPersistentHistoryTracking * All Core Data names are preceeded with "CD_" in CloudKit * Core Data Relationships are mapped thru CDMR records in CloudKit +* Sharing is supported via zones ###### CloudCore * Support requires specific configuration in the Core Data Model @@ -43,6 +45,8 @@ NSPersistentCloudKitContainer provides native support for Core Data <-> CloudKit * Offline Synchronziation via NSPersistentHistoryTracking * Core Data names are mapped exactly in CloudKit * Core Data Relationships are mapped to CloudKit CKReferences +* Maskable Attributes provides fine-grain control over local-only data and manually managed remote data. +* Sharing is supported via root records During their WWDC presentation, Apple very clearly stated that NSPersistentCloudKitContainer is a foundation for future support of more advanced features #YMMV @@ -169,10 +173,25 @@ When your *entities have relationships*, CloudCore will look for the following k ### 💡 Tips * I recommend to set the *Record Name* attribute as `Indexed`, to speed up updates in big databases. -* *Record Data* attributes are used to store archived version of `CKRecord` with system fields only (like timestamps, tokens), so don't worry about size, no real data will be stored here. +* *P… Record Data* attributes are used to store archived version of `CKRecord` with system fields only (like timestamps, tokens), so don't worry about size, no real data will be stored here. + +## Scope: Public and/or Private +You can designate which databases each entity will synchronized with. For each entity you want to synchronize, add an item to the entity's UserInfo, using the key `CloudCoreScope` and following values: +* `public` = pushed to public database +* `private` = synchronized with private (or shared) database +* 'public,private' = both + +### Why Both? +Maintaining two copies of a record means we get all the benefits of a private (and sharable) record, while also automatically maintaining a fully updated public copy. + +## Maskable Attributes +You can designate attributes in your managed objects to be masked during upload and/or download. For each attribute you want to mask, add an item to the attribute's UserInfo, using the key `CloudCoreMasks` and following values: +* `upload` = ignored during modify operations +* `download` = ignored during fetch operations +* `upload,download` = both ## CloudKit Sharing -CloudCore now has built-in support for CloudKit Sharing. There are several additional steps you must take to enable it in your application. +CloudCore has built-in support for CloudKit Sharing. There are several additional steps you must take to enable it in your application. 1. Add the CKSharingSupported key, with value true, to your info.plist From 2d5218afb2582b16a1abf1289631ba85082a43f3 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 16 Apr 2022 11:36:38 -0700 Subject: [PATCH 158/203] (add missing classes, update names to project) --- CloudCore.xcodeproj/project.pbxproj | 62 ++++++++++++++----- .../xcshareddata/xcschemes/CloudCore.xcscheme | 28 ++++----- .../DeleteFromCoreDataOperationTests.swift | 0 .../RecordToCoreDataOperationTests.swift | 0 .../CoreDataAttributeTests.swift | 0 .../CoreDataRelationshipTests.swift | 0 .../ObjectToRecordOperationTests.swift | 0 7 files changed, 60 insertions(+), 30 deletions(-) rename Tests/CloudCoreTests/Classes/{Fetch => Pull}/Operations/DeleteFromCoreDataOperationTests.swift (100%) rename Tests/CloudCoreTests/Classes/{Fetch => Pull}/Operations/RecordToCoreDataOperationTests.swift (100%) rename Tests/CloudCoreTests/Classes/{Upload => Push}/ObjectToRecord/CoreDataAttributeTests.swift (100%) rename Tests/CloudCoreTests/Classes/{Upload => Push}/ObjectToRecord/CoreDataRelationshipTests.swift (100%) rename Tests/CloudCoreTests/Classes/{Upload => Push}/ObjectToRecord/ObjectToRecordOperationTests.swift (100%) diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index e19f1784..0d17fcab 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -11,6 +11,12 @@ 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57505AAF21A7591500D9CF8F /* PullResult.swift */; }; 575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */; }; 575ADF472655AB7C0050D693 /* PullChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */; }; + 5763BF7E280B427900B2CCCD /* CloudCoreSharingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF7C280B427900B2CCCD /* CloudCoreSharingController.swift */; }; + 5763BF7F280B427900B2CCCD /* CloudCoreSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF7D280B427900B2CCCD /* CloudCoreSharing.swift */; }; + 5763BF8B280B42F400B2CCCD /* CloudKitSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF89280B42F400B2CCCD /* CloudKitSharing.swift */; }; + 5763BF8C280B42F400B2CCCD /* CloudCoreType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF8A280B42F400B2CCCD /* CloudCoreType.swift */; }; + 5763BF8F280B430C00B2CCCD /* NSManagedContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF8D280B430C00B2CCCD /* NSManagedContainer.swift */; }; + 5763BF90280B430C00B2CCCD /* UIViewController+CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF8E280B430C00B2CCCD /* UIViewController+CloudKit.swift */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; @@ -93,6 +99,12 @@ 57505AAF21A7591500D9CF8F /* PullResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullResult.swift; sourceTree = ""; }; 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullRecordOperation.swift; sourceTree = ""; }; 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullChangesOperation.swift; sourceTree = ""; }; + 5763BF7C280B427900B2CCCD /* CloudCoreSharingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreSharingController.swift; sourceTree = ""; }; + 5763BF7D280B427900B2CCCD /* CloudCoreSharing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreSharing.swift; sourceTree = ""; }; + 5763BF89280B42F400B2CCCD /* CloudKitSharing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitSharing.swift; sourceTree = ""; }; + 5763BF8A280B42F400B2CCCD /* CloudCoreType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreType.swift; sourceTree = ""; }; + 5763BF8D280B430C00B2CCCD /* NSManagedContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedContainer.swift; sourceTree = ""; }; + 5763BF8E280B430C00B2CCCD /* UIViewController+CloudKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+CloudKit.swift"; sourceTree = ""; }; 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5C6298B1C3A8BBD007F7B7C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -196,6 +208,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5763BF7B280B427900B2CCCD /* Sharing */ = { + isa = PBXGroup; + children = ( + 5763BF7D280B427900B2CCCD /* CloudCoreSharing.swift */, + 5763BF7C280B427900B2CCCD /* CloudCoreSharingController.swift */, + ); + path = Sharing; + sourceTree = ""; + }; D5B2E8951C3A780C00C0327D = { isa = PBXGroup; children = ( @@ -249,9 +270,9 @@ children = ( D9089D491FE14E57000FC60C /* SetupOperation.swift */, D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */, - D985DEAA1FE0335800236870 /* PushAllLocalDataOperation.swift */, D985DEA71FE0292000236870 /* SubscribeOperation.swift */, 570D8D22280631F900E6836A /* DeleteCloudCoreZoneOperation.swift */, + D985DEAA1FE0335800236870 /* PushAllLocalDataOperation.swift */, ); path = Setup; sourceTree = ""; @@ -315,10 +336,11 @@ isa = PBXGroup; children = ( E2075FF81E4BBEAC00E31F1F /* AsynchronousOperation.swift */, + E200D44C1E48E13200B707D4 /* CloudCore.swift */, D9089D481FE14E4A000FC60C /* Setup */, - E2075FF31E4BB70D00E31F1F /* Pull */, E2075FF21E4BB6F700E31F1F /* Push */, - E200D44C1E48E13200B707D4 /* CloudCore.swift */, + E2075FF31E4BB70D00E31F1F /* Pull */, + 5763BF7B280B427900B2CCCD /* Sharing */, ); path = Classes; sourceTree = ""; @@ -326,10 +348,10 @@ E2075FF21E4BB6F700E31F1F /* Push */ = { isa = PBXGroup; children = ( + E22C40451E42956C009469A1 /* CoreDataObserver.swift */, + E23C478B1E48A404004310F9 /* PushOperationQueue.swift */, E2FA74461E769D8700C3489D /* Model */, E288C5751E4C9519002360A1 /* ObjectToRecord */, - E23C478B1E48A404004310F9 /* PushOperationQueue.swift */, - E22C40451E42956C009469A1 /* CoreDataObserver.swift */, ); path = Push; sourceTree = ""; @@ -337,11 +359,11 @@ E2075FF31E4BB70D00E31F1F /* Pull */ = { isa = PBXGroup; children = ( - E277DB0F1E77FC9F00DC334A /* PublicSubscriptions */, - E2C02A171E4CDEDA001B2871 /* SubOperations */, E2E4D83D1E76D4EF00550CBE /* PullOperation.swift */, 575ADF452655AB7C0050D693 /* PullChangesOperation.swift */, 575ADF442655AB7C0050D693 /* PullRecordOperation.swift */, + E2C02A171E4CDEDA001B2871 /* SubOperations */, + E277DB0F1E77FC9F00DC334A /* PublicSubscriptions */, ); path = Pull; sourceTree = ""; @@ -358,6 +380,8 @@ isa = PBXGroup; children = ( D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */, + 5763BF8A280B42F400B2CCCD /* CloudCoreType.swift */, + 5763BF89280B42F400B2CCCD /* CloudKitSharing.swift */, ); path = Protocols; sourceTree = ""; @@ -365,18 +389,18 @@ E247EF8A1E67771C00EBD75E /* Classes */ = { isa = PBXGroup; children = ( - E247EF8E1E677D1400EBD75E /* Fetch */, - E29D11771E69808800E3DCBF /* Upload */, + E247EF8E1E677D1400EBD75E /* Pull */, + E29D11771E69808800E3DCBF /* Push */, ); path = Classes; sourceTree = ""; }; - E247EF8E1E677D1400EBD75E /* Fetch */ = { + E247EF8E1E677D1400EBD75E /* Pull */ = { isa = PBXGroup; children = ( E247EF8F1E677D1B00EBD75E /* Operations */, ); - path = Fetch; + path = Pull; sourceTree = ""; }; E247EF8F1E677D1B00EBD75E /* Operations */ = { @@ -420,6 +444,8 @@ E29BB21F1E433FDA0020F5B6 /* Extensions */ = { isa = PBXGroup; children = ( + 5763BF8D280B430C00B2CCCD /* NSManagedContainer.swift */, + 5763BF8E280B430C00B2CCCD /* UIViewController+CloudKit.swift */, D985DEAD1FE034A900236870 /* NSManagedObjectModel.swift */, E2D390071E4A49350019BBCD /* NSEntityDescription.swift */, E29BB2221E4346FF0020F5B6 /* NSManagedObject.swift */, @@ -437,12 +463,12 @@ path = Tests; sourceTree = ""; }; - E29D11771E69808800E3DCBF /* Upload */ = { + E29D11771E69808800E3DCBF /* Push */ = { isa = PBXGroup; children = ( E29D11781E69810F00E3DCBF /* ObjectToRecord */, ); - path = Upload; + path = Push; sourceTree = ""; }; E29D11781E69810F00E3DCBF /* ObjectToRecord */ = { @@ -596,7 +622,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0910; - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1330; ORGANIZATIONNAME = "Vasily Ulianov"; TargetAttributes = { D5B2E89E1C3A780C00C0327D = { @@ -691,16 +717,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5763BF8F280B430C00B2CCCD /* NSManagedContainer.swift in Sources */, E21FA03E1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift in Sources */, D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */, E2E4D8411E76D5A600550CBE /* PullOperation.swift in Sources */, E2C02A141E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift in Sources */, + 5763BF7E280B427900B2CCCD /* CloudCoreSharingController.swift in Sources */, E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */, 570D8D23280631F900E6836A /* DeleteCloudCoreZoneOperation.swift in Sources */, E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */, E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */, + 5763BF7F280B427900B2CCCD /* CloudCoreSharing.swift in Sources */, D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */, E23C478C1E48A404004310F9 /* PushOperationQueue.swift in Sources */, + 5763BF90280B430C00B2CCCD /* UIViewController+CloudKit.swift in Sources */, E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */, E22C40461E42956C009469A1 /* CoreDataObserver.swift in Sources */, E2075FF91E4BBEAC00E31F1F /* AsynchronousOperation.swift in Sources */, @@ -715,11 +745,13 @@ E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */, D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */, D985DEAB1FE0335800236870 /* PushAllLocalDataOperation.swift in Sources */, + 5763BF8C280B42F400B2CCCD /* CloudCoreType.swift in Sources */, E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */, D985DE9D1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift in Sources */, E2FA74481E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */, E29BB2211E4344E80020F5B6 /* CKRecord.swift in Sources */, E2D390081E4A49350019BBCD /* NSEntityDescription.swift in Sources */, + 5763BF8B280B42F400B2CCCD /* CloudKitSharing.swift in Sources */, E29BB2231E4346FF0020F5B6 /* NSManagedObject.swift in Sources */, E200D44D1E48E13200B707D4 /* CloudCore.swift in Sources */, D985DEA81FE0292000236870 /* SubscribeOperation.swift in Sources */, @@ -827,6 +859,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -889,6 +922,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; diff --git a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme b/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme index 9fac0451..90e5d898 100644 --- a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme +++ b/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + + + + @@ -40,17 +49,6 @@ - - - - - - - - Date: Tue, 19 Apr 2022 17:05:18 -0700 Subject: [PATCH 159/203] CloudCoreCacheable, manual upload & download --- CloudCore.xcodeproj/project.pbxproj | 24 ++ .../project.pbxproj | 8 + Example/Resources/Base.lproj/Main.storyboard | 44 +-- Example/Sources/Class/ModelFactory.swift | 22 +- .../Model/Datafile+CloudCoreCacheable.swift | 21 ++ .../Model/Datafile+CoreDataClass.swift | 14 + .../Model.xcdatamodel/contents | 50 +++- .../Model/Organization+CloudCoreSharing.swift | 1 - .../Model/Organization+CoreDataClass.swift | 1 - .../DetailViewController.swift | 33 ++- .../Caching/CloudCoreCacheManager.swift | 264 ++++++++++++++++++ .../Classes/Caching/CloudCoreCacheable.swift | 91 ++++++ Source/Classes/CloudCore.swift | 3 + Source/Model/ServiceAttributeName.swift | 1 + 14 files changed, 542 insertions(+), 35 deletions(-) create mode 100644 Example/Sources/Model/Datafile+CloudCoreCacheable.swift create mode 100644 Example/Sources/Model/Datafile+CoreDataClass.swift create mode 100644 Source/Classes/Caching/CloudCoreCacheManager.swift create mode 100644 Source/Classes/Caching/CloudCoreCacheable.swift diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 0d17fcab..612c64e1 100644 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -17,6 +17,10 @@ 5763BF8C280B42F400B2CCCD /* CloudCoreType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF8A280B42F400B2CCCD /* CloudCoreType.swift */; }; 5763BF8F280B430C00B2CCCD /* NSManagedContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF8D280B430C00B2CCCD /* NSManagedContainer.swift */; }; 5763BF90280B430C00B2CCCD /* UIViewController+CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF8E280B430C00B2CCCD /* UIViewController+CloudKit.swift */; }; + 5763BF9F280B490200B2CCCD /* DownloadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF9B280B490200B2CCCD /* DownloadOperation.swift */; }; + 5763BFA0280B490200B2CCCD /* CloudCoreCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF9C280B490200B2CCCD /* CloudCoreCacheManager.swift */; }; + 5763BFA1280B490200B2CCCD /* CloudCoreCacheable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF9D280B490200B2CCCD /* CloudCoreCacheable.swift */; }; + 5763BFA2280B490200B2CCCD /* UploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5763BF9E280B490200B2CCCD /* UploadOperation.swift */; }; D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; @@ -105,6 +109,10 @@ 5763BF8A280B42F400B2CCCD /* CloudCoreType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreType.swift; sourceTree = ""; }; 5763BF8D280B430C00B2CCCD /* NSManagedContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedContainer.swift; sourceTree = ""; }; 5763BF8E280B430C00B2CCCD /* UIViewController+CloudKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+CloudKit.swift"; sourceTree = ""; }; + 5763BF9B280B490200B2CCCD /* DownloadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadOperation.swift; sourceTree = ""; }; + 5763BF9C280B490200B2CCCD /* CloudCoreCacheManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreCacheManager.swift; sourceTree = ""; }; + 5763BF9D280B490200B2CCCD /* CloudCoreCacheable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreCacheable.swift; sourceTree = ""; }; + 5763BF9E280B490200B2CCCD /* UploadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadOperation.swift; sourceTree = ""; }; 8EAC4D8B1B0EF4ECE4BDA160 /* Pods_CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5C6298B1C3A8BBD007F7B7C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -217,6 +225,17 @@ path = Sharing; sourceTree = ""; }; + 5763BF9A280B490200B2CCCD /* Caching */ = { + isa = PBXGroup; + children = ( + 5763BF9D280B490200B2CCCD /* CloudCoreCacheable.swift */, + 5763BF9C280B490200B2CCCD /* CloudCoreCacheManager.swift */, + 5763BF9E280B490200B2CCCD /* UploadOperation.swift */, + 5763BF9B280B490200B2CCCD /* DownloadOperation.swift */, + ); + path = Caching; + sourceTree = ""; + }; D5B2E8951C3A780C00C0327D = { isa = PBXGroup; children = ( @@ -341,6 +360,7 @@ E2075FF21E4BB6F700E31F1F /* Push */, E2075FF31E4BB70D00E31F1F /* Pull */, 5763BF7B280B427900B2CCCD /* Sharing */, + 5763BF9A280B490200B2CCCD /* Caching */, ); path = Classes; sourceTree = ""; @@ -738,17 +758,21 @@ D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */, D97465FA1FE31A650060EA66 /* Module.swift in Sources */, 57505AB021A7591500D9CF8F /* PullResult.swift in Sources */, + 5763BFA1280B490200B2CCCD /* CloudCoreCacheable.swift in Sources */, 575ADF472655AB7C0050D693 /* PullChangesOperation.swift in Sources */, E29BB2371E4377F80020F5B6 /* CoreDataAttribute.swift in Sources */, E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */, 575ADF462655AB7C0050D693 /* PullRecordOperation.swift in Sources */, E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */, D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */, + 5763BFA0280B490200B2CCCD /* CloudCoreCacheManager.swift in Sources */, D985DEAB1FE0335800236870 /* PushAllLocalDataOperation.swift in Sources */, + 5763BFA2280B490200B2CCCD /* UploadOperation.swift in Sources */, 5763BF8C280B42F400B2CCCD /* CloudCoreType.swift in Sources */, E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */, D985DE9D1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift in Sources */, E2FA74481E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */, + 5763BF9F280B490200B2CCCD /* DownloadOperation.swift in Sources */, E29BB2211E4344E80020F5B6 /* CKRecord.swift in Sources */, E2D390081E4A49350019BBCD /* NSEntityDescription.swift in Sources */, 5763BF8B280B42F400B2CCCD /* CloudKitSharing.swift in Sources */, diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index 61fd0b22..11414d2f 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 57182766280E340E0078B30C /* Datafile+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57182765280E340E0078B30C /* Datafile+CoreDataClass.swift */; }; + 57182768280E344A0078B30C /* Datafile+CloudCoreCacheable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57182767280E344A0078B30C /* Datafile+CloudCoreCacheable.swift */; }; 579DFE0B2660506100B0A079 /* Organization+CloudCoreSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */; }; 579DFE0D266050A000B0A079 /* Organization+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579DFE0C266050A000B0A079 /* Organization+CoreDataClass.swift */; }; B4532A37427BB629A3A47821 /* Pods_CloudCoreExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */; }; @@ -40,6 +42,8 @@ /* Begin PBXFileReference section */ 2AD0596598E464554C061BBB /* Pods-CloudCoreExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample.release.xcconfig"; sourceTree = ""; }; 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 57182765280E340E0078B30C /* Datafile+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datafile+CoreDataClass.swift"; sourceTree = ""; }; + 57182767280E344A0078B30C /* Datafile+CloudCoreCacheable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datafile+CloudCoreCacheable.swift"; sourceTree = ""; }; 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Organization+CloudCoreSharing.swift"; sourceTree = ""; }; 579DFE0C266050A000B0A079 /* Organization+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Organization+CoreDataClass.swift"; sourceTree = ""; }; D97438151FE168D800650541 /* FRCTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRCTableViewDataSource.swift; sourceTree = ""; }; @@ -79,6 +83,8 @@ E2C3E3551E53299800A733BF /* Model.xcdatamodeld */, 579DFE0C266050A000B0A079 /* Organization+CoreDataClass.swift */, 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */, + 57182765280E340E0078B30C /* Datafile+CoreDataClass.swift */, + 57182767280E344A0078B30C /* Datafile+CloudCoreCacheable.swift */, ); path = Model; sourceTree = ""; @@ -305,9 +311,11 @@ D974381F1FE18ED100650541 /* NotificationsObserver.swift in Sources */, D97438231FE199F500650541 /* EmployeeTableViewCell.swift in Sources */, 579DFE0D266050A000B0A079 /* Organization+CoreDataClass.swift in Sources */, + 57182766280E340E0078B30C /* Datafile+CoreDataClass.swift in Sources */, E2C3E3571E53299800A733BF /* Model.xcdatamodeld in Sources */, E2C3E3541E53299800A733BF /* AppDelegate.swift in Sources */, D974381D1FE16E6E00650541 /* ModelFactory.swift in Sources */, + 57182768280E344A0078B30C /* Datafile+CloudCoreCacheable.swift in Sources */, D97438161FE168D800650541 /* FRCTableViewDataSource.swift in Sources */, 579DFE0B2660506100B0A079 /* Organization+CloudCoreSharing.swift in Sources */, E2C3E3591E53299800A733BF /* MasterViewController.swift in Sources */, diff --git a/Example/Resources/Base.lproj/Main.storyboard b/Example/Resources/Base.lproj/Main.storyboard index eb869eb0..8552674b 100644 --- a/Example/Resources/Base.lproj/Main.storyboard +++ b/Example/Resources/Base.lproj/Main.storyboard @@ -1,13 +1,9 @@ - - - - + + - - - + @@ -15,16 +11,16 @@ - + - + - + - + @@ -41,19 +37,19 @@ diff --git a/Example/Sources/Class/ModelFactory.swift b/Example/Sources/Class/ModelFactory.swift index 55838228..388ba886 100644 --- a/Example/Sources/Class/ModelFactory.swift +++ b/Example/Sources/Class/ModelFactory.swift @@ -47,13 +47,21 @@ class ModelFactory { } static func insertEmployee(context: NSManagedObjectContext) -> Employee { - let user = Employee(context: context) - user.department = faker.commerce.department() - user.name = faker.name.name() - user.workingSince = Date(timeIntervalSince1970: faker.number.randomDouble(min: 661109847, max: 1513186653)) - user.photoData = randomAvatar() - - return user + let employee = Employee(context: context) + employee.department = faker.commerce.department() + employee.name = faker.name.name() + employee.workingSince = Date(timeIntervalSince1970: faker.number.randomDouble(min: 661109847, max: 1513186653)) + + let datafile = Datafile(context: context) + datafile.suffix = ".png" + datafile.cacheState = .local + datafile.remoteStatus = .pending + datafile.employee = employee + + let photoData = randomAvatar() + try? photoData?.write(to: datafile.url) + + return employee } private static func randomAvatar() -> Data? { diff --git a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift new file mode 100644 index 00000000..3f383510 --- /dev/null +++ b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift @@ -0,0 +1,21 @@ +// +// Datafile+CloudCoreCacheable.swift +// CloudCoreExample +// +// Created by deeje cooley on 4/18/22. +// Copyright © 2022 Vasily Ulianov. All rights reserved. +// + +import CoreData +import CloudKit +import CloudCore + +extension Datafile: CloudCoreCacheable { + + override public func awakeFromInsert() { + super.awakeFromInsert() + + recordName = UUID().uuidString // want this precomputed so that url is functional + } + +} diff --git a/Example/Sources/Model/Datafile+CoreDataClass.swift b/Example/Sources/Model/Datafile+CoreDataClass.swift new file mode 100644 index 00000000..ac77bb6b --- /dev/null +++ b/Example/Sources/Model/Datafile+CoreDataClass.swift @@ -0,0 +1,14 @@ +// +// Datafile+CoreDataClass.swift +// CloudCoreExample +// +// Created by deeje cooley on 4/18/22. +// Copyright © 2022 Vasily Ulianov. All rights reserved. +// + +import CoreData + +@objc(Datafile) +public class Datafile: NSManagedObject { + +} diff --git a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index b6f4db0a..81cdfba8 100644 --- a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,14 +1,57 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -41,7 +84,8 @@ - + + \ No newline at end of file diff --git a/Example/Sources/Model/Organization+CloudCoreSharing.swift b/Example/Sources/Model/Organization+CloudCoreSharing.swift index 300c490b..a404ef9e 100644 --- a/Example/Sources/Model/Organization+CloudCoreSharing.swift +++ b/Example/Sources/Model/Organization+CloudCoreSharing.swift @@ -8,7 +8,6 @@ import CoreData import CloudKit -import UIKit import CloudCore extension Organization: CloudCoreSharing { diff --git a/Example/Sources/Model/Organization+CoreDataClass.swift b/Example/Sources/Model/Organization+CoreDataClass.swift index 2c793db1..3706bfba 100644 --- a/Example/Sources/Model/Organization+CoreDataClass.swift +++ b/Example/Sources/Model/Organization+CoreDataClass.swift @@ -7,7 +7,6 @@ // This file was automatically generated and then moved into the project. // -import UIKit import CoreData @objc(Organization) diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift index 53082eb4..8e8ebba3 100644 --- a/Example/Sources/View Controller/DetailViewController.swift +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -29,6 +29,7 @@ class DetailViewController: UITableViewController { tableDataSource = DetailTableDataSource(fetchRequest: fetchRequest, context: context, sectionNameKeyPath: nil, delegate: self, tableView: tableView) tableView.dataSource = tableDataSource + tableView.delegate = self try! tableDataSource.performFetch() guard let organization = try? self.context.existingObject(with: self.organizationID) as? CloudCoreSharing else { return } @@ -86,7 +87,30 @@ class DetailViewController: UITableViewController { } } } - + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let employee = tableDataSource.object(at: indexPath) + let employeeID = employee.objectID + + persistentContainer.performBackgroundTask { moc in + if let employee = try? moc.existingObject(with: employeeID) as? Employee, + let datafile = employee.datafiles?.allObjects.first as? Datafile + { + if datafile.remoteStatus == .available && datafile.cacheState != .cached { + datafile.cacheState = .download + } else if datafile.cacheState == .local && datafile.remoteStatus == .pending { + datafile.cacheState = .upload + } + + if moc.hasChanges { + try? moc.save() + } + } + } + + tableView.deselectRow(at: indexPath, animated: true) + } + } extension DetailViewController: FRCTableViewDelegate { @@ -97,8 +121,11 @@ extension DetailViewController: FRCTableViewDelegate { cell.nameLabel.text = employee.name - if let imageData = employee.photoData, let image = UIImage(data: imageData) { - cell.photoImageView.image = image + if let datafile = employee.datafiles?.allObjects.first as? Datafile, + datafile.localAvailable, + let image = UIImage(contentsOfFile: datafile.urlPath) + { + cell.photoImageView.image = image } else { cell.photoImageView.image = nil } diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift new file mode 100644 index 00000000..0c9f499f --- /dev/null +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -0,0 +1,264 @@ +// +// CloudCoreCacheManager.swift +// CloudCore +// +// Created by deeje cooley on 4/16/22. +// + +import Foundation +import CoreData +import CloudKit +import Network + +@objc +class CloudCoreCacheManager: NSObject { + + private let persistentContainer: NSPersistentContainer + private let backgroundContext: NSManagedObjectContext + private let container: CKContainer + private let cacheableClassNames: [String] + + private var frcs: [AnyObject] = [] + + public init(persistentContainer: NSPersistentContainer) { + self.persistentContainer = persistentContainer + + let backgroundContext = persistentContainer.newBackgroundContext() + backgroundContext.automaticallyMergesChangesFromParent = true + backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + self.backgroundContext = backgroundContext + + self.container = CloudCore.config.container + + var cacheableClassNames: [String] = [] + let entities = persistentContainer.managedObjectModel.entities + for entity in entities { + if let userInfo = entity.userInfo, userInfo[ServiceAttributeNames.keyCacheable] != nil { + cacheableClassNames.append(entity.managedObjectClassName!) + } + } + self.cacheableClassNames = cacheableClassNames + + super.init() + + restoreLongLivedOperations() + configureObservers() + } + + func process(cacheables: [CloudCoreCacheable]) { + for cacheable in cacheables { + switch cacheable.cacheState { + case .upload, .uploading: + upload(cacheableID: cacheable.objectID) + case .download, .downloading: + download(cacheableID: cacheable.objectID) + default: + break + } + } + } + + func update(_ cacheableID: NSManagedObjectID, in context: NSManagedObjectContext, change: @escaping (CloudCoreCacheable) -> Void) { + context.perform { + guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } + + change(cacheable) + + try? context.save() + } + } + + private func configureObservers() { + let context = backgroundContext + + context.perform { + for name in self.cacheableClassNames { + let triggerUpload = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.upload.rawValue) + let triggerDownload = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.download.rawValue) + let triggers = NSCompoundPredicate(orPredicateWithSubpredicates: [triggerUpload, triggerDownload]) + + let triggerRequest = NSFetchRequest(entityName: name) + triggerRequest.predicate = triggers + triggerRequest.sortDescriptors = [NSSortDescriptor(key: "cacheStateRaw", ascending: true)] + + let frc = NSFetchedResultsController(fetchRequest: triggerRequest, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil) + frc.delegate = self + + try? frc.performFetch() + if let cacheables = frc.fetchedObjects as? [CloudCoreCacheable] { + self.process(cacheables: cacheables) + } + + self.frcs.append(frc) + } + } + } + + func restoreLongLivedOperations() { + let context = backgroundContext + + context.perform { + for name in self.cacheableClassNames { + let uploading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.uploading.rawValue) + let downloading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.downloading.rawValue) + let existing = NSCompoundPredicate(orPredicateWithSubpredicates: [uploading, downloading]) + + let restoreRequest = NSFetchRequest(entityName: name) + restoreRequest.predicate = existing + + if let cacheables = try? context.fetch(restoreRequest) as? [CloudCoreCacheable] { + self.process(cacheables: cacheables) + } + } + } + } + + func findLongLivedOperation(with operationID: String) -> CKOperation? { + var foundOperation: CKOperation? = nil + + let semaphore = DispatchSemaphore(value: 0) + container.fetchLongLivedOperation(withID: operationID) { operation, error in + if let error = error { + print("Error fetching operation: \(operationID)\n\(error)") + // Handle error + // return + } + + foundOperation = operation as? CKModifyRecordsOperation + + semaphore.signal() + } + semaphore.wait() + + return foundOperation + } + + func longLivedConfiguration() -> CKOperation.Configuration { + let configuration = CKOperation.Configuration() + configuration.container = container + configuration.isLongLived = true + configuration.qualityOfService = .utility + + return configuration + } + + func upload(cacheableID: NSManagedObjectID) { + let container = container + let context = backgroundContext + + context.perform { + guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } + + var modifyOp: CKModifyRecordsOperation! + if let operationID = cacheable.operationID { + modifyOp = self.findLongLivedOperation(with: operationID) as? CKModifyRecordsOperation + } + + if modifyOp == nil + { + guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } + + record[cacheable.assetFieldName] = CKAsset(fileURL: cacheable.url) + record["remoteStatusRaw"] = RemoteStatus.available.rawValue + + modifyOp = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: nil) + modifyOp.configuration = self.longLivedConfiguration() + modifyOp.savePolicy = .changedKeys + + cacheable.operationID = modifyOp.operationID + } + + modifyOp.perRecordProgressBlock = { record, progress in + self.update(cacheableID, in: context) { cacheable in + cacheable.uploadProgress = progress + } + } + modifyOp.perRecordCompletionBlock = { record, error in + if error != nil { return } + + self.update(cacheableID, in: context) { cacheable in + cacheable.uploadProgress = 0 + cacheable.cacheState = .cached + } + } + modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in } + modifyOp.longLivedOperationWasPersistedBlock = { } + container.privateCloudDatabase.add(modifyOp) + + cacheable.cacheState = .uploading + try? context.save() + } + } + + func download(cacheableID: NSManagedObjectID) { + let container = container + let context = backgroundContext + + context.perform { + guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } + + var fetchOp: CKFetchRecordsOperation! + if let operationID = cacheable.operationID { + fetchOp = self.findLongLivedOperation(with: operationID) as? CKFetchRecordsOperation + } + + if fetchOp == nil + { + guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } + + fetchOp = CKFetchRecordsOperation(recordIDs: [record.recordID]) + fetchOp.configuration = self.longLivedConfiguration() + fetchOp.desiredKeys = [cacheable.assetFieldName] + + cacheable.operationID = fetchOp.operationID + } + + fetchOp.perRecordProgressBlock = { record, progress in + self.update(cacheableID, in: context) { cacheable in + cacheable.downloadProgress = progress + } + } + fetchOp.perRecordCompletionBlock = { record, recordID, error in + if error != nil { return } + + self.update(cacheableID, in: context) { cacheable in + if let asset = record?[cacheable.assetFieldName] as? CKAsset, + let downloadURL = asset.fileURL + { + let fileManager = FileManager.default + + try? fileManager.moveItem(at: downloadURL, to: cacheable.url) + } + + cacheable.downloadProgress = 0 + cacheable.cacheState = .cached + } + } + fetchOp.longLivedOperationWasPersistedBlock = { } + container.privateCloudDatabase.add(fetchOp) + + cacheable.cacheState = .uploading + try? context.save() + } + } + +} + +extension CloudCoreCacheManager: NSFetchedResultsControllerDelegate { + + func controller(_ controller: NSFetchedResultsController, + didChange anObject: Any, + at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, + newIndexPath: IndexPath?) { + guard let cacheable = anObject as? CloudCoreCacheable else { return } + + if cacheable.cacheState == .upload || cacheable.cacheState == .download { + process(cacheables: [cacheable]) + } + } + +} diff --git a/Source/Classes/Caching/CloudCoreCacheable.swift b/Source/Classes/Caching/CloudCoreCacheable.swift new file mode 100644 index 00000000..7d5c1a1c --- /dev/null +++ b/Source/Classes/Caching/CloudCoreCacheable.swift @@ -0,0 +1,91 @@ +// +// CloudCoreCacheable.swift +// CloudCore +// +// Created by deeje cooley on 4/16/22. +// + +import Foundation +import CoreData + +public enum CacheState: String { + case local + case upload // -> uploading -> cached + case uploading + + case remote + case download // -> downloading -> cached + case downloading + + case unload // -> remote + + case cached +} + +public enum RemoteStatus: String { + case pending + case available +} + +public protocol CloudCoreCacheable: CloudCoreType { + + // usually hardcoded in the class + var assetFieldName: String { get } + + // fully masked + var cacheStateRaw: String? { get set } + var operationID: String? { get set } + var uploadProgress: Double { get set } + var downloadProgress: Double { get set } + + // sync'ed + var remoteStatusRaw: String? { get set } + var suffix: String? { get set } + +} + +public extension CloudCoreCacheable { + + var assetFieldName: String { + return "assetData" + } + + var cacheState: CacheState { + get { + return cacheStateRaw == nil ? .remote : CacheState(rawValue: cacheStateRaw!)! + } + set { + cacheStateRaw = newValue.rawValue + } + } + + var remoteStatus: RemoteStatus { + get { + return remoteStatusRaw == nil ? .pending : RemoteStatus(rawValue: remoteStatusRaw!)! + } + set { + remoteStatusRaw = newValue.rawValue + } + } + + var localAvailable: Bool { + let availableStates: [CacheState] = [.local, .upload, .uploading, .cached] + + return availableStates.contains(cacheState) + } + + var remoteAvailable: Bool { + return remoteStatus == .available + } + + var urlPath: String { + let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first + + return cacheDirectory! + "/" + recordName! + "." + (suffix ?? "") + } + + var url: URL { + return URL(fileURLWithPath: urlPath) + } + +} diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 048d1424..9b0a91e7 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -52,6 +52,7 @@ open class CloudCore { // MARK: - Properties private(set) static var coreDataObserver: CoreDataObserver? + private(set) static var cacheManager: CloudCoreCacheManager? public static var isOnline: Bool { get { return coreDataObserver?.isOnline ?? false @@ -97,6 +98,8 @@ open class CloudCore { observer.start() self.coreDataObserver = observer + self.cacheManager = CloudCoreCacheManager(persistentContainer: container) + // Subscribe (subscription may be outdated/removed) let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = { diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index d1c53b10..ff3eb31d 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -15,6 +15,7 @@ struct ServiceAttributeNames { static let keyScopes = "CloudCoreScopes" static let keyParent = "CloudCoreParent" static let keyMasks = "CloudCoreMasks" + static let keyCacheable = "CloudCoreCacheable" static let valueRecordName = "recordName" static let valueOwnerName = "ownerName" From 66c4c8a9dfcee62309ee2da808f2fb185c8a8360 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 20 Apr 2022 13:20:39 -0700 Subject: [PATCH 160/203] trigger upload after successful push --- .../Caching/CloudCoreCacheManager.swift | 2 +- Source/Classes/Push/CoreDataObserver.swift | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 0c9f499f..ac28db9b 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -240,7 +240,7 @@ class CloudCoreCacheManager: NSObject { fetchOp.longLivedOperationWasPersistedBlock = { } container.privateCloudDatabase.add(fetchOp) - cacheable.cacheState = .uploading + cacheable.cacheState = .downloading try? context.save() } } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index b4d53205..b8ad677a 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -121,7 +121,7 @@ class CoreDataObserver { return success } - + @objc private func willSave(notification: Notification) { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } @@ -218,12 +218,27 @@ class CoreDataObserver { self.converter.prepareOperationsFor(inserted: insertedObjects, updated: updatedObject, deleted: deletedRecordIDs) - + try? moc.save() if self.converter.hasPendingOperations { success = self.processChanges() } + + // check for cached assets + if success == true { + for insertedObject in insertedObjects { + moc.refresh(insertedObject, mergeChanges: true) + + guard let cacheable = insertedObject as? CloudCoreCacheable, + cacheable.cacheState == .local + else { continue } + + cacheable.cacheState = .upload + } + + try? moc.save() + } } return success From 2524a390a095830ec2b1744aa70aabbe25b1ee26 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 20 Apr 2022 14:44:31 -0700 Subject: [PATCH 161/203] show progress during upload, automatic download --- .../project.pbxproj | 4 + Example/Resources/Base.lproj/Main.storyboard | 10 ++ .../Model/CoreDataContextObserver.swift | 133 ++++++++++++++++++ .../DetailViewController.swift | 72 ++++++---- .../Sources/View/EmployeeTableViewCell.swift | 9 ++ .../Classes/Caching/CloudCoreCacheable.swift | 15 +- 6 files changed, 213 insertions(+), 30 deletions(-) create mode 100644 Example/Sources/Model/CoreDataContextObserver.swift diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index 11414d2f..4ed6336f 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 570603D72810A3D900F52353 /* CoreDataContextObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570603D62810A3D900F52353 /* CoreDataContextObserver.swift */; }; 57182766280E340E0078B30C /* Datafile+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57182765280E340E0078B30C /* Datafile+CoreDataClass.swift */; }; 57182768280E344A0078B30C /* Datafile+CloudCoreCacheable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57182767280E344A0078B30C /* Datafile+CloudCoreCacheable.swift */; }; 579DFE0B2660506100B0A079 /* Organization+CloudCoreSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */; }; @@ -41,6 +42,7 @@ /* Begin PBXFileReference section */ 2AD0596598E464554C061BBB /* Pods-CloudCoreExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample.release.xcconfig"; sourceTree = ""; }; + 570603D62810A3D900F52353 /* CoreDataContextObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataContextObserver.swift; sourceTree = ""; }; 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57182765280E340E0078B30C /* Datafile+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datafile+CoreDataClass.swift"; sourceTree = ""; }; 57182767280E344A0078B30C /* Datafile+CloudCoreCacheable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datafile+CloudCoreCacheable.swift"; sourceTree = ""; }; @@ -85,6 +87,7 @@ 579DFE0A2660506100B0A079 /* Organization+CloudCoreSharing.swift */, 57182765280E340E0078B30C /* Datafile+CoreDataClass.swift */, 57182767280E344A0078B30C /* Datafile+CloudCoreCacheable.swift */, + 570603D62810A3D900F52353 /* CoreDataContextObserver.swift */, ); path = Model; sourceTree = ""; @@ -314,6 +317,7 @@ 57182766280E340E0078B30C /* Datafile+CoreDataClass.swift in Sources */, E2C3E3571E53299800A733BF /* Model.xcdatamodeld in Sources */, E2C3E3541E53299800A733BF /* AppDelegate.swift in Sources */, + 570603D72810A3D900F52353 /* CoreDataContextObserver.swift in Sources */, D974381D1FE16E6E00650541 /* ModelFactory.swift in Sources */, 57182768280E344A0078B30C /* Datafile+CloudCoreCacheable.swift in Sources */, D97438161FE168D800650541 /* FRCTableViewDataSource.swift in Sources */, diff --git a/Example/Resources/Base.lproj/Main.storyboard b/Example/Resources/Base.lproj/Main.storyboard index 8552674b..dac73e96 100644 --- a/Example/Resources/Base.lproj/Main.storyboard +++ b/Example/Resources/Base.lproj/Main.storyboard @@ -54,13 +54,22 @@ + + + + + + + + + @@ -75,6 +84,7 @@ + diff --git a/Example/Sources/Model/CoreDataContextObserver.swift b/Example/Sources/Model/CoreDataContextObserver.swift new file mode 100644 index 00000000..66580c0d --- /dev/null +++ b/Example/Sources/Model/CoreDataContextObserver.swift @@ -0,0 +1,133 @@ +// +// CoreDataContextObserver.swift +// +// Created by Michal Zaborowski on 10.05.2016. +// Copyright © 2016 Inspace Labs Sp z o. o. Spółka Komandytowa. All rights reserved. +// +import Foundation +import CoreData + +public struct CoreDataContextObserverState: OptionSet { + public let rawValue: Int + public init(rawValue: Int) { self.rawValue = rawValue } + + public static let Inserted = CoreDataContextObserverState(rawValue: 1 << 0) + public static let Updated = CoreDataContextObserverState(rawValue: 1 << 1) + public static let Deleted = CoreDataContextObserverState(rawValue: 1 << 2) + public static let Refreshed = CoreDataContextObserverState(rawValue: 1 << 3) + + public static let All: CoreDataContextObserverState = [Inserted, Updated, Deleted, Refreshed] +} + +public typealias CoreDataContextObserverCompletionBlock = (NSManagedObject,CoreDataContextObserverState) -> () +public typealias CoreDataContextObserverContextChangeBlock = (_ notification: NSNotification, _ changedObjects: [CoreDataObserverObjectChange]) -> () + +public enum CoreDataObserverObjectChange { + case Updated(NSManagedObject) + case Refreshed(NSManagedObject) + case Inserted(NSManagedObject) + case Deleted(NSManagedObject) + + public func managedObject() -> NSManagedObject { + switch self { + case let .Updated(value): return value + case let .Inserted(value): return value + case let .Refreshed(value): return value + case let .Deleted(value): return value + } + } +} + +public struct CoreDataObserverAction { + var state: CoreDataContextObserverState + var completionBlock: CoreDataContextObserverCompletionBlock +} + +public class CoreDataContextObserver { + public var enabled: Bool = true + public var contextChangeBlock: CoreDataContextObserverContextChangeBlock? + + private var notificationObserver: NSObjectProtocol? + private(set) var context: NSManagedObjectContext + private(set) var actionsForManagedObjectID: Dictionary = [:] + private(set) weak var persistentStoreCoordinator: NSPersistentStoreCoordinator? + + deinit { + unobserveAllObjects() + if let notificationObserver = notificationObserver { + NotificationCenter.default.removeObserver(notificationObserver) + } + } + + public init(context: NSManagedObjectContext) { + self.context = context + self.persistentStoreCoordinator = context.persistentStoreCoordinator + + notificationObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: context, queue: nil) { [weak self] notification in + self?.handleContextObjectDidChangeNotification(notification: notification as NSNotification) + } + } + + private func handleContextObjectDidChangeNotification(notification: NSNotification) { + guard let incomingContext = notification.object as? NSManagedObjectContext, + let persistentStoreCoordinator = persistentStoreCoordinator, + let incomingPersistentStoreCoordinator = incomingContext.persistentStoreCoordinator, enabled && persistentStoreCoordinator == incomingPersistentStoreCoordinator else { + return + } + + let insertedObjectsSet = notification.userInfo?[NSInsertedObjectsKey] as? Set ?? Set() + let updatedObjectsSet = notification.userInfo?[NSUpdatedObjectsKey] as? Set ?? Set() + let deletedObjectsSet = notification.userInfo?[NSDeletedObjectsKey] as? Set ?? Set() + let refreshedObjectsSet = notification.userInfo?[NSRefreshedObjectsKey] as? Set ?? Set() + + var combinedObjectChanges = insertedObjectsSet.map({ CoreDataObserverObjectChange.Inserted($0) }) + combinedObjectChanges += updatedObjectsSet.map({ CoreDataObserverObjectChange.Updated($0) }) + combinedObjectChanges += deletedObjectsSet.map({ CoreDataObserverObjectChange.Deleted($0) }) + combinedObjectChanges += refreshedObjectsSet.map({ CoreDataObserverObjectChange.Refreshed($0) }) + + contextChangeBlock?(notification, combinedObjectChanges) + + let combinedSet = insertedObjectsSet.union(updatedObjectsSet).union(deletedObjectsSet).union(refreshedObjectsSet) + let allObjectIDs = Array(actionsForManagedObjectID.keys) + let filteredObjects = combinedSet.filter({ allObjectIDs.contains($0.objectID) }) + + for object in filteredObjects { + guard let actionsForObject = actionsForManagedObjectID[object.objectID] else { continue } + + for action in actionsForObject { + if action.state.contains(.Inserted) && insertedObjectsSet.contains(object) { + action.completionBlock(object,.Inserted) + } else if action.state.contains(.Updated) && updatedObjectsSet.contains(object) { + action.completionBlock(object,.Updated) + } else if action.state.contains(.Deleted) && deletedObjectsSet.contains(object) { + action.completionBlock(object,.Deleted) + } else if action.state.contains(.Refreshed) && refreshedObjectsSet.contains(object) { + action.completionBlock(object,.Refreshed) + } + } + } + } + + public func observeObject(object: NSManagedObject, state: CoreDataContextObserverState = .All, completionBlock: @escaping CoreDataContextObserverCompletionBlock) { + let action = CoreDataObserverAction(state: state, completionBlock: completionBlock) + if var actionArray = actionsForManagedObjectID[object.objectID] { + actionArray.append(action) + actionsForManagedObjectID[object.objectID] = actionArray + } else { + actionsForManagedObjectID[object.objectID] = [action] + } + + } + + public func unobserveObject(object: NSManagedObject, forState state: CoreDataContextObserverState = .All) { + if state == .All { + actionsForManagedObjectID[object.objectID] = nil + } else if let actionsForObject = actionsForManagedObjectID[object.objectID] { + actionsForManagedObjectID[object.objectID] = actionsForObject.filter { !$0.state.contains(state) } + } + } + + public func unobserveAllObjects() { + actionsForManagedObjectID.removeAll() + } +} diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift index 8e8ebba3..04ef5a6f 100644 --- a/Example/Sources/View Controller/DetailViewController.swift +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -19,6 +19,13 @@ class DetailViewController: UITableViewController { private var sharingController: CloudCoreSharingController! + private var datafilesObserver: CoreDataContextObserver! + private var updateCellQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + override func viewDidLoad() { super.viewDidLoad() @@ -46,6 +53,8 @@ class DetailViewController: UITableViewController { buttons.append(shareButton) navigationItem.setRightBarButtonItems(buttons, animated: false) + + datafilesObserver = CoreDataContextObserver(context: context) } @objc private func add(_ sender: UIBarButtonItem) { @@ -89,25 +98,6 @@ class DetailViewController: UITableViewController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let employee = tableDataSource.object(at: indexPath) - let employeeID = employee.objectID - - persistentContainer.performBackgroundTask { moc in - if let employee = try? moc.existingObject(with: employeeID) as? Employee, - let datafile = employee.datafiles?.allObjects.first as? Datafile - { - if datafile.remoteStatus == .available && datafile.cacheState != .cached { - datafile.cacheState = .download - } else if datafile.cacheState == .local && datafile.remoteStatus == .pending { - datafile.cacheState = .upload - } - - if moc.hasChanges { - try? moc.save() - } - } - } - tableView.deselectRow(at: indexPath, animated: true) } @@ -120,15 +110,41 @@ extension DetailViewController: FRCTableViewDelegate { let employee = tableDataSource.object(at: indexPath) cell.nameLabel.text = employee.name - - if let datafile = employee.datafiles?.allObjects.first as? Datafile, - datafile.localAvailable, - let image = UIImage(contentsOfFile: datafile.urlPath) - { - cell.photoImageView.image = image - } else { - cell.photoImageView.image = nil - } + + cell.progressView.isHidden = true + cell.progressView.progress = 0 + + if let datafile = employee.datafiles?.allObjects.first as? Datafile { + if datafile.localAvailable { + cell.photoImageView.image = UIImage(contentsOfFile: datafile.urlPath) + } else if datafile.readyToDownload { + datafilesObserver.observeObject(object: datafile) { datafile, state in + guard let cacheable = datafile as? CloudCoreCacheable else { return } + + cell.progressView.isHidden = cacheable.progress == 0 + cell.progressView.progress = Float(cacheable.progress) + + if cacheable.localAvailable { + cell.photoImageView.image = UIImage(contentsOfFile: cacheable.urlPath) + cell.progressView.isHidden = true + cell.progressView.progress = 0 + + self.datafilesObserver.unobserveObject(object: datafile) + } + } + + persistentContainer.performBackgroundTask { moc in + guard let cacheable = try? moc.existingObject(with: datafile.objectID) as? CloudCoreCacheable else { return } + + cacheable.cacheState = .download + + try? moc.save() + } + } else { + cell.progressView.isHidden = datafile.progress == 0 + cell.progressView.progress = Float(datafile.progress) + } + } var departmentText = employee.department ?? "No" departmentText += " department" diff --git a/Example/Sources/View/EmployeeTableViewCell.swift b/Example/Sources/View/EmployeeTableViewCell.swift index 71021935..6d7a2b5b 100644 --- a/Example/Sources/View/EmployeeTableViewCell.swift +++ b/Example/Sources/View/EmployeeTableViewCell.swift @@ -14,5 +14,14 @@ class EmployeeTableViewCell: UITableViewCell { @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var departmentLabel: UILabel! @IBOutlet weak var sinceLabel: UILabel! + @IBOutlet weak var progressView: UIProgressView! + override func prepareForReuse() { + super.prepareForReuse() + + photoImageView.image = nil + progressView.progress = 0 + progressView.isHidden = true + } + } diff --git a/Source/Classes/Caching/CloudCoreCacheable.swift b/Source/Classes/Caching/CloudCoreCacheable.swift index 7d5c1a1c..a07b98e0 100644 --- a/Source/Classes/Caching/CloudCoreCacheable.swift +++ b/Source/Classes/Caching/CloudCoreCacheable.swift @@ -74,8 +74,19 @@ public extension CloudCoreCacheable { return availableStates.contains(cacheState) } - var remoteAvailable: Bool { - return remoteStatus == .available + var readyToDownload: Bool { + return remoteStatus == .available && cacheState == .remote + } + + var progress: Double { + switch cacheState { + case .uploading: + return uploadProgress + case .downloading: + return downloadProgress + default: + return 0 + } } var urlPath: String { From 16f739beecd947ba90562254e4408be8c06c19cf Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 20 Apr 2022 15:30:15 -0700 Subject: [PATCH 162/203] no refresh needed when triggering cacheable.upload --- Source/Classes/Push/CoreDataObserver.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index b8ad677a..33432406 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -227,9 +227,7 @@ class CoreDataObserver { // check for cached assets if success == true { - for insertedObject in insertedObjects { - moc.refresh(insertedObject, mergeChanges: true) - + for insertedObject in insertedObjects { guard let cacheable = insertedObject as? CloudCoreCacheable, cacheable.cacheState == .local else { continue } From 54bcdc9a7d869997da6f9a0568ebd96b7d66005c Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 20 Apr 2022 17:11:55 -0700 Subject: [PATCH 163/203] remove data file when cacheable is deleted --- Example/Sources/Model/Datafile+CloudCoreCacheable.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift index 3f383510..9959f909 100644 --- a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift +++ b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift @@ -18,4 +18,10 @@ extension Datafile: CloudCoreCacheable { recordName = UUID().uuidString // want this precomputed so that url is functional } + override public func prepareForDeletion() { + if localAvailable { + try? FileManager.default.removeItem(at: url) + } + } + } From 362543773d4e5eea9fb5ff1f0384f0c2dd01799e Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 20 Apr 2022 17:12:49 -0700 Subject: [PATCH 164/203] =?UTF-8?q?(we=E2=80=99ll=20come=20back=20to=20.un?= =?UTF-8?q?load)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/Classes/Caching/CloudCoreCacheable.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheable.swift b/Source/Classes/Caching/CloudCoreCacheable.swift index a07b98e0..d0733290 100644 --- a/Source/Classes/Caching/CloudCoreCacheable.swift +++ b/Source/Classes/Caching/CloudCoreCacheable.swift @@ -17,8 +17,6 @@ public enum CacheState: String { case download // -> downloading -> cached case downloading - case unload // -> remote - case cached } From e3cc7fda1beb7931b70cd05f287174205cb2c804 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 20 Apr 2022 17:37:19 -0700 Subject: [PATCH 165/203] when caching fails, save lastErrorMessage --- .../Model.xcdatamodel/contents | 7 +- .../Caching/CloudCoreCacheManager.swift | 70 +++++++++++++------ .../Classes/Caching/CloudCoreCacheable.swift | 1 + Source/Classes/Push/CoreDataObserver.swift | 25 ++++--- Source/Enum/Module.swift | 6 +- 5 files changed, 75 insertions(+), 34 deletions(-) diff --git a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 81cdfba8..543ff6d7 100644 --- a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -16,6 +16,11 @@ + + + + + @@ -84,7 +89,7 @@ - + diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index ac28db9b..0b93070c 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -41,7 +41,7 @@ class CloudCoreCacheManager: NSObject { super.init() - restoreLongLivedOperations() + restoreDanglingOperations() configureObservers() } @@ -58,16 +58,25 @@ class CloudCoreCacheManager: NSObject { } } - func update(_ cacheableID: NSManagedObjectID, in context: NSManagedObjectContext, change: @escaping (CloudCoreCacheable) -> Void) { - context.perform { - guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } - - change(cacheable) - - try? context.save() + func update(_ cacheableIDs: [NSManagedObjectID], change: @escaping (CloudCoreCacheable) -> Void) { + persistentContainer.performBackgroundTask { context in + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + do { + for cacheableID in cacheableIDs { + if let cacheable = try context.existingObject(with: cacheableID) as? CloudCoreCacheable { + change(cacheable) + } + } + + if context.hasChanges { + try context.save() + } + } catch { + CloudCore.delegate?.error(error: error, module: nil) + } } } - + private func configureObservers() { let context = backgroundContext @@ -97,7 +106,7 @@ class CloudCoreCacheManager: NSObject { } } - func restoreLongLivedOperations() { + func restoreDanglingOperations() { let context = backgroundContext context.perform { @@ -105,13 +114,24 @@ class CloudCoreCacheManager: NSObject { let uploading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.uploading.rawValue) let downloading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.downloading.rawValue) let existing = NSCompoundPredicate(orPredicateWithSubpredicates: [uploading, downloading]) - let restoreRequest = NSFetchRequest(entityName: name) restoreRequest.predicate = existing - if let cacheables = try? context.fetch(restoreRequest) as? [CloudCoreCacheable] { self.process(cacheables: cacheables) } + + let hasError = NSPredicate(format: "%K != nil", "lastErrorMessage") + let isLocal = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.local.rawValue) + let failedToUpload = NSCompoundPredicate(orPredicateWithSubpredicates: [hasError, isLocal]) + let restartRequest = NSFetchRequest(entityName: name) + restartRequest.predicate = failedToUpload + if let cacheables = try? context.fetch(restartRequest) as? [CloudCoreCacheable] { + let cacheableIDs = cacheables.map { $0.objectID } + self.update(cacheableIDs) { cacheable in + cacheable.lastErrorMessage = nil + cacheable.cacheState = .upload + } + } } } } @@ -172,16 +192,19 @@ class CloudCoreCacheManager: NSObject { } modifyOp.perRecordProgressBlock = { record, progress in - self.update(cacheableID, in: context) { cacheable in + self.update([cacheableID]) { cacheable in cacheable.uploadProgress = progress } } modifyOp.perRecordCompletionBlock = { record, error in - if error != nil { return } - - self.update(cacheableID, in: context) { cacheable in + self.update([cacheableID]) { cacheable in cacheable.uploadProgress = 0 - cacheable.cacheState = .cached + cacheable.cacheState = (error == nil) ? .cached : .local + cacheable.lastErrorMessage = error?.localizedDescription + } + + if let error = error { + CloudCore.delegate?.error(error: error, module: .cacheToCloud) } } modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in } @@ -217,14 +240,12 @@ class CloudCoreCacheManager: NSObject { } fetchOp.perRecordProgressBlock = { record, progress in - self.update(cacheableID, in: context) { cacheable in + self.update([cacheableID]) { cacheable in cacheable.downloadProgress = progress } } fetchOp.perRecordCompletionBlock = { record, recordID, error in - if error != nil { return } - - self.update(cacheableID, in: context) { cacheable in + self.update([cacheableID]) { cacheable in if let asset = record?[cacheable.assetFieldName] as? CKAsset, let downloadURL = asset.fileURL { @@ -234,7 +255,12 @@ class CloudCoreCacheManager: NSObject { } cacheable.downloadProgress = 0 - cacheable.cacheState = .cached + cacheable.cacheState = (error == nil) ? .cached : .remote + cacheable.lastErrorMessage = error?.localizedDescription + } + + if let error = error { + CloudCore.delegate?.error(error: error, module: .cacheFromCloud) } } fetchOp.longLivedOperationWasPersistedBlock = { } diff --git a/Source/Classes/Caching/CloudCoreCacheable.swift b/Source/Classes/Caching/CloudCoreCacheable.swift index d0733290..a822e5b3 100644 --- a/Source/Classes/Caching/CloudCoreCacheable.swift +++ b/Source/Classes/Caching/CloudCoreCacheable.swift @@ -35,6 +35,7 @@ public protocol CloudCoreCacheable: CloudCoreType { var operationID: String? { get set } var uploadProgress: Double { get set } var downloadProgress: Double { get set } + var lastErrorMessage: String? { get set } // sync'ed var remoteStatusRaw: String? { get set } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 33432406..7027cc23 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -224,18 +224,25 @@ class CoreDataObserver { if self.converter.hasPendingOperations { success = self.processChanges() } - + // check for cached assets if success == true { - for insertedObject in insertedObjects { - guard let cacheable = insertedObject as? CloudCoreCacheable, - cacheable.cacheState == .local - else { continue } - - cacheable.cacheState = .upload + let insertedIDs = insertedObjects.map { $0.objectID } + container.performBackgroundTask { moc in + do { + for insertedID in insertedIDs { + guard let cacheable = try moc.existingObject(with: insertedID) as? CloudCoreCacheable, + cacheable.cacheState == .local + else { continue } + + cacheable.cacheState = .upload + } + + try moc.save() + } catch { + self.delegate?.error(error: error, module: .some(.pushToCloud)) + } } - - try? moc.save() } } diff --git a/Source/Enum/Module.swift b/Source/Enum/Module.swift index cf956d54..6f31bf2a 100644 --- a/Source/Enum/Module.swift +++ b/Source/Enum/Module.swift @@ -11,10 +11,12 @@ import Foundation /// Enumeration with module name that issued an error in `CloudCoreErrorDelegate` public enum Module { - /// Save to CloudKit module case pushToCloud - /// Fetch from CloudKit module case pullFromCloud + + case cacheToCloud + + case cacheFromCloud } From 918422508357fdfad0608a0767498fc4fba32d16 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 21 Apr 2022 13:00:54 -0700 Subject: [PATCH 166/203] retry on rateLimit or zoneBusy errors --- .../Caching/CloudCoreCacheManager.swift | 18 ++++++++++++++++++ Source/Classes/Push/CoreDataObserver.swift | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 0b93070c..c592ecff 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -205,6 +205,15 @@ class CloudCoreCacheManager: NSObject { if let error = error { CloudCore.delegate?.error(error: error, module: .cacheToCloud) + + if let cloudError = error as? CKError, + cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, + let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber + { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { + self.upload(cacheableID: cacheableID) + } + } } } modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in } @@ -261,6 +270,15 @@ class CloudCoreCacheManager: NSObject { if let error = error { CloudCore.delegate?.error(error: error, module: .cacheFromCloud) + + if let cloudError = error as? CKError, + cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, + let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber + { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { + self.download(cacheableID: cacheableID) + } + } } } fetchOp.longLivedOperationWasPersistedBlock = { } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 7027cc23..55b56d2f 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -229,6 +229,7 @@ class CoreDataObserver { if success == true { let insertedIDs = insertedObjects.map { $0.objectID } container.performBackgroundTask { moc in + moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy do { for insertedID in insertedIDs { guard let cacheable = try moc.existingObject(with: insertedID) as? CloudCoreCacheable, @@ -292,6 +293,16 @@ class CoreDataObserver { } switch cloudError.code { + case .requestRateLimited, .zoneBusy: + pushOperationQueue.cancelAllOperations() + + if let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { [weak self] in + guard let observer = self else { return } + observer.processPersistentHistory() + } + } + // Zone was accidentally deleted (NOT PURGED), we need to reupload all data accroding Apple Guidelines case .zoneNotFound: pushOperationQueue.cancelAllOperations() From 423b123b0df27f58dc25cd4951540ffb56a8ad0c Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 21 Apr 2022 13:16:40 -0700 Subject: [PATCH 167/203] (call UI completion handlers on main thread) --- .../Sources/View Controller/MasterViewController.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Example/Sources/View Controller/MasterViewController.swift b/Example/Sources/View Controller/MasterViewController.swift index 78500c75..339e9c58 100644 --- a/Example/Sources/View Controller/MasterViewController.swift +++ b/Example/Sources/View Controller/MasterViewController.swift @@ -116,7 +116,9 @@ extension MasterViewController { moc.delete(personEntity) try? moc.save() } - completion(true) + DispatchQueue.main.async { + completion(true) + } } } alert.addAction(confirm) @@ -139,7 +141,9 @@ extension MasterViewController { moc.delete(deleteObject) try? moc.save() } - completion(true) + DispatchQueue.main.async { + completion(true) + } } } else { completion(false) From aef1d45098897e57c11803c6aba3240f88cde94d Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 21 Apr 2022 13:20:28 -0700 Subject: [PATCH 168/203] =?UTF-8?q?merged=20can=E2=80=99t=20fail=20during?= =?UTF-8?q?=20pull?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/Classes/Pull/PullChangesOperation.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Classes/Pull/PullChangesOperation.swift b/Source/Classes/Pull/PullChangesOperation.swift index 91814515..c4a11f14 100644 --- a/Source/Classes/Pull/PullChangesOperation.swift +++ b/Source/Classes/Pull/PullChangesOperation.swift @@ -57,6 +57,7 @@ public class PullChangesOperation: PullOperation { let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.name = CloudCore.config.pullContextName + backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy for database in databases { let databaseChangeToken = tokens.token(for: database.databaseScope) From 9fa8f861e492cb1e9e756c38a28808564ce86842 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 21 Apr 2022 15:28:24 -0700 Subject: [PATCH 169/203] store cacheables in App Support directory --- Source/Classes/Caching/CloudCoreCacheable.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheable.swift b/Source/Classes/Caching/CloudCoreCacheable.swift index a822e5b3..9fb07a31 100644 --- a/Source/Classes/Caching/CloudCoreCacheable.swift +++ b/Source/Classes/Caching/CloudCoreCacheable.swift @@ -27,9 +27,6 @@ public enum RemoteStatus: String { public protocol CloudCoreCacheable: CloudCoreType { - // usually hardcoded in the class - var assetFieldName: String { get } - // fully masked var cacheStateRaw: String? { get set } var operationID: String? { get set } @@ -89,7 +86,7 @@ public extension CloudCoreCacheable { } var urlPath: String { - let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first + let cacheDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first return cacheDirectory! + "/" + recordName! + "." + (suffix ?? "") } From 5a94ae06dde4c1e943c5b7bf271d9cfdcd135106 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 21 Apr 2022 15:29:23 -0700 Subject: [PATCH 170/203] update ReadMe with Cacheable Assets info --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d57f4b6..06822618 100755 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ * Available on iOS and iPadOS (watchOS and tvOS haven't been tested) * Sharing can be extended to your NSManagedObject classes, and native SharingUI is implemented * Maskable Attributes allows you to control which attributes are ignored during upload and/or download. +* Cacheable Assets are uploaded automatically and downloaded on-demand, using long-lived operations separate from sync operations. #### CloudCore vs NSPersistentCloudKitContainer? @@ -190,6 +191,56 @@ You can designate attributes in your managed objects to be masked during upload * `download` = ignored during fetch operations * `upload,download` = both +##Cacheable Assets +By default, CloudCore will transform assets in your CloudKit records into binary data attributes in your Core Data objects. + +But when you're working with very large files, such as photos, audio, or video, this default mode isn't optimal. + +* Uploading large files can take a long time, and sync will fail if not completed timely. +* To optimize a user's device storage, you may want to downloading large files on-demand. + +Cacheable Assets addresses these requirements by leveraging Maskable Attributes to ignore asset fields during sync, and then enabling push and pull of asset fields using long-lived operations. + +In order to manage cache state, assets must be stored in their own special entity type in your existing schema, which comform to the CloudCoreCacheable protocol. This protocol defines a number of attributes required to manage cache state: + +```swift +public protocol CloudCoreCacheable: CloudCoreType { + // fully masked + var cacheStateRaw: String? { get set } + var operationID: String? { get set } + var uploadProgress: Double { get set } + var downloadProgress: Double { get set } + var lastErrorMessage: String? { get set } + // sync'ed + var remoteStatusRaw: String? { get set } + var suffix: String? { get set } +} +``` + +The heart of CloudCoreCacheable is implemented using the following properties: + +```swift +public extension CloudCoreCacheable { + + var cacheState: CacheState + var remoteStatus: RemoteStatus + var url: URL + +} +``` + +Once you've configured your Core Data schema to support cacheable assets, you can create and download them as needed. + +When you create a new cacheable managed object, you must store its data at the file URL before saving it. The default value of cacheState is "local" and the default value of remoteStatus is "pending". Once CloudCore pushes the new cacheable record, it sets the cacheState to "upload", which triggers a long-lived upload operation. On completion, the cacheable managed object will have its cacheState set to "cached" and its remoteStatus set to "available". + +When cacheable records are pulled from CloudKit, the asset field is ignored (because it is masked), and the cacheState will be "remote". When the remoteStatus is "available", you can trigger a download by setting the cacheState to "download" and saving the object. Once completed, the cacheable object will have its cacheState set to "cached", and the data will be locally available at the file URL. + +Note that cacheState represents a state machine. +(**new**) => local -> (push) -> upload -> uploading -> cached +(pull) => remote -> **download** -> downloading -> cached + +See the Example app for specific details. + ## CloudKit Sharing CloudCore has built-in support for CloudKit Sharing. There are several additional steps you must take to enable it in your application. @@ -236,7 +287,7 @@ Note that when a user accepts a share, the app does not receive a remote notific 4. When a user wants to delete an object, your app must distinguish between the owner and a sharer, and either delete the object or the share. ## Example application -You can find example application at [Example](/Example/) directory, which has been updated to demonstrate sharing. +You can find example application at [Example](/Example/) directory, which has been updated to demonstrate sharing, maskable attributes, and cacheable assets. **How to run it:** 1. Set Bundle Identifier. From b66bd713df537664217a02158bc534c1b7b85ab8 Mon Sep 17 00:00:00 2001 From: deeje Date: Thu, 21 Apr 2022 15:37:33 -0700 Subject: [PATCH 171/203] MUST override awakeFromInsert & prepareForDeletion --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06822618..246ef940 100755 --- a/README.md +++ b/README.md @@ -239,7 +239,8 @@ Note that cacheState represents a state machine. (**new**) => local -> (push) -> upload -> uploading -> cached (pull) => remote -> **download** -> downloading -> cached -See the Example app for specific details. +### Important +See the Example app for specific details. Note, specifically, that I **need to override awakeFromInsert and prepareForDeletion** for my cacheable managed object type Datafile. If anyone has ideas on how to push this critical implementation detail into CloudCore itself, let me know! ## CloudKit Sharing CloudCore has built-in support for CloudKit Sharing. There are several additional steps you must take to enable it in your application. From 88a80e5243d23964c3fda74718907fadb8c8ecf5 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 25 Apr 2022 12:09:05 -0700 Subject: [PATCH 172/203] very important to preserveAfterDeletion --- .../Model.xcdatamodeld/Model.xcdatamodel/contents | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 543ff6d7..de799290 100644 --- a/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Example/Sources/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -27,9 +27,9 @@ - - - + + + @@ -54,7 +54,7 @@ - + @@ -73,7 +73,7 @@ - + From 1c473afbac078111ce9fb740c1898c624dc33ed2 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 25 Apr 2022 12:42:10 -0700 Subject: [PATCH 173/203] move default values into awakeFromInsert --- Example/Sources/Class/ModelFactory.swift | 2 -- Example/Sources/Model/Datafile+CloudCoreCacheable.swift | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Example/Sources/Class/ModelFactory.swift b/Example/Sources/Class/ModelFactory.swift index 388ba886..ee05253c 100644 --- a/Example/Sources/Class/ModelFactory.swift +++ b/Example/Sources/Class/ModelFactory.swift @@ -54,8 +54,6 @@ class ModelFactory { let datafile = Datafile(context: context) datafile.suffix = ".png" - datafile.cacheState = .local - datafile.remoteStatus = .pending datafile.employee = employee let photoData = randomAvatar() diff --git a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift index 9959f909..9b5f0e2d 100644 --- a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift +++ b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift @@ -15,7 +15,9 @@ extension Datafile: CloudCoreCacheable { override public func awakeFromInsert() { super.awakeFromInsert() - recordName = UUID().uuidString // want this precomputed so that url is functional + recordName = UUID().uuidString // want this precomputed so that url is functional + cacheState = .local + remoteStatus = .pending } override public func prepareForDeletion() { From a39bc0f5aeb960e08b115d56931ea6b9df010b40 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 25 Apr 2022 15:29:32 -0700 Subject: [PATCH 174/203] nope, developer must manually set cache state --- Example/Sources/Class/ModelFactory.swift | 2 ++ Example/Sources/Model/Datafile+CloudCoreCacheable.swift | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/Sources/Class/ModelFactory.swift b/Example/Sources/Class/ModelFactory.swift index ee05253c..388ba886 100644 --- a/Example/Sources/Class/ModelFactory.swift +++ b/Example/Sources/Class/ModelFactory.swift @@ -54,6 +54,8 @@ class ModelFactory { let datafile = Datafile(context: context) datafile.suffix = ".png" + datafile.cacheState = .local + datafile.remoteStatus = .pending datafile.employee = employee let photoData = randomAvatar() diff --git a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift index 9b5f0e2d..87f9d467 100644 --- a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift +++ b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift @@ -16,8 +16,6 @@ extension Datafile: CloudCoreCacheable { super.awakeFromInsert() recordName = UUID().uuidString // want this precomputed so that url is functional - cacheState = .local - remoteStatus = .pending } override public func prepareForDeletion() { From c8a7009bb612101f0a853fb1234b2c40a3e6d958 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 25 Apr 2022 15:30:11 -0700 Subject: [PATCH 175/203] use FileManager to find, and create, user paths --- Source/Classes/Caching/CloudCoreCacheable.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheable.swift b/Source/Classes/Caching/CloudCoreCacheable.swift index 9fb07a31..456070e0 100644 --- a/Source/Classes/Caching/CloudCoreCacheable.swift +++ b/Source/Classes/Caching/CloudCoreCacheable.swift @@ -85,14 +85,13 @@ public extension CloudCoreCacheable { } } - var urlPath: String { - let cacheDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first - - return cacheDirectory! + "/" + recordName! + "." + (suffix ?? "") - } - var url: URL { - return URL(fileURLWithPath: urlPath) + let fileName = recordName! + (suffix ?? "") + + var cacheDirectory = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + cacheDirectory.appendPathComponent(fileName) + + return cacheDirectory } } From bdcd8b66effae710868b754d56435d3f7dd2180b Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 27 Apr 2022 19:15:02 -0700 Subject: [PATCH 176/203] when deleting cacheable, try to cancel them too --- Source/Classes/Caching/CloudCoreCacheManager.swift | 14 ++++++++++++-- Source/Classes/Push/CoreDataObserver.swift | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index c592ecff..1686e6dc 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -18,7 +18,7 @@ class CloudCoreCacheManager: NSObject { private let container: CKContainer private let cacheableClassNames: [String] - private var frcs: [AnyObject] = [] + private var frcs: [NSFetchedResultsController] = [] public init(persistentContainer: NSPersistentContainer) { self.persistentContainer = persistentContainer @@ -111,6 +111,7 @@ class CloudCoreCacheManager: NSObject { context.perform { for name in self.cacheableClassNames { + // restore existing ops let uploading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.uploading.rawValue) let downloading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.downloading.rawValue) let existing = NSCompoundPredicate(orPredicateWithSubpredicates: [uploading, downloading]) @@ -120,6 +121,7 @@ class CloudCoreCacheManager: NSObject { self.process(cacheables: cacheables) } + // restart failed uploads let hasError = NSPredicate(format: "%K != nil", "lastErrorMessage") let isLocal = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.local.rawValue) let failedToUpload = NSCompoundPredicate(orPredicateWithSubpredicates: [hasError, isLocal]) @@ -147,7 +149,7 @@ class CloudCoreCacheManager: NSObject { // return } - foundOperation = operation as? CKModifyRecordsOperation + foundOperation = operation semaphore.signal() } @@ -289,6 +291,14 @@ class CloudCoreCacheManager: NSObject { } } + public func cancelOperations(with operationIDs: [String]) { + for operationID in operationIDs { + if let op = findLongLivedOperation(with: operationID) { + op.cancel() + } + } + } + } extension CloudCoreCacheManager: NSFetchedResultsControllerDelegate { diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 55b56d2f..ba12ff82 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -176,6 +176,7 @@ class CoreDataObserver { var insertedObjects = Set() var updatedObject = Set() var deletedRecordIDs: [RecordIDWithDatabase] = [] + var operationIDs: [String] = [] for change in changes { switch change.changeType { @@ -208,6 +209,9 @@ class CoreDataObserver { let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.publicCloudDatabase) deletedRecordIDs.append(recordIDWithDatabase) } + if let operationID = change.tombstone!["operationID"] as? String { + operationIDs.append(operationID) + } } default: @@ -245,6 +249,10 @@ class CoreDataObserver { } } } + + if !operationIDs.isEmpty { + CloudCore.cacheManager?.cancelOperations(with: operationIDs) + } } return success From 958deddeeabaead46f61cd27ff8c8372fa1f8a1c Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 9 May 2022 20:09:12 -0700 Subject: [PATCH 177/203] return pull results in callback --- Source/Classes/CloudCore.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 9b0a91e7..c62b2949 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -175,10 +175,16 @@ open class CloudCore { - error: block will be called every time when error occurs during process - completion: `PullResult` enumeration with results of operation */ - public static func pull(to container: NSPersistentContainer, error: ErrorBlock?, completion: (() -> Void)?) { + public static func pull(to container: NSPersistentContainer, error: ErrorBlock?, completion: ((_ fetchResult: PullResult) -> Void)?) { + var hadError = false let operation = PullChangesOperation(persistentContainer: container) - operation.errorBlock = error - operation.completionBlock = completion + operation.errorBlock = { + hadError = true + error?($0) + } + operation.completionBlock = { + completion?(hadError ? PullResult.failed : PullResult.newData) + } queue.addOperation(operation) } From de85998f7bd99e8bf840368e2192001d1f2d58cb Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 10 May 2022 09:56:10 -0700 Subject: [PATCH 178/203] (fix a typo in ReadMe) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 246ef940..70bf8e0c 100755 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ You can designate attributes in your managed objects to be masked during upload * `download` = ignored during fetch operations * `upload,download` = both -##Cacheable Assets +## Cacheable Assets By default, CloudCore will transform assets in your CloudKit records into binary data attributes in your Core Data objects. But when you're working with very large files, such as photos, audio, or video, this default mode isn't optimal. From a24f8773ce8d56d5b0252fef61581e94fdb8a8e9 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 10 May 2022 10:01:24 -0700 Subject: [PATCH 179/203] (more ReadMe edits for clarity) --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 70bf8e0c..f8f895f8 100755 --- a/README.md +++ b/README.md @@ -231,13 +231,15 @@ public extension CloudCoreCacheable { Once you've configured your Core Data schema to support cacheable assets, you can create and download them as needed. -When you create a new cacheable managed object, you must store its data at the file URL before saving it. The default value of cacheState is "local" and the default value of remoteStatus is "pending". Once CloudCore pushes the new cacheable record, it sets the cacheState to "upload", which triggers a long-lived upload operation. On completion, the cacheable managed object will have its cacheState set to "cached" and its remoteStatus set to "available". +When you create a new cacheable managed object, you must store its data at the file URL before saving it. The default value of cacheState is "local" and the default value of remoteStatus is "pending". Once CloudCore pushes the new cacheable record, it sets the cacheState to "upload", which triggers a long-lived modify operation. On completion, the cacheable managed object will have its cacheState set to "cached" and its remoteStatus set to "available". -When cacheable records are pulled from CloudKit, the asset field is ignored (because it is masked), and the cacheState will be "remote". When the remoteStatus is "available", you can trigger a download by setting the cacheState to "download" and saving the object. Once completed, the cacheable object will have its cacheState set to "cached", and the data will be locally available at the file URL. +When cacheable records are pulled from CloudKit, the asset field is ignored (because it is masked), and the cacheState will be "remote". When the remoteStatus is "available", you can trigger a long-lived fetch operation by setting the cacheState to "download" and saving the object. Once completed, the cacheable object will have its cacheState set to "cached", and the data will be locally available at the file URL. -Note that cacheState represents a state machine. +Note that cacheState represents a state machine. +``` (**new**) => local -> (push) -> upload -> uploading -> cached (pull) => remote -> **download** -> downloading -> cached +``` ### Important See the Example app for specific details. Note, specifically, that I **need to override awakeFromInsert and prepareForDeletion** for my cacheable managed object type Datafile. If anyone has ideas on how to push this critical implementation detail into CloudCore itself, let me know! From cc5ec9ed5805ace369e4e36c43ec0e9a826405c5 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 10 May 2022 10:04:17 -0700 Subject: [PATCH 180/203] (updated authors attributions) --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8f895f8..32456867 100755 --- a/README.md +++ b/README.md @@ -328,7 +328,11 @@ To run them you need to: ## Authors deeje cooley, [deeje.com](http://www.deeje.com/) -- added NSPersistentHistory and CloudKit Sharing Support +- refactored into Pull/Push termonology +- added offline sync via NSPersistentHistory +- added CloudKit Sharing support +- added Maskable Attributes +- added Cacheable Assets Vasily Ulianov, [va...@me.com](http://www.google.com/recaptcha/mailhide/d?k=01eFEpy-HM-qd0Vf6QGABTjw==&c=JrKKY2bjm0Bp58w7zTvPiQ==) Open for hire / relocation. From 6f2e983a32424c713b37da25a916ca431f979d7f Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 10 May 2022 10:13:39 -0700 Subject: [PATCH 181/203] (update comparison w NSPersistentCloudKitCOntainer) --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 32456867..b6284801 100755 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ #### CloudCore vs NSPersistentCloudKitContainer? -NSPersistentCloudKitContainer provides native support for Core Data <-> CloudKit synchronization. Here are some thoughts on the differences between these two approaches. +NSPersistentCloudKitContainer provides native support for Core Data <-> CloudKit synchronization. Here are some thoughts on the differences between these two approaches, as of May 2022. ###### NSPersistentCloudKitContainer * Simple to enable @@ -36,6 +36,7 @@ NSPersistentCloudKitContainer provides native support for Core Data <-> CloudKit * All Core Data names are preceeded with "CD_" in CloudKit * Core Data Relationships are mapped thru CDMR records in CloudKit * Sharing is supported via zones +* No(?) long-lived operations support for large file upload/download ###### CloudCore * Support requires specific configuration in the Core Data Model @@ -46,13 +47,14 @@ NSPersistentCloudKitContainer provides native support for Core Data <-> CloudKit * Offline Synchronziation via NSPersistentHistoryTracking * Core Data names are mapped exactly in CloudKit * Core Data Relationships are mapped to CloudKit CKReferences -* Maskable Attributes provides fine-grain control over local-only data and manually managed remote data. +* Maskable Attributes provides fine-grain control over local-only data and manually managed remote data * Sharing is supported via root records +* Supports upload/download of large data files via long-lived operations, with proper schema configuration -During their WWDC presentation, Apple very clearly stated that NSPersistentCloudKitContainer is a foundation for future support of more advanced features #YMMV +Apple very clearly states that NSPersistentCloudKitContainer is a foundation for future support of more advanced features. I'm still waiting to learn which first-party apps use it. #YMMV ## How it works? -CloudCore is built using a "black box" architecture, so it works invisibly for your application. You just need to add several lines to your `AppDelegate` to enable it, as well as identify various aspects of your Core Data Model schema. Synchronization and error resolving is managed automatically. +CloudCore is built using a "black box" architecture, so it works fairly invisibly for your application. You just need to add several lines to your `AppDelegate` to enable it, as well as identify various aspects of your Core Data Model schema. Synchronization and error resolving is managed automatically. 1. CloudCore stores *change tokens* from CloudKit, so only changed data is downloaded. 2. When CloudCore is enabled (`CloudCore.enable`) it pulls changed data from CloudKit and subscribes to CloudKit push notifications about new changes. @@ -161,8 +163,6 @@ The most simple way is to name attributes with default names because you don't n ### Mapping via UserInfo You can map your own attributes to the required service attributes. For each attribute you want to map, add an item to the attribute's UserInfo, using the key `CloudCoreType` and following values: -* *Private Record Data* value is `privateRecordData`. -* *Public Record Data* value is `publicRecordData`. * *Record Name* value is `recordName`. * *Owner Name* value is `ownerName`. From ca363ffa82c5ce5dad15d5e681176943ec81c0b1 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 15 May 2022 14:20:24 -0700 Subject: [PATCH 182/203] implement CacheState.unload --- .../Caching/CloudCoreCacheManager.swift | 22 +++++++++++++++++-- .../Classes/Caching/CloudCoreCacheable.swift | 10 ++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 1686e6dc..61e37cbc 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -52,6 +52,8 @@ class CloudCoreCacheManager: NSObject { upload(cacheableID: cacheable.objectID) case .download, .downloading: download(cacheableID: cacheable.objectID) + case .unload: + unload(cacheableID: cacheable.objectID) default: break } @@ -84,7 +86,8 @@ class CloudCoreCacheManager: NSObject { for name in self.cacheableClassNames { let triggerUpload = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.upload.rawValue) let triggerDownload = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.download.rawValue) - let triggers = NSCompoundPredicate(orPredicateWithSubpredicates: [triggerUpload, triggerDownload]) + let triggerUnload = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.unload.rawValue) + let triggers = NSCompoundPredicate(orPredicateWithSubpredicates: [triggerUpload, triggerDownload, triggerUnload]) let triggerRequest = NSFetchRequest(entityName: name) triggerRequest.predicate = triggers @@ -291,6 +294,18 @@ class CloudCoreCacheManager: NSObject { } } + func unload(cacheableID: NSManagedObjectID) { + let context = backgroundContext + + context.perform { + guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } + + cacheable.removeLocal() + cacheable.cacheState = .remote + try? context.save() + } + } + public func cancelOperations(with operationIDs: [String]) { for operationID in operationIDs { if let op = findLongLivedOperation(with: operationID) { @@ -310,7 +325,10 @@ extension CloudCoreCacheManager: NSFetchedResultsControllerDelegate { newIndexPath: IndexPath?) { guard let cacheable = anObject as? CloudCoreCacheable else { return } - if cacheable.cacheState == .upload || cacheable.cacheState == .download { + if cacheable.cacheState == .upload + || cacheable.cacheState == .download + || cacheable.cacheState == .unload + { process(cacheables: [cacheable]) } } diff --git a/Source/Classes/Caching/CloudCoreCacheable.swift b/Source/Classes/Caching/CloudCoreCacheable.swift index 456070e0..b49e1abc 100644 --- a/Source/Classes/Caching/CloudCoreCacheable.swift +++ b/Source/Classes/Caching/CloudCoreCacheable.swift @@ -18,6 +18,8 @@ public enum CacheState: String { case downloading case cached + + case unload // -> remote } public enum RemoteStatus: String { @@ -65,7 +67,7 @@ public extension CloudCoreCacheable { } var localAvailable: Bool { - let availableStates: [CacheState] = [.local, .upload, .uploading, .cached] + let availableStates: [CacheState] = [.local, .upload, .uploading, .cached, .unload] return availableStates.contains(cacheState) } @@ -94,4 +96,10 @@ public extension CloudCoreCacheable { return cacheDirectory } + func removeLocal() { + if localAvailable { + try? FileManager.default.removeItem(at: url) + } + } + } From 6b49b5790a4a9d78d8c858a824a28d5563bb39c6 Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 15 May 2022 14:46:42 -0700 Subject: [PATCH 183/203] update example to call removeLocal() --- Example/Sources/Model/Datafile+CloudCoreCacheable.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift index 87f9d467..6525cb4e 100644 --- a/Example/Sources/Model/Datafile+CloudCoreCacheable.swift +++ b/Example/Sources/Model/Datafile+CloudCoreCacheable.swift @@ -19,9 +19,7 @@ extension Datafile: CloudCoreCacheable { } override public func prepareForDeletion() { - if localAvailable { - try? FileManager.default.removeItem(at: url) - } + removeLocal() } } From 3be6fdf2d1891ef4ba42c8feb22749ddcb77f802 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 18 May 2022 13:20:47 -0700 Subject: [PATCH 184/203] fix compiler errors in Example app --- .../Sources/View Controller/DetailViewController.swift | 10 ++++++++-- .../Sources/View Controller/MasterViewController.swift | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift index 04ef5a6f..fb819ff3 100644 --- a/Example/Sources/View Controller/DetailViewController.swift +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -116,7 +116,10 @@ extension DetailViewController: FRCTableViewDelegate { if let datafile = employee.datafiles?.allObjects.first as? Datafile { if datafile.localAvailable { - cell.photoImageView.image = UIImage(contentsOfFile: datafile.urlPath) + if let data = try? Data(contentsOf: datafile.url) + { + cell.photoImageView.image = UIImage(data: data) + } } else if datafile.readyToDownload { datafilesObserver.observeObject(object: datafile) { datafile, state in guard let cacheable = datafile as? CloudCoreCacheable else { return } @@ -125,7 +128,10 @@ extension DetailViewController: FRCTableViewDelegate { cell.progressView.progress = Float(cacheable.progress) if cacheable.localAvailable { - cell.photoImageView.image = UIImage(contentsOfFile: cacheable.urlPath) + if let data = try? Data(contentsOf: cacheable.url) + { + cell.photoImageView.image = UIImage(data: data) + } cell.progressView.isHidden = true cell.progressView.progress = 0 diff --git a/Example/Sources/View Controller/MasterViewController.swift b/Example/Sources/View Controller/MasterViewController.swift index 339e9c58..e5269eb9 100644 --- a/Example/Sources/View Controller/MasterViewController.swift +++ b/Example/Sources/View Controller/MasterViewController.swift @@ -44,7 +44,7 @@ class MasterViewController: UITableViewController { DispatchQueue.main.async { sender.endRefreshing() } - }) { + }) { _ in DispatchQueue.main.async { sender.endRefreshing() } From 764379e2f62e0155779a5922fe0105bcc411c092 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 18 May 2022 13:25:30 -0700 Subject: [PATCH 185/203] update version to 5.0.2 --- CloudCore.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 88afae75..21c72ca7 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "5.0.0" + s.version = "5.0.2" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } From a1d42813f7f7bb1b2468c34c6ae54a895aff2119 Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 27 May 2022 10:40:34 -0700 Subject: [PATCH 186/203] progress only increases, qos is configurable --- .../Classes/Caching/CloudCoreCacheManager.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 61e37cbc..cfa22689 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -161,11 +161,11 @@ class CloudCoreCacheManager: NSObject { return foundOperation } - func longLivedConfiguration() -> CKOperation.Configuration { + func longLivedConfiguration(qos: QualityOfService) -> CKOperation.Configuration { let configuration = CKOperation.Configuration() configuration.container = container configuration.isLongLived = true - configuration.qualityOfService = .utility + configuration.qualityOfService = qos return configuration } @@ -190,7 +190,7 @@ class CloudCoreCacheManager: NSObject { record["remoteStatusRaw"] = RemoteStatus.available.rawValue modifyOp = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: nil) - modifyOp.configuration = self.longLivedConfiguration() + modifyOp.configuration = self.longLivedConfiguration(qos: .utility) modifyOp.savePolicy = .changedKeys cacheable.operationID = modifyOp.operationID @@ -198,7 +198,9 @@ class CloudCoreCacheManager: NSObject { modifyOp.perRecordProgressBlock = { record, progress in self.update([cacheableID]) { cacheable in - cacheable.uploadProgress = progress + if progress > cacheable.uploadProgress { + cacheable.uploadProgress = progress + } } } modifyOp.perRecordCompletionBlock = { record, error in @@ -247,7 +249,7 @@ class CloudCoreCacheManager: NSObject { guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } fetchOp = CKFetchRecordsOperation(recordIDs: [record.recordID]) - fetchOp.configuration = self.longLivedConfiguration() + fetchOp.configuration = self.longLivedConfiguration(qos: .userInitiated) fetchOp.desiredKeys = [cacheable.assetFieldName] cacheable.operationID = fetchOp.operationID @@ -255,7 +257,9 @@ class CloudCoreCacheManager: NSObject { fetchOp.perRecordProgressBlock = { record, progress in self.update([cacheableID]) { cacheable in - cacheable.downloadProgress = progress + if progress > cacheable.downloadProgress { + cacheable.downloadProgress = progress + } } } fetchOp.perRecordCompletionBlock = { record, recordID, error in From 5cb19d118a339e095cda5f6d575d65c6994816cb Mon Sep 17 00:00:00 2001 From: deeje Date: Fri, 27 May 2022 10:54:54 -0700 Subject: [PATCH 187/203] bump podspec version to 5.1.0 --- CloudCore.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CloudCore.podspec b/CloudCore.podspec index 21c72ca7..16910e01 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" s.summary = "Framework that enables synchronization between CloudKit and Core Data." - s.version = "5.0.2" + s.version = "5.1.0" s.homepage = "https://github.com/deeje/CloudCore" s.license = 'MIT' s.author = { "deeje" => "deeje@mac.com", "Vasily Ulianov" => "vasily@me.com" } From a83107a4c99dd0c8ad544d96040584e5a9161911 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 17:40:17 -0700 Subject: [PATCH 188/203] always use persistentHistory, reduce churn on rapid saves remove old code paths --- Source/Classes/CloudCore.swift | 2 +- Source/Classes/Push/CoreDataObserver.swift | 295 ++++++++++----------- 2 files changed, 138 insertions(+), 159 deletions(-) diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index c62b2949..fc7ab90e 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -248,7 +248,7 @@ open class CloudCore { // Zone wasn't found, we need to create it self.queue.cancelAllOperations() - let setupOperation = SetupOperation(container: container, uploadAllData: !(coreDataObserver?.usePersistentHistoryForPush)!) + let setupOperation = SetupOperation(container: container, uploadAllData: true) // arg, why is this a question?! // for completeness, pull again let pullOperation = PullChangesOperation(persistentContainer: container) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index ba12ff82..7690c9d9 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -24,32 +24,31 @@ class CoreDataObserver { // Used for errors delegation weak var delegate: CloudCoreDelegate? - var usePersistentHistoryForPush = false var isOnline = true { didSet { - if isOnline != oldValue && isOnline == true && usePersistentHistoryForPush == true { + if isOnline != oldValue && isOnline == true { processPersistentHistory() } } } + var processTimer: Timer? + public init(container: NSPersistentContainer) { self.container = container converter.errorBlock = { [weak self] in self?.delegate?.error(error: $0, module: .some(.pushToCloud)) } - if #available(iOS 11.0, watchOS 4.0, tvOS 11.0, OSX 10.13, *) { - let storeDescription = container.persistentStoreDescriptions.first - if let persistentHistoryNumber = storeDescription?.options[NSPersistentHistoryTrackingKey] as? NSNumber - { - usePersistentHistoryForPush = persistentHistoryNumber.boolValue - } - - if usePersistentHistoryForPush { - processPersistentHistory() - } + var usePersistentHistoryForPush = false + if let storeDescription = container.persistentStoreDescriptions.first, + let persistentHistoryNumber = storeDescription.options[NSPersistentHistoryTrackingKey] as? NSNumber + { + usePersistentHistoryForPush = persistentHistoryNumber.boolValue } + assert(usePersistentHistoryForPush) + + processPersistentHistory() } /// Observe Core Data willSave and didSave notifications @@ -122,176 +121,156 @@ class CoreDataObserver { return success } - @objc private func willSave(notification: Notification) { - guard let context = notification.object as? NSManagedObjectContext else { return } - guard shouldProcess(context) else { return } + func process(_ transaction: NSPersistentHistoryTransaction, in moc: NSManagedObjectContext) -> Bool { + var success = true + + if transaction.contextName != CloudCore.config.pushContextName { return success } - if usePersistentHistoryForPush { - context.insertedObjects.forEach { (inserted) in - if let serviceAttributeNames = inserted.entity.serviceAttributeNames { - for scope in serviceAttributeNames.scopes { - let _ = try? inserted.setRecordInformation(for: scope) + if let changes = transaction.changes { + var insertedObjects = Set() + var updatedObject = Set() + var deletedRecordIDs: [RecordIDWithDatabase] = [] + var operationIDs: [String] = [] + + for change in changes { + switch change.changeType { + case .insert: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + insertedObjects.insert(inserted) + } + + case .update: + if let inserted = try? moc.existingObject(with: change.changedObjectID) { + if let updatedProperties = change.updatedProperties { + let updatedPropertyNames: [String] = updatedProperties.map { (propertyDescription) in + return propertyDescription.name + } + inserted.updatedPropertyNames = updatedPropertyNames + } + updatedObject.insert(inserted) } + + case .delete: + if change.tombstone != nil { + if let privateRecordData = change.tombstone!["privateRecordData"] as? Data { + let ckRecord = CKRecord(archivedData: privateRecordData) + let database = ckRecord?.recordID.zoneID.ownerName == CKCurrentUserDefaultName ? CloudCore.config.container.privateCloudDatabase : CloudCore.config.container.sharedCloudDatabase + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, database) + deletedRecordIDs.append(recordIDWithDatabase) + } + if let publicRecordData = change.tombstone!["publicRecordData"] as? Data { + let ckRecord = CKRecord(archivedData: publicRecordData) + let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.publicCloudDatabase) + deletedRecordIDs.append(recordIDWithDatabase) + } + if let operationID = change.tombstone!["operationID"] as? String { + operationIDs.append(operationID) + } + } + + default: + break } } - } else { - converter.prepareOperationsFor(inserted: context.insertedObjects, - updated: context.updatedObjects, - deleted: context.deletedObjects) - } - } - - @objc private func didSave(notification: Notification) { - guard let context = notification.object as? NSManagedObjectContext else { return } - guard shouldProcess(context) else { return } - - if usePersistentHistoryForPush == true { - DispatchQueue.main.async { [weak self] in - guard let observer = self else { return } - observer.processPersistentHistory() + + self.converter.prepareOperationsFor(inserted: insertedObjects, + updated: updatedObject, + deleted: deletedRecordIDs) + + try? moc.save() + + if self.converter.hasPendingOperations { + success = self.processChanges() + } + + // check for cached assets + if success == true { + let insertedIDs = insertedObjects.map { $0.objectID } + + for insertedID in insertedIDs { + guard let cacheable = try? moc.existingObject(with: insertedID) as? CloudCoreCacheable, + cacheable.cacheState == .local + else { continue } + + cacheable.cacheState = .upload + } + + try? moc.save() } - } else { - guard converter.hasPendingOperations else { return } - DispatchQueue.global(qos: .utility).async { [weak self] in - guard let observer = self else { return } - _ = observer.processChanges() + if !operationIDs.isEmpty { + CloudCore.cacheManager?.cancelOperations(with: operationIDs) } } - } - func processPersistentHistory() { + return success + } + + @objc func processPersistentHistory() { #if os(iOS) guard isOnline else { return } #endif - if #available(iOS 11.0, watchOSApplicationExtension 4.0, tvOS 11.0, OSX 10.13, *) { + container.performBackgroundTask { moc in + moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - func process(_ transaction: NSPersistentHistoryTransaction, in moc: NSManagedObjectContext) -> Bool { - var success = true - - if transaction.contextName != CloudCore.config.pushContextName { return success } + let settings = UserDefaults.standard + do { + var token: NSPersistentHistoryToken? = nil + if let data = settings.object(forKey: CloudCore.config.persistentHistoryTokenKey) as? Data { + token = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken + } + let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) + let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult - if let changes = transaction.changes { - var insertedObjects = Set() - var updatedObject = Set() - var deletedRecordIDs: [RecordIDWithDatabase] = [] - var operationIDs: [String] = [] - - for change in changes { - switch change.changeType { - case .insert: - if let inserted = try? moc.existingObject(with: change.changedObjectID) { - insertedObjects.insert(inserted) - } - - case .update: - if let inserted = try? moc.existingObject(with: change.changedObjectID) { - if let updatedProperties = change.updatedProperties { - let updatedPropertyNames: [String] = updatedProperties.map { (propertyDescription) in - return propertyDescription.name - } - inserted.updatedPropertyNames = updatedPropertyNames - } - updatedObject.insert(inserted) - } - - case .delete: - if change.tombstone != nil { - if let privateRecordData = change.tombstone!["privateRecordData"] as? Data { - let ckRecord = CKRecord(archivedData: privateRecordData) - let database = ckRecord?.recordID.zoneID.ownerName == CKCurrentUserDefaultName ? CloudCore.config.container.privateCloudDatabase : CloudCore.config.container.sharedCloudDatabase - let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, database) - deletedRecordIDs.append(recordIDWithDatabase) - } - if let publicRecordData = change.tombstone!["publicRecordData"] as? Data { - let ckRecord = CKRecord(archivedData: publicRecordData) - let recordIDWithDatabase = RecordIDWithDatabase((ckRecord?.recordID)!, CloudCore.config.container.publicCloudDatabase) - deletedRecordIDs.append(recordIDWithDatabase) - } - if let operationID = change.tombstone!["operationID"] as? String { - operationIDs.append(operationID) - } - } + if let history = historyResult.result as? [NSPersistentHistoryTransaction] { + for transaction in history { + if self.process(transaction, in: moc) { + let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) + try moc.execute(deleteRequest) - default: + let data = try NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) + settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) + } else { break } } - - self.converter.prepareOperationsFor(inserted: insertedObjects, - updated: updatedObject, - deleted: deletedRecordIDs) - - try? moc.save() - - if self.converter.hasPendingOperations { - success = self.processChanges() - } - - // check for cached assets - if success == true { - let insertedIDs = insertedObjects.map { $0.objectID } - container.performBackgroundTask { moc in - moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - do { - for insertedID in insertedIDs { - guard let cacheable = try moc.existingObject(with: insertedID) as? CloudCoreCacheable, - cacheable.cacheState == .local - else { continue } - - cacheable.cacheState = .upload - } - - try moc.save() - } catch { - self.delegate?.error(error: error, module: .some(.pushToCloud)) - } - } - } - - if !operationIDs.isEmpty { - CloudCore.cacheManager?.cancelOperations(with: operationIDs) - } } - - return success + } catch { + let nserror = error as NSError + switch nserror.code { + case NSPersistentHistoryTokenExpiredError: + settings.set(nil, forKey: CloudCore.config.persistentHistoryTokenKey) + default: + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } } - - container.performBackgroundTask { (moc) in - let settings = UserDefaults.standard - do { - var token: NSPersistentHistoryToken? = nil - if let data = settings.object(forKey: CloudCore.config.persistentHistoryTokenKey) as? Data { - token = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken - } - let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) - let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult - - if let history = historyResult.result as? [NSPersistentHistoryTransaction] { - for transaction in history { - if process(transaction, in: moc) { - let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) - try moc.execute(deleteRequest) - - let data = try NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) - settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) - } else { - break - } - } - } - } catch { - let nserror = error as NSError - switch nserror.code { - case NSPersistentHistoryTokenExpiredError: - settings.set(nil, forKey: CloudCore.config.persistentHistoryTokenKey) - default: - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } + } + } + + @objc private func willSave(notification: Notification) { + guard let context = notification.object as? NSManagedObjectContext else { return } + guard shouldProcess(context) else { return } + + context.insertedObjects.forEach { (inserted) in + if let serviceAttributeNames = inserted.entity.serviceAttributeNames { + for scope in serviceAttributeNames.scopes { + let _ = try? inserted.setRecordInformation(for: scope) } } } + } + + @objc private func didSave(notification: Notification) { + guard let context = notification.object as? NSManagedObjectContext else { return } + guard shouldProcess(context) else { return } + + DispatchQueue.main.async { + self.processTimer?.invalidate() + self.processTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + self.processPersistentHistory() + } + } } private func handle(error: Error, parentContext: NSManagedObjectContext) { From 32ed3f96e0506886b175c62f9300af35a4cb3c97 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 17:41:08 -0700 Subject: [PATCH 189/203] need to update removeStatus locally to stay in sync --- Source/Classes/Caching/CloudCoreCacheManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index cfa22689..25350205 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -207,6 +207,7 @@ class CloudCoreCacheManager: NSObject { self.update([cacheableID]) { cacheable in cacheable.uploadProgress = 0 cacheable.cacheState = (error == nil) ? .cached : .local + cacheable.remoteStatus = (error == nil) ? .available : .pending cacheable.lastErrorMessage = error?.localizedDescription } From 5ab9a31dac5627105a03a4da2972ae2aeaf36e7b Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 18:31:50 -0700 Subject: [PATCH 190/203] simplify the .upload triggering --- Source/Classes/Push/CoreDataObserver.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 7690c9d9..36e1085a 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -185,17 +185,17 @@ class CoreDataObserver { // check for cached assets if success == true { - let insertedIDs = insertedObjects.map { $0.objectID } - - for insertedID in insertedIDs { - guard let cacheable = try? moc.existingObject(with: insertedID) as? CloudCoreCacheable, - cacheable.cacheState == .local - else { continue } + moc.perform { + for insertedObject in insertedObjects { + guard let cacheable = insertedObject as? CloudCoreCacheable, + cacheable.cacheState == .local + else { continue } + + cacheable.cacheState = .upload + } - cacheable.cacheState = .upload + try? moc.save() } - - try? moc.save() } if !operationIDs.isEmpty { From 3e56f14dc2359de6e9de92e588824b557678c352 Mon Sep 17 00:00:00 2001 From: deeje Date: Sat, 28 May 2022 18:32:09 -0700 Subject: [PATCH 191/203] increase delay in sync to 5 sec after last save --- Source/Classes/Push/CoreDataObserver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 36e1085a..02ecdc09 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -267,7 +267,7 @@ class CoreDataObserver { DispatchQueue.main.async { self.processTimer?.invalidate() - self.processTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + self.processTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in self.processPersistentHistory() } } From d1e0855577ac356c2992f902d9b7e5c0dcfd788e Mon Sep 17 00:00:00 2001 From: deeje Date: Sun, 29 May 2022 09:54:32 -0700 Subject: [PATCH 192/203] cacheable ops can already be in running --- .../Caching/CloudCoreCacheManager.swift | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 25350205..2e278178 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -215,7 +215,7 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheToCloud) if let cloudError = error as? CKError, - cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, +// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { @@ -226,10 +226,16 @@ class CloudCoreCacheManager: NSObject { } modifyOp.modifyRecordsCompletionBlock = { records, recordIDs, error in } modifyOp.longLivedOperationWasPersistedBlock = { } - container.privateCloudDatabase.add(modifyOp) + if !modifyOp.isExecuting { + container.privateCloudDatabase.add(modifyOp) + } - cacheable.cacheState = .uploading - try? context.save() + if cacheable.cacheState != .uploading { + cacheable.cacheState = .uploading + } + if context.hasChanges { + try? context.save() + } } } @@ -282,7 +288,7 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheFromCloud) if let cloudError = error as? CKError, - cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, +// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { @@ -292,10 +298,16 @@ class CloudCoreCacheManager: NSObject { } } fetchOp.longLivedOperationWasPersistedBlock = { } - container.privateCloudDatabase.add(fetchOp) + if !fetchOp.isExecuting { + container.privateCloudDatabase.add(fetchOp) + } - cacheable.cacheState = .downloading - try? context.save() + if cacheable.cacheState != .downloading { + cacheable.cacheState = .downloading + } + if context.hasChanges { + try? context.save() + } } } From 15c51735fd5a770fc2ca590431d0508604062d70 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 15:09:53 -0700 Subject: [PATCH 193/203] restart all long-lived ops if one errors out --- Source/Classes/Caching/CloudCoreCacheManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 2e278178..cd568588 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -219,7 +219,7 @@ class CloudCoreCacheManager: NSObject { let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { - self.upload(cacheableID: cacheableID) + self.restoreDanglingOperations() } } } From cacc1221d8ed0d9d28677ab561fd2b1a93c1642b Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 16:01:40 -0700 Subject: [PATCH 194/203] if CKErrorRetryAfterKey, wait to push more changes --- Source/Classes/Push/CoreDataObserver.swift | 63 ++++++++++++++++------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 02ecdc09..e820ea2f 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -19,7 +19,13 @@ class CoreDataObserver { static let syncContextName = "CloudCoreSync" - let processSemaphore = DispatchSemaphore(value: 1) + var processContext: NSManagedObjectContext! + static let processContextName = "CloudCoreHistory" + var processTimer: Timer? + var pauseUntil: Date? + + var isProcessing = false + var processAgain = true // Used for errors delegation weak var delegate: CloudCoreDelegate? @@ -32,8 +38,6 @@ class CoreDataObserver { } } - var processTimer: Timer? - public init(container: NSPersistentContainer) { self.container = container converter.errorBlock = { [weak self] in @@ -48,6 +52,11 @@ class CoreDataObserver { } assert(usePersistentHistoryForPush) + processContext = container.newBackgroundContext() + processContext.name = CoreDataObserver.processContextName + processContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + processContext.automaticallyMergesChangesFromParent = true + processPersistentHistory() } @@ -83,11 +92,6 @@ class CoreDataObserver { } func processChanges() -> Bool { - processSemaphore.wait() - defer { - processSemaphore.signal() - } - var success = true CloudCore.delegate?.willSyncToCloud() @@ -211,9 +215,17 @@ class CoreDataObserver { guard isOnline else { return } #endif - container.performBackgroundTask { moc in - moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + if isProcessing { + processAgain = true + return + } + + let backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CloudCore.processPersistentHistory") + + isProcessing = true + + processContext.perform { let settings = UserDefaults.standard do { var token: NSPersistentHistoryToken? = nil @@ -221,13 +233,13 @@ class CoreDataObserver { token = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSPersistentHistoryToken.classForKeyedUnarchiver()], from: data) as? NSPersistentHistoryToken } let historyRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token) - let historyResult = try moc.execute(historyRequest) as! NSPersistentHistoryResult + let historyResult = try self.processContext.execute(historyRequest) as! NSPersistentHistoryResult if let history = historyResult.result as? [NSPersistentHistoryTransaction] { for transaction in history { - if self.process(transaction, in: moc) { + if self.process(transaction, in: self.processContext) { let deleteRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: transaction) - try moc.execute(deleteRequest) + try self.processContext.execute(deleteRequest) let data = try NSKeyedArchiver.archivedData(withRootObject: transaction.token, requiringSecureCoding: false) settings.set(data, forKey: CloudCore.config.persistentHistoryTokenKey) @@ -245,6 +257,18 @@ class CoreDataObserver { fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } + + UIApplication.shared.endBackgroundTask(backgroundTask) + + DispatchQueue.main.async { + self.isProcessing = false + + if self.processAgain { + self.processAgain = false + + self.processPersistentHistory() + } + } } } @@ -265,9 +289,12 @@ class CoreDataObserver { guard let context = notification.object as? NSManagedObjectContext else { return } guard shouldProcess(context) else { return } + // we've been asked to retry later + if let date = pauseUntil, date.timeIntervalSinceNow > 0 { return } + DispatchQueue.main.async { self.processTimer?.invalidate() - self.processTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in + self.processTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in self.processPersistentHistory() } } @@ -280,13 +307,15 @@ class CoreDataObserver { } switch cloudError.code { - case .requestRateLimited, .zoneBusy: + case .requestRateLimited, .zoneBusy, .serviceUnavailable: pushOperationQueue.cancelAllOperations() if let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { + pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { [weak self] in - guard let observer = self else { return } - observer.processPersistentHistory() + guard let self = self else { return } + + self.processPersistentHistory() } } From 2ab656ec5d1b90fc03dd4c733bdd290320837d80 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 18:06:27 -0700 Subject: [PATCH 195/203] centralize the pause timer --- .../Caching/CloudCoreCacheManager.swift | 56 ++++++++++++------- Source/Classes/CloudCore.swift | 20 +++++++ Source/Classes/Push/CoreDataObserver.swift | 14 ++--- 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index cd568588..f6787bfe 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -41,7 +41,7 @@ class CloudCoreCacheManager: NSObject { super.init() - restoreDanglingOperations() + restartOperations() configureObservers() } @@ -109,18 +109,20 @@ class CloudCoreCacheManager: NSObject { } } - func restoreDanglingOperations() { + func restartOperations() { let context = backgroundContext context.perform { for name in self.cacheableClassNames { - // restore existing ops + // retart new & existing ops + let upload = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.upload.rawValue) let uploading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.uploading.rawValue) + let download = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.download.rawValue) let downloading = NSPredicate(format: "%K == %@", "cacheStateRaw", CacheState.downloading.rawValue) - let existing = NSCompoundPredicate(orPredicateWithSubpredicates: [uploading, downloading]) + let newOrExisting = NSCompoundPredicate(orPredicateWithSubpredicates: [upload, uploading, download, downloading]) let restoreRequest = NSFetchRequest(entityName: name) - restoreRequest.predicate = existing - if let cacheables = try? context.fetch(restoreRequest) as? [CloudCoreCacheable] { + restoreRequest.predicate = newOrExisting + if let cacheables = try? context.fetch(restoreRequest) as? [CloudCoreCacheable], !cacheables.isEmpty { self.process(cacheables: cacheables) } @@ -130,7 +132,7 @@ class CloudCoreCacheManager: NSObject { let failedToUpload = NSCompoundPredicate(orPredicateWithSubpredicates: [hasError, isLocal]) let restartRequest = NSFetchRequest(entityName: name) restartRequest.predicate = failedToUpload - if let cacheables = try? context.fetch(restartRequest) as? [CloudCoreCacheable] { + if let cacheables = try? context.fetch(restartRequest) as? [CloudCoreCacheable], !cacheables.isEmpty { let cacheableIDs = cacheables.map { $0.objectID } self.update(cacheableIDs) { cacheable in cacheable.lastErrorMessage = nil @@ -171,6 +173,11 @@ class CloudCoreCacheManager: NSObject { } func upload(cacheableID: NSManagedObjectID) { + // we've been asked to retry later + if let date = CloudCore.pauseUntil, + date.timeIntervalSinceNow > 0 + { return } + let container = container let context = backgroundContext @@ -186,6 +193,19 @@ class CloudCoreCacheManager: NSObject { { guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } + /* + var newRecord: CKRecord? + let semaphore = DispatchSemaphore(value: 0) + container.privateCloudDatabase.fetch(withRecordID: record.recordID) { record, error in + newRecord = record + + semaphore.signal() + } + semaphore.wait() + + guard let record = newRecord else { return } + */ + record[cacheable.assetFieldName] = CKAsset(fileURL: cacheable.url) record["remoteStatusRaw"] = RemoteStatus.available.rawValue @@ -215,12 +235,9 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheToCloud) if let cloudError = error as? CKError, -// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { - self.restoreDanglingOperations() - } + CloudCore.pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) } } } @@ -240,6 +257,11 @@ class CloudCoreCacheManager: NSObject { } func download(cacheableID: NSManagedObjectID) { + // we've been asked to retry later + if let date = CloudCore.pauseUntil, + date.timeIntervalSinceNow > 0 + { return } + let container = container let context = backgroundContext @@ -288,12 +310,9 @@ class CloudCoreCacheManager: NSObject { CloudCore.delegate?.error(error: error, module: .cacheFromCloud) if let cloudError = error as? CKError, -// cloudError.code == .requestRateLimited || cloudError.code == .zoneBusy, let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { - self.download(cacheableID: cacheableID) - } + CloudCore.pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) } } } @@ -312,14 +331,9 @@ class CloudCoreCacheManager: NSObject { } func unload(cacheableID: NSManagedObjectID) { - let context = backgroundContext - - context.perform { - guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } - + update([cacheableID]) { cacheable in cacheable.removeLocal() cacheable.cacheState = .remote - try? context.save() } } diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index fc7ab90e..3e5eef1c 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -85,6 +85,26 @@ open class CloudCore { return q }() + // if CloudKit says to retry later… + private static var pauseTimer: Timer? + static var pauseUntil: Date? { + didSet { + DispatchQueue.main.async { + CloudCore.pauseTimer?.invalidate() + if let fireDate = CloudCore.pauseUntil { + let interval = fireDate.timeIntervalSinceNow + print("pausing for \(interval) seconds") + CloudCore.pauseTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { timer in + CloudCore.pauseUntil = nil + + CloudCore.coreDataObserver?.processPersistentHistory() + CloudCore.cacheManager?.restartOperations() + } + } + } + } + } + // MARK: - Methods /// Enable CloudKit and Core Data synchronization diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index e820ea2f..2d95285d 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -22,7 +22,6 @@ class CoreDataObserver { var processContext: NSManagedObjectContext! static let processContextName = "CloudCoreHistory" var processTimer: Timer? - var pauseUntil: Date? var isProcessing = false var processAgain = true @@ -290,11 +289,13 @@ class CoreDataObserver { guard shouldProcess(context) else { return } // we've been asked to retry later - if let date = pauseUntil, date.timeIntervalSinceNow > 0 { return } + if let date = CloudCore.pauseUntil, + date.timeIntervalSinceNow > 0 + { return } DispatchQueue.main.async { self.processTimer?.invalidate() - self.processTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in + self.processTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in self.processPersistentHistory() } } @@ -311,12 +312,7 @@ class CoreDataObserver { pushOperationQueue.cancelAllOperations() if let number = cloudError.userInfo[CKErrorRetryAfterKey] as? NSNumber { - pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(number.intValue)) { [weak self] in - guard let self = self else { return } - - self.processPersistentHistory() - } + CloudCore.pauseUntil = Date(timeIntervalSinceNow: number.doubleValue) } // Zone was accidentally deleted (NOT PURGED), we need to reupload all data accroding Apple Guidelines From 950234bceedd78f91def1d70fb6ca78df1dc7e54 Mon Sep 17 00:00:00 2001 From: deeje Date: Mon, 30 May 2022 18:47:56 -0700 Subject: [PATCH 196/203] share MOC across CoreDataObserver & CacheManager for processing the push of history and upload of cacheables --- .../Caching/CloudCoreCacheManager.swift | 18 +++++------- Source/Classes/CloudCore.swift | 19 +++++++++---- Source/Classes/Push/CoreDataObserver.swift | 28 ++++++++----------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index f6787bfe..3dd37aec 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -14,19 +14,15 @@ import Network class CloudCoreCacheManager: NSObject { private let persistentContainer: NSPersistentContainer - private let backgroundContext: NSManagedObjectContext + private let processContext: NSManagedObjectContext private let container: CKContainer private let cacheableClassNames: [String] private var frcs: [NSFetchedResultsController] = [] - public init(persistentContainer: NSPersistentContainer) { + public init(persistentContainer: NSPersistentContainer, processContext: NSManagedObjectContext) { self.persistentContainer = persistentContainer - - let backgroundContext = persistentContainer.newBackgroundContext() - backgroundContext.automaticallyMergesChangesFromParent = true - backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - self.backgroundContext = backgroundContext + self.processContext = processContext self.container = CloudCore.config.container @@ -80,7 +76,7 @@ class CloudCoreCacheManager: NSObject { } private func configureObservers() { - let context = backgroundContext + let context = processContext context.perform { for name in self.cacheableClassNames { @@ -110,7 +106,7 @@ class CloudCoreCacheManager: NSObject { } func restartOperations() { - let context = backgroundContext + let context = processContext context.perform { for name in self.cacheableClassNames { @@ -179,7 +175,7 @@ class CloudCoreCacheManager: NSObject { { return } let container = container - let context = backgroundContext + let context = processContext context.perform { guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } @@ -263,7 +259,7 @@ class CloudCoreCacheManager: NSObject { { return } let container = container - let context = backgroundContext + let context = processContext context.perform { guard let cacheable = try? context.existingObject(with: cacheableID) as? CloudCoreCacheable else { return } diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 3e5eef1c..03fa829a 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -51,6 +51,8 @@ open class CloudCore { // MARK: - Properties + private(set) static var processContext: NSManagedObjectContext! + private(set) static var coreDataObserver: CoreDataObserver? private(set) static var cacheManager: CloudCoreCacheManager? public static var isOnline: Bool { @@ -111,23 +113,30 @@ open class CloudCore { /// /// - Parameters: /// - container: `NSPersistentContainer` that will be used to save data - public static func enable(persistentContainer container: NSPersistentContainer) { + public static func enable(persistentContainer: NSPersistentContainer) { + // share a MOC between CoreDataObserver and CacheManager + let processContext = persistentContainer.newBackgroundContext() + processContext.name = "CloudCoreProcess" + processContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + processContext.automaticallyMergesChangesFromParent = true + self.processContext = processContext + // Listen for local changes - let observer = CoreDataObserver(container: container) + let observer = CoreDataObserver(persistentContainer: persistentContainer, processContext: processContext) observer.delegate = self.delegate observer.start() self.coreDataObserver = observer - self.cacheManager = CloudCoreCacheManager(persistentContainer: container) + self.cacheManager = CloudCoreCacheManager(persistentContainer: persistentContainer, processContext: processContext) // Subscribe (subscription may be outdated/removed) let subscribeOperation = SubscribeOperation() subscribeOperation.errorBlock = { - handle(subscriptionError: $0, container: container) + handle(subscriptionError: $0, container: persistentContainer) } // Fetch updated data (e.g. push notifications weren't received) - let pullOperation = PullChangesOperation(persistentContainer: container) + let pullOperation = PullChangesOperation(persistentContainer: persistentContainer) pullOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pullFromCloud)) } diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index 2d95285d..e2260487 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -12,15 +12,14 @@ import CloudKit /// Class responsible for taking action on Core Data changes class CoreDataObserver { - var container: NSPersistentContainer - + var persistentContainer: NSPersistentContainer + var processContext: NSManagedObjectContext + let converter = ObjectToRecordConverter() let pushOperationQueue = PushOperationQueue() - static let syncContextName = "CloudCoreSync" + static let pushContextName = "CloudCorePush" - var processContext: NSManagedObjectContext! - static let processContextName = "CloudCoreHistory" var processTimer: Timer? var isProcessing = false @@ -37,25 +36,22 @@ class CoreDataObserver { } } - public init(container: NSPersistentContainer) { - self.container = container + public init(persistentContainer: NSPersistentContainer, processContext: NSManagedObjectContext) { + self.persistentContainer = persistentContainer + self.processContext = processContext + converter.errorBlock = { [weak self] in self?.delegate?.error(error: $0, module: .some(.pushToCloud)) } var usePersistentHistoryForPush = false - if let storeDescription = container.persistentStoreDescriptions.first, + if let storeDescription = persistentContainer.persistentStoreDescriptions.first, let persistentHistoryNumber = storeDescription.options[NSPersistentHistoryTrackingKey] as? NSNumber { usePersistentHistoryForPush = persistentHistoryNumber.boolValue } assert(usePersistentHistoryForPush) - processContext = container.newBackgroundContext() - processContext.name = CoreDataObserver.processContextName - processContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - processContext.automaticallyMergesChangesFromParent = true - processPersistentHistory() } @@ -95,8 +91,8 @@ class CoreDataObserver { CloudCore.delegate?.willSyncToCloud() - let backgroundContext = container.newBackgroundContext() - backgroundContext.name = CoreDataObserver.syncContextName + let backgroundContext = persistentContainer.newBackgroundContext() + backgroundContext.name = CoreDataObserver.pushContextName let records = converter.processPendingOperations(in: backgroundContext) pushOperationQueue.errorBlock = { @@ -350,7 +346,7 @@ class CoreDataObserver { resetZoneOperations.append(subscribeOperation) // Upload all local data - let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) + let uploadOperation = PushAllLocalDataOperation(parentContext: parentContext, managedObjectModel: persistentContainer.managedObjectModel) uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.pushToCloud)) } uploadOperation.addDependency(createZoneOperation) resetZoneOperations.append(uploadOperation) From 4b93c612822766a7187112bba9ffce0faf2f7776 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 14:38:09 -0700 Subject: [PATCH 197/203] push operations are atomic --- Source/Classes/Push/PushOperationQueue.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Classes/Push/PushOperationQueue.swift b/Source/Classes/Push/PushOperationQueue.swift index b8a3083f..8e83ac9c 100644 --- a/Source/Classes/Push/PushOperationQueue.swift +++ b/Source/Classes/Push/PushOperationQueue.swift @@ -50,6 +50,7 @@ class PushOperationQueue: OperationQueue { modifyRecords.database = database modifyRecords.savePolicy = .changedKeys modifyRecords.qualityOfService = .userInitiated + modifyRecords.isAtomic = true modifyRecords.perRecordCompletionBlock = { record, error in if let error = error { From 997898de411783cce97db3dbf7808dcbeae619a2 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 15:01:04 -0700 Subject: [PATCH 198/203] rapid inserts could cause converter to fail why use yet another background context?! --- Source/Classes/Push/CoreDataObserver.swift | 1 + .../ObjectToRecord/ObjectToRecordConverter.swift | 2 +- .../ObjectToRecord/ObjectToRecordOperation.swift | 14 +++++--------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index e2260487..e9e46a30 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -93,6 +93,7 @@ class CoreDataObserver { let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.name = CoreDataObserver.pushContextName + backgroundContext.automaticallyMergesChangesFromParent = true let records = converter.processPendingOperations(in: backgroundContext) pushOperationQueue.errorBlock = { diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift index 06d5c6d5..bbfee950 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordConverter.swift @@ -123,7 +123,7 @@ class ObjectToRecordConverter { /// - attention: Don't call this method from same context's `perfom`, that will cause deadlock func processPendingOperations(in context: NSManagedObjectContext) -> (recordsToSave: [RecordWithDatabase], recordIDsToDelete: [RecordIDWithDatabase]) { for operation in pendingConvertOperations { - operation.parentContext = context + operation.managedObjectContext = context operationQueue.addOperation(operation) } diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index 20556a6a..c51f2e39 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -10,8 +10,7 @@ import CloudKit import CoreData class ObjectToRecordOperation: Operation { - /// Need to set before starting operation, child context from it will be created - var parentContext: NSManagedObjectContext? + var managedObjectContext: NSManagedObjectContext? // Set on init let scope: CKDatabase.Scope @@ -37,7 +36,7 @@ class ObjectToRecordOperation: Operation { override func main() { if self.isCancelled { return } - guard let parentContext = parentContext else { + guard let context = managedObjectContext else { let error = CloudCoreError.coreData("CloudCore framework error") errorCompletionBlock?(error) return @@ -53,13 +52,10 @@ class ObjectToRecordOperation: Operation { } #endif - let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.performAndWait { - childContext.parent = parentContext - + context.performAndWait { do { - try self.fillRecordWithData(using: childContext) - try childContext.save() + try self.fillRecordWithData(using: context) + try context.save() self.conversionCompletionBlock?(self.record) } catch { self.errorCompletionBlock?(error) From d0b3cecb405ed48cf15881b122435761dec1feb6 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 15:01:41 -0700 Subject: [PATCH 199/203] (remove testing code) --- Source/Classes/Caching/CloudCoreCacheManager.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Source/Classes/Caching/CloudCoreCacheManager.swift b/Source/Classes/Caching/CloudCoreCacheManager.swift index 3dd37aec..29f68942 100644 --- a/Source/Classes/Caching/CloudCoreCacheManager.swift +++ b/Source/Classes/Caching/CloudCoreCacheManager.swift @@ -189,19 +189,6 @@ class CloudCoreCacheManager: NSObject { { guard let record = try? cacheable.restoreRecordWithSystemFields(for: .private) else { return } - /* - var newRecord: CKRecord? - let semaphore = DispatchSemaphore(value: 0) - container.privateCloudDatabase.fetch(withRecordID: record.recordID) { record, error in - newRecord = record - - semaphore.signal() - } - semaphore.wait() - - guard let record = newRecord else { return } - */ - record[cacheable.assetFieldName] = CKAsset(fileURL: cacheable.url) record["remoteStatusRaw"] = RemoteStatus.available.rawValue From 899df48e8e481b8882c40a492de9b37f7e78c648 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 15:16:51 -0700 Subject: [PATCH 200/203] more cleanup of ObjectToRecordOperation --- .../Push/ObjectToRecord/ObjectToRecordOperation.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift index c51f2e39..0c95ed2f 100644 --- a/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Push/ObjectToRecord/ObjectToRecordOperation.swift @@ -54,7 +54,7 @@ class ObjectToRecordOperation: Operation { context.performAndWait { do { - try self.fillRecordWithData(using: context) + try self.fillRecordWithData() try context.save() self.conversionCompletionBlock?(self.record) } catch { @@ -63,8 +63,8 @@ class ObjectToRecordOperation: Operation { } } - private func fillRecordWithData(using context: NSManagedObjectContext) throws { - guard let managedObject = try fetchObject(for: record, using: context) else { + private func fillRecordWithData() throws { + guard let managedObject = try fetchObject(for: record) else { throw CloudCoreError.coreData("Unable to find managed object for record: \(record)") } @@ -99,12 +99,12 @@ class ObjectToRecordOperation: Operation { } } - private func fetchObject(for record: CKRecord, using context: NSManagedObjectContext) throws -> NSManagedObject? { + private func fetchObject(for record: CKRecord) throws -> NSManagedObject? { let entityName = record.recordType let fetchRequest = NSFetchRequest(entityName: entityName) fetchRequest.predicate = NSPredicate(format: serviceAttributeNames.recordName + " == %@", record.recordID.recordName) - return try context.fetch(fetchRequest).first as? NSManagedObject + return try managedObjectContext?.fetch(fetchRequest).first as? NSManagedObject } } From 85098a4a2fe58c2b8eace08284fcae88754f2990 Mon Sep 17 00:00:00 2001 From: deeje Date: Tue, 31 May 2022 16:47:11 -0700 Subject: [PATCH 201/203] background tasks are only on iOS --- Source/Classes/Push/CoreDataObserver.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Classes/Push/CoreDataObserver.swift b/Source/Classes/Push/CoreDataObserver.swift index e9e46a30..9c2ef7e6 100644 --- a/Source/Classes/Push/CoreDataObserver.swift +++ b/Source/Classes/Push/CoreDataObserver.swift @@ -217,7 +217,9 @@ class CoreDataObserver { return } + #if TARGET_OS_IOS let backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CloudCore.processPersistentHistory") + #endif isProcessing = true @@ -254,7 +256,9 @@ class CoreDataObserver { } } + #if TARGET_OS_IOS UIApplication.shared.endBackgroundTask(backgroundTask) + #endif DispatchQueue.main.async { self.isProcessing = false From 258b7b0511ee030d086348029cd88c2a0805bbd8 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 1 Jun 2022 12:49:05 -0700 Subject: [PATCH 202/203] update ReadMe to include links to MediaBook app --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b6284801..5702d7bd 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # CloudCore ![Platform](https://img.shields.io/cocoapods/p/CloudCore.svg?style=flat) -![Status](https://img.shields.io/badge/status-beta-orange.svg) +![Status](https://img.shields.io/badge/status-production-green.svg) ![Swift](https://img.shields.io/badge/swift-5.0-orange.svg) **CloudCore** is an advanced sync engine for CloudKit and Core Data. @@ -303,6 +303,9 @@ You can find example application at [Example](/Example/) directory, which has be * **refresh** button calls `pull` to fetch data from Cloud. That is only useful for simulators because Simulator unable to receive push notifications * Use [CloudKit dashboard](https://icloud.developer.apple.com/dashboard/) to make changes and see it at application, and make change in application and see ones in dashboard. Don't forget to refresh dashboard's page because it doesn't update data on-the-fly. +## Example app using Cacheable Assets +[MediaBook](https://github.com/deeje/MediaBook) is a production-level iOS app being developed, which demonstrates how to handle cacheable assets in collection views. + ## Tests CloudKit objects can't be mocked up, that's why there are 2 different types of tests: From fb9b5e4983f81ad5d86b64e863f0827350553713 Mon Sep 17 00:00:00 2001 From: deeje Date: Wed, 1 Jun 2022 12:53:33 -0700 Subject: [PATCH 203/203] NSPersistentHistoryTracking is now required --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5702d7bd..12e5f62d 100755 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ func application(_ application: UIApplication, didReceiveRemoteNotification user } ``` -6. If you want to enable offline support, **enable NSPersistentHistoryTracking** when you initialize your Core Data stack +6. **Enable NSPersistentHistoryTracking** when you initialize your Core Data stack ```swift lazy var persistentContainer: NSPersistentContainer = { @@ -136,7 +136,7 @@ lazy var persistentContainer: NSPersistentContainer = { }() ``` -7. To identify changes from your app that should be pushed, **save** from a background ManagedObjectContext named `CloudCorePushContext`, or use the convenience function performBackgroundPushTask +7. To identify changes from your app that should be pushed, **save** from the convenience function performBackgroundPushTask ```swift persistentContainer.performBackgroundPushTask { moc in