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) { }
}