diff --git a/.gitignore b/.gitignore index f1933103..6ee42f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ ## Build generated build/ DerivedData/ +.DS_Store ## Various settings *.pbxuser diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 6a51003b..857d7f4c 100755 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -595,7 +595,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0910; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = "Vasily Ulianov"; TargetAttributes = { D5B2E89E1C3A780C00C0327D = { @@ -634,6 +634,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -816,12 +817,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; @@ -855,7 +858,7 @@ ONLY_ACTIVE_ARCH = YES; SUPPORTED_PLATFORMS = "macosx watchsimulator watchos appletvsimulator appletvos iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -874,12 +877,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; @@ -906,7 +911,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SUPPORTED_PLATFORMS = "macosx watchsimulator watchos appletvsimulator appletvos iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; 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.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/Example/Sources/Class/NotificationsObserver.swift b/Example/Sources/Class/NotificationsObserver.swift index 0cc34643..ac0cab7e 100644 --- a/Example/Sources/Class/NotificationsObserver.swift +++ b/Example/Sources/Class/NotificationsObserver.swift @@ -28,7 +28,7 @@ class CloudCoreDelegateHandler: CloudCoreDelegate { os_log("✅ Finished saving to iCloud", log: OSLog.default, type: .debug) } - func error(error: Error, module: Module?) { + func error(error: Error, module: Module) { print("⚠️ CloudCore error detected in module \(String(describing: module)): \(error)") } diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift index 84421763..dea2efe6 100644 --- a/Source/Classes/CloudCore.swift +++ b/Source/Classes/CloudCore.swift @@ -47,7 +47,7 @@ import CloudKit 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:)` */ -open class CloudCore { +@objc @objcMembers open class CloudCore: NSObject { // MARK: - Properties @@ -93,7 +93,7 @@ open class CloudCore { // Fetch updated data (e.g. push notifications weren't received) let updateFromCloudOperation = FetchAndSaveOperation(persistentContainer: container) updateFromCloudOperation.errorBlock = { - self.delegate?.error(error: $0, module: .some(.fetchFromCloud)) + self.delegate?.error(error: $0, module: .fetchFromCloud) } #if !os(watchOS) @@ -112,6 +112,11 @@ open class CloudCore { // FIXME: unsubscribe } + + /// Set Database Version + public static func setDatabaseVersion(_ version: Int) { + config.databaseVersion = version + } // MARK: Fetchers @@ -185,7 +190,7 @@ open class CloudCore { static private func handle(subscriptionError: Error, container: NSPersistentContainer) { guard let cloudError = subscriptionError as? CKError, let partialErrorValues = cloudError.partialErrorsByItemID?.values else { - delegate?.error(error: subscriptionError, module: nil) + delegate?.error(error: subscriptionError, module: .subscribeToCloud) return } @@ -203,7 +208,7 @@ open class CloudCore { } } - delegate?.error(error: subscriptionError, module: nil) + delegate?.error(error: subscriptionError, module: .subscribeToCloud) } } diff --git a/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift index 67f0a407..00021e24 100644 --- a/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift @@ -60,11 +60,12 @@ class FetchRecordZoneChangesOperation: Operation { self.recordWithIDWasDeletedBlock?(recordID) } fetchOperation.recordZoneFetchCompletionBlock = { zoneId, serverChangeToken, clientChangeTokenData, isMore, error in - self.tokens.tokensByRecordZoneID[zoneId] = serverChangeToken if let error = error { self.errorBlock?(zoneId, error) - } + } else { + self.tokens.tokensByRecordZoneID[zoneId] = serverChangeToken + } if isMore { let moreOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) diff --git a/Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift index bbc99627..5c5f7edc 100644 --- a/Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift @@ -53,7 +53,13 @@ class RecordToCoreDataOperation: AsynchronousOperation { guard let serviceAttributes = NSEntityDescription.entity(forEntityName: entityName, in: context)?.serviceAttributeNames else { throw CloudCoreError.missingServiceAttributes(entityName: entityName) } - + if let recordVersion = record.value(forKey: ServiceAttributeNames.recordVersion) as? Int { + guard recordVersion <= CloudCore.config.databaseVersion else { + CloudCore.tokens.canSaveToken = false + throw CloudCoreError.incompatibleVersion(recordVersion) + } + } + // Try to find existing objects let fetchRequest = NSFetchRequest(entityName: entityName) fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordID + " == %@", record.recordID.encodedString) @@ -74,11 +80,13 @@ class RecordToCoreDataOperation: AsynchronousOperation { /// - 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 attribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) - let coreDataValue = try attribute.makeCoreDataValue() - object.setValue(coreDataValue, forKey: key) + if key != ServiceAttributeNames.recordVersion { + let recordValue = record.value(forKey: key) + + let attribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) + let coreDataValue = try attribute.makeCoreDataValue() + object.setValue(coreDataValue, forKey: key) + } } // Set system headers diff --git a/Source/Classes/Save/CoreDataListener.swift b/Source/Classes/Save/CoreDataListener.swift index 7410706f..989ef590 100644 --- a/Source/Classes/Save/CoreDataListener.swift +++ b/Source/Classes/Save/CoreDataListener.swift @@ -25,7 +25,7 @@ class CoreDataListener { 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: .saveToCloud) } } @@ -82,7 +82,7 @@ class CoreDataListener { try backgroundContext.save() } } catch { - listener.delegate?.error(error: error, module: .some(.saveToCloud)) + listener.delegate?.error(error: error, module: .saveToCloud) } CloudCore.delegate?.didSyncToCloud() @@ -91,7 +91,7 @@ class CoreDataListener { 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: .saveToCloud) return } @@ -103,25 +103,25 @@ class CoreDataListener { // Create CloudCore Zone let createZoneOperation = CreateCloudCoreZoneOperation() createZoneOperation.errorBlock = { - self.delegate?.error(error: $0, module: .some(.saveToCloud)) + self.delegate?.error(error: $0, module: .saveToCloud) self.cloudSaveOperationQueue.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: .saveToCloud) } subscribeOperation.addDependency(createZoneOperation) cloudSaveOperationQueue.addOperation(subscribeOperation) #endif // Upload all local data let uploadOperation = UploadAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) - uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) } + uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .saveToCloud) } cloudSaveOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) case .operationCancelled: return - default: delegate?.error(error: cloudError, module: .some(.saveToCloud)) + default: delegate?.error(error: cloudError, module: .saveToCloud) } } diff --git a/Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift index aa3d9bf8..798962c8 100644 --- a/Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift @@ -51,6 +51,9 @@ class ObjectToRecordConverter { } else { recordWithSystemFields = try object.setRecordInformation() } + + // Update record version + recordWithSystemFields.setValue(CloudCore.config.databaseVersion, forKey: ServiceAttributeNames.recordVersion) var changedAttributes: [String]? diff --git a/Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift index 917200bd..b0c013e9 100644 --- a/Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift @@ -56,7 +56,7 @@ class ObjectToRecordOperation: Operation { throw CloudCoreError.coreData("Unable to find managed object for record: \(record)") } - let changedValues = managedObject.committedValues(forKeys: changedAttributes) + let changedValues = managedObject.committedValues(forKeys: nil) for (attributeName, value) in changedValues { if attributeName == serviceAttributeNames.recordData || attributeName == serviceAttributeNames.recordID { continue } diff --git a/Source/Enum/CloudCoreError.swift b/Source/Enum/CloudCoreError.swift index 70c4e98b..af19817b 100644 --- a/Source/Enum/CloudCoreError.swift +++ b/Source/Enum/CloudCoreError.swift @@ -22,8 +22,10 @@ public enum CloudCoreError: Error, CustomStringConvertible { /// Custom error, description is placed inside associated value case custom(String) - - + + /// Incompatible record version error + case incompatibleVersion(Int) + /// CloudCore doesn't support relationships with `NSOrderedSet` type case orderedSetRelationshipIsNotSupported(NSRelationshipDescription) @@ -36,6 +38,7 @@ public enum CloudCoreError: Error, CustomStringConvertible { case .cloudKit(let text): return "iCloud error: \(text)" case .coreData(let text): return "Core Data error: \(text)" case .custom(let error): return error + case .incompatibleVersion(let version): return "Record version \(version) / Database version \(CloudCore.config.databaseVersion)" case .orderedSetRelationshipIsNotSupported(let relationship): return "Relationships with NSOrderedSet type are not supported. Error occured in: \(relationship)" } } diff --git a/Source/Enum/Module.swift b/Source/Enum/Module.swift index 2ff7525b..5eacae59 100644 --- a/Source/Enum/Module.swift +++ b/Source/Enum/Module.swift @@ -9,12 +9,14 @@ import Foundation /// Enumeration with module name that issued an error in `CloudCoreErrorDelegate` -public enum Module { - - /// Save to CloudKit module - case saveToCloud - - /// Fetch from CloudKit module - case fetchFromCloud +@objc public enum Module: Int { + /// Save to CloudKit module + case saveToCloud + + /// Fetch from CloudKit module + case fetchFromCloud + + /// No CloudKit module + case subscribeToCloud } diff --git a/Source/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift index 582fc48e..de18df0d 100644 --- a/Source/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -43,6 +43,11 @@ public struct CloudCoreConfig { /// subscriptionID's prefix for custom CKSubscription in public databases var publicSubscriptionIDPrefix = "CloudCore-" + + /// Database version to handle incompatible record version + /// + /// Default value is 1 + public var databaseVersion = 1 // MARK: Core Data let contextName = "CloudCoreFetchAndSave" diff --git a/Source/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift index 7174eaf4..1b070730 100644 --- a/Source/Model/ServiceAttributeName.swift +++ b/Source/Model/ServiceAttributeName.swift @@ -15,6 +15,7 @@ struct ServiceAttributeNames { static let valueRecordData = "recordData" static let valueRecordID = "recordID" + static let recordVersion: String = "recordVersion" let entityName: String let recordData: String diff --git a/Source/Model/Tokens.swift b/Source/Model/Tokens.swift index a91ab8a8..62781ac6 100644 --- a/Source/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -27,9 +27,10 @@ import CloudKit } ``` */ -open class Tokens: NSObject, NSCoding { +@objc @objcMembers open class Tokens: NSObject, NSCoding { var tokensByRecordZoneID = [CKRecordZoneID: CKServerChangeToken]() + var canSaveToken = true private struct ArchiverKey { static let tokensByRecordZoneID = "tokensByRecordZoneID" @@ -45,7 +46,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() @@ -56,6 +57,10 @@ open class Tokens: NSObject, NSCoding { /// Save tokens to UserDefaults and synchronize. Key is used from `CloudCoreConfig.userDefaultsKeyTokens` open func saveToUserDefaults() { + guard canSaveToken else { + NSLog("CloudCore will not save tokens as incompatible version error occured") + return + } let tokensData = NSKeyedArchiver.archivedData(withRootObject: self) UserDefaults.standard.set(tokensData, forKey: CloudCore.config.userDefaultsKeyTokens) UserDefaults.standard.synchronize() diff --git a/Source/Protocols/CloudCoreDelegate.swift b/Source/Protocols/CloudCoreDelegate.swift index d0f28cf0..004d2b29 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 { +@objc public protocol CloudCoreDelegate: class { // MARK: Notifications @@ -34,7 +34,7 @@ public protocol CloudCoreDelegate: class { /// - Parameters: /// - error: in most cases contains `CloudCoreError` or `CKError` /// - module: framework's module that throwed an error - func error(error: Error, module: Module?) + func error(error: Error, module: Module) } @@ -44,6 +44,6 @@ public extension CloudCoreDelegate { func didSyncFromCloud() { } func willSyncToCloud() { } func didSyncToCloud() { } - func error(error: Error, module: Module?) { } + func error(error: Error, module: Module) { } }