CloudKit documentation states that tokens for CKFetchDatabaseChangesOperation and CKFetchRecordZoneChangesOperation aren't compatible and should be segregated in app's cache.
I have written a class to help persist these tokens in UserDefaults (code below). The class constructs strings to be used as UserDefaults keys based on the following logic:
- For zones, zoneID.ownerName + zoneID.zoneName, which results in something like _defaultOwner__Employees
- For databases, "db" + database.databaseScope.rawValue, which results in something like db3 for shared database
While I'm quite happy how this works, I've realized that this exposes iCloud userIDs when you persist a zone from a shared database. How big of a security problem is this? Any suggestions what I could do differently?
Code: (also available as GitHub gist)
import CloudKit
final class ChangeTokenCache {
// MARK: - Zone support
/// Determines a unique UserDefaults key for storing zone change tokens
/// like from CKFetchRecordZoneChangesOperation
private static func userDefaultsKey(_ zoneID: CKRecordZone.ID) -> String {
return "token_" + zoneID.ownerName + zoneID.zoneName
}
/// Persists a zone change token in standard UserDefaults
static func setZoneToken(_ zoneID: CKRecordZone.ID, token: CKServerChangeToken?) {
setToken(userDefaultsKey(zoneID), token: token)
}
/// Fetches a zone change token from standard UserDefaults
static func getZoneToken(_ zoneID: CKRecordZone.ID) -> CKServerChangeToken? {
return getToken(userDefaultsKey(zoneID))
}
// MARK: - Database support
/// Determines a unique UserDefaults key for storing database change tokens
/// like from CKFetchDatabaseChangesOperation
private static func userDefaultsKey(_ database: CKDatabase) -> String {
return "token_db\(database.databaseScope.rawValue)"
}
/// Persists a database change token in standard UserDefaults
static func setDatabaseToken(_ database: CKDatabase, token: CKServerChangeToken?) {
setToken(userDefaultsKey(database), token: token)
}
/// Fetches a database change token from standard UserDefaults
static func getDatabaseToken(_ database: CKDatabase) -> CKServerChangeToken? {
return getToken(userDefaultsKey(database))
}
// MARK: - Actual workers
/// Private method that archives a token as data and persists that to UserDefaults
private static func setToken(_ userDefaultsKey: String, token: CKServerChangeToken?) {
if let token = token,
let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: false) {
UserDefaults.standard.set(data, forKey: userDefaultsKey)
} else {
UserDefaults.standard.removeObject(forKey: userDefaultsKey)
}
}
/// Private method that gets data from UserDefaults and unarchives it as a token
private static func getToken(_ userDefaultsKey: String) -> CKServerChangeToken? {
guard let data = UserDefaults.standard.value(forKey: userDefaultsKey) as? Data else { return nil }
var token: CKServerChangeToken?
do {
token = try NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: data)
} catch {
token = nil
}
return token
}
}