2020-03-22 22:35:03 +01:00
//
// C l o u d K i t Z o n e . s w i f t
// A c c o u n t
//
// C r e a t e d b y M a u r i c e P a r k e r o n 3 / 2 1 / 2 0 .
// C o p y r i g h t © 2 0 2 0 R a n c h e r o S o f t w a r e , L L C . A l l r i g h t s r e s e r v e d .
//
import CloudKit
2020-03-30 00:12:34 +02:00
import os . log
import RSWeb
2020-03-22 22:35:03 +01:00
2020-04-04 22:04:38 +02:00
enum CloudKitZoneError : LocalizedError {
2020-03-29 10:43:20 +02:00
case userDeletedZone
2020-03-29 15:52:59 +02:00
case invalidParameter
2020-03-27 19:59:42 +01:00
case unknown
2020-04-04 22:04:38 +02:00
var errorDescription : String ? {
2020-04-06 09:15:28 +02:00
if case . userDeletedZone = self {
return NSLocalizedString ( " The iCloud data was deleted. Please delete the NetNewsWire iCloud account and add it again to continue using NetNewsWire's iCloud support. " , comment : " User deleted zone. " )
} else {
return NSLocalizedString ( " An unexpected CloudKit error occurred. " , comment : " An unexpected CloudKit error occurred. " )
}
2020-04-04 22:04:38 +02:00
}
2020-03-27 19:59:42 +01:00
}
2020-03-29 18:53:52 +02:00
protocol CloudKitZoneDelegate : class {
2020-04-02 03:21:14 +02:00
func cloudKitDidModify ( changed : [ CKRecord ] , deleted : [ CloudKitRecordKey ] , completion : @ escaping ( Result < Void , Error > ) -> Void ) ;
2020-03-29 18:53:52 +02:00
}
2020-04-01 19:22:59 +02:00
typealias CloudKitRecordKey = ( recordType : CKRecord . RecordType , recordID : CKRecord . ID )
2020-03-29 18:53:52 +02:00
protocol CloudKitZone : class {
2020-03-22 22:35:03 +01:00
2020-03-27 19:59:42 +01:00
static var zoneID : CKRecordZone . ID { get }
2020-03-30 00:12:34 +02:00
var log : OSLog { get }
var container : CKContainer ? { get }
var database : CKDatabase ? { get }
2020-03-29 18:53:52 +02:00
var delegate : CloudKitZoneDelegate ? { get set }
2020-03-30 00:12:34 +02:00
2020-04-04 09:33:41 +02:00
// / R e s e t t h e c h a n g e t o k e n u s e d t o d e t e r m i n e w h a t p o i n t i n t i m e w e a r e d o i n g c h a n g e s f e t c h e s
func resetChangeToken ( )
// / G e n e r a t e s a n e w C K R e c o r d . I D u s i n g a U U I D f o r t h e r e c o r d ' s n a m e
func generateRecordID ( ) -> CKRecord . ID
// / S u b s c r i b e t o c h a n g e s a t a z o n e l e v e l
2020-04-04 20:33:49 +02:00
func subscribeToZoneChanges ( )
2020-04-04 09:33:41 +02:00
// / P r o c e s s a r e m o v e n o t i f i c a t i o n
func receiveRemoteNotification ( userInfo : [ AnyHashable : Any ] , completion : @ escaping ( ) -> Void )
2020-03-22 22:35:03 +01:00
}
extension CloudKitZone {
2020-04-03 18:25:01 +02:00
// / R e s e t t h e c h a n g e t o k e n u s e d t o d e t e r m i n e w h a t p o i n t i n t i m e w e a r e d o i n g c h a n g e s f e t c h e s
2020-03-30 00:12:34 +02:00
func resetChangeToken ( ) {
changeToken = nil
}
2020-03-27 19:59:42 +01:00
func generateRecordID ( ) -> CKRecord . ID {
return CKRecord . ID ( recordName : UUID ( ) . uuidString , zoneID : Self . zoneID )
}
2020-04-06 09:15:28 +02:00
func retryIfPossible ( after : Double , block : @ escaping ( ) -> ( ) ) {
let delayTime = DispatchTime . now ( ) + after
DispatchQueue . main . asyncAfter ( deadline : delayTime , execute : {
block ( )
} )
}
2020-03-30 09:48:25 +02:00
func receiveRemoteNotification ( userInfo : [ AnyHashable : Any ] , completion : @ escaping ( ) -> Void ) {
let note = CKRecordZoneNotification ( fromRemoteNotificationDictionary : userInfo )
guard note ? . recordZoneID ? . zoneName = = Self . zoneID . zoneName else {
completion ( )
return
}
fetchChangesInZone ( ) { result in
if case . failure ( let error ) = result {
2020-04-05 00:35:09 +02:00
os_log ( . error , log : self . log , " %@ zone remote notification fetch error: %@ " , Self . zoneID . zoneName , error . localizedDescription )
2020-03-30 09:48:25 +02:00
}
completion ( )
}
}
2020-04-06 09:15:28 +02:00
func createZoneRecord ( completion : @ escaping ( Result < Void , Error > ) -> Void ) {
guard let database = database else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
database . save ( CKRecordZone ( zoneID : Self . zoneID ) ) { ( recordZone , error ) in
if let error = error {
DispatchQueue . main . async {
completion ( . failure ( CloudKitError ( error ) ) )
}
} else {
DispatchQueue . main . async {
completion ( . success ( ( ) ) )
}
}
}
}
func subscribeToZoneChanges ( ) {
let subscription = CKRecordZoneSubscription ( zoneID : Self . zoneID )
let info = CKSubscription . NotificationInfo ( )
info . shouldSendContentAvailable = true
subscription . notificationInfo = info
save ( subscription ) { result in
if case . failure ( let error ) = result {
os_log ( . error , log : self . log , " %@ zone subscribe to changes error: %@ " , Self . zoneID . zoneName , error . localizedDescription )
}
}
}
2020-03-22 22:35:03 +01:00
2020-04-03 18:25:01 +02:00
// / C h e c k s t o s e e i f t h e r e c o r d d e s c r i b e d i n t h e q u e r y e x i s t s b y r e t r i e v i n g o n l y t h e t e s t F i e l d p a r a m e t e r f i e l d .
func exists ( _ query : CKQuery , completion : @ escaping ( Result < Bool , Error > ) -> Void ) {
var recordFound = false
let op = CKQueryOperation ( query : query )
op . desiredKeys = [ " creationDate " ]
op . recordFetchedBlock = { record in
recordFound = true
}
op . queryCompletionBlock = { [ weak self ] ( _ , error ) in
switch CloudKitZoneResult . resolve ( error ) {
case . success :
DispatchQueue . main . async {
completion ( . success ( recordFound ) )
}
2020-04-06 09:15:28 +02:00
case . zoneNotFound :
self ? . createZoneRecord ( ) { result in
switch result {
case . success :
self ? . exists ( query , completion : completion )
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
2020-04-03 18:25:01 +02:00
case . retry ( let timeToWait ) :
self ? . retryIfPossible ( after : timeToWait ) {
self ? . exists ( query , completion : completion )
}
2020-04-06 09:15:28 +02:00
case . userDeletedZone :
DispatchQueue . main . async {
completion ( . failure ( CloudKitZoneError . userDeletedZone ) )
}
2020-04-03 18:25:01 +02:00
default :
DispatchQueue . main . async {
2020-04-04 04:20:55 +02:00
completion ( . failure ( CloudKitError ( error ! ) ) )
2020-04-03 18:25:01 +02:00
}
}
}
database ? . add ( op )
}
// / I s s u e a C K Q u e r y a n d r e t u r n t h e r e s u l t i n g C K R e c o r d s . s
2020-03-30 22:15:45 +02:00
func query ( _ query : CKQuery , completion : @ escaping ( Result < [ CKRecord ] , Error > ) -> Void ) {
guard let database = database else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
2020-03-31 18:07:54 +02:00
database . perform ( query , inZoneWith : Self . zoneID ) { [ weak self ] records , error in
2020-03-30 22:15:45 +02:00
switch CloudKitZoneResult . resolve ( error ) {
case . success :
2020-03-31 18:07:54 +02:00
DispatchQueue . main . async {
if let records = records {
completion ( . success ( records ) )
} else {
completion ( . failure ( CloudKitZoneError . unknown ) )
}
2020-03-30 22:15:45 +02:00
}
2020-04-06 09:15:28 +02:00
case . zoneNotFound :
self ? . createZoneRecord ( ) { result in
switch result {
case . success :
self ? . query ( query , completion : completion )
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
2020-03-30 22:15:45 +02:00
case . retry ( let timeToWait ) :
2020-03-31 18:07:54 +02:00
self ? . retryIfPossible ( after : timeToWait ) {
self ? . query ( query , completion : completion )
2020-03-30 22:15:45 +02:00
}
2020-04-04 03:39:50 +02:00
case . userDeletedZone :
DispatchQueue . main . async {
completion ( . failure ( CloudKitZoneError . userDeletedZone ) )
}
2020-03-30 22:15:45 +02:00
default :
2020-03-31 18:07:54 +02:00
DispatchQueue . main . async {
2020-04-04 04:20:55 +02:00
completion ( . failure ( CloudKitError ( error ! ) ) )
2020-03-31 18:07:54 +02:00
}
2020-03-30 22:15:45 +02:00
}
}
}
2020-04-03 18:25:01 +02:00
// / F e t c h a C K R e c o r d b y u s i n g i t s e x t e r n a l I D
2020-03-31 10:30:53 +02:00
func fetch ( externalID : String ? , completion : @ escaping ( Result < CKRecord , Error > ) -> Void ) {
guard let externalID = externalID else {
completion ( . failure ( CloudKitZoneError . invalidParameter ) )
return
}
let recordID = CKRecord . ID ( recordName : externalID , zoneID : Self . zoneID )
2020-03-31 18:07:54 +02:00
database ? . fetch ( withRecordID : recordID ) { [ weak self ] record , error in
2020-03-31 10:30:53 +02:00
switch CloudKitZoneResult . resolve ( error ) {
case . success :
DispatchQueue . main . async {
if let record = record {
completion ( . success ( record ) )
} else {
completion ( . failure ( CloudKitZoneError . unknown ) )
}
}
2020-04-06 09:15:28 +02:00
case . zoneNotFound :
self ? . createZoneRecord ( ) { result in
switch result {
case . success :
self ? . fetch ( externalID : externalID , completion : completion )
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
2020-03-31 10:30:53 +02:00
case . retry ( let timeToWait ) :
2020-03-31 18:07:54 +02:00
self ? . retryIfPossible ( after : timeToWait ) {
self ? . fetch ( externalID : externalID , completion : completion )
2020-03-31 10:30:53 +02:00
}
2020-04-04 03:39:50 +02:00
case . userDeletedZone :
DispatchQueue . main . async {
completion ( . failure ( CloudKitZoneError . userDeletedZone ) )
}
2020-03-31 10:30:53 +02:00
default :
DispatchQueue . main . async {
2020-04-04 04:20:55 +02:00
completion ( . failure ( CloudKitError ( error ! ) ) )
2020-03-31 10:30:53 +02:00
}
}
}
}
2020-04-03 18:25:01 +02:00
// / S a v e t h e C K R e c o r d
2020-03-31 10:30:53 +02:00
func save ( _ record : CKRecord , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
modify ( recordsToSave : [ record ] , recordIDsToDelete : [ ] , completion : completion )
}
2020-04-04 22:04:38 +02:00
// / S a v e t h e C K R e c o r d s
func save ( _ records : [ CKRecord ] , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
modify ( recordsToSave : records , recordIDsToDelete : [ ] , completion : completion )
}
2020-04-05 17:49:15 +02:00
// / S a v e s o r m o d i f i e s t h e r e c o r d s a s l o n g a s t h e y a r e u n c h a n g e d r e l a t i v e t o t h e l o c a l v e r s i o n
func saveIfNew ( _ records : [ CKRecord ] , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
let op = CKModifyRecordsOperation ( recordsToSave : records , recordIDsToDelete : [ CKRecord . ID ] ( ) )
op . savePolicy = . ifServerRecordUnchanged
op . isAtomic = false
op . modifyRecordsCompletionBlock = { [ weak self ] ( _ , _ , error ) in
guard let self = self else { return }
switch CloudKitZoneResult . resolve ( error ) {
case . success :
DispatchQueue . main . async {
completion ( . success ( ( ) ) )
}
case . zoneNotFound :
self . createZoneRecord ( ) { result in
switch result {
case . success :
self . saveIfNew ( records , completion : completion )
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
case . userDeletedZone :
DispatchQueue . main . async {
completion ( . failure ( CloudKitZoneError . userDeletedZone ) )
}
case . retry ( let timeToWait ) :
self . retryIfPossible ( after : timeToWait ) {
self . saveIfNew ( records , completion : completion )
}
case . limitExceeded :
let chunkedRecords = records . chunked ( into : 300 )
let group = DispatchGroup ( )
var errorOccurred = false
for chunk in chunkedRecords {
group . enter ( )
self . saveIfNew ( chunk ) { result in
if case . failure ( let error ) = result {
os_log ( . error , log : self . log , " %@ zone modify records error: %@ " , Self . zoneID . zoneName , error . localizedDescription )
errorOccurred = true
}
group . leave ( )
}
}
group . notify ( queue : DispatchQueue . main ) {
if errorOccurred {
completion ( . failure ( CloudKitZoneError . unknown ) )
} else {
completion ( . success ( ( ) ) )
}
}
default :
DispatchQueue . main . async {
completion ( . failure ( CloudKitError ( error ! ) ) )
}
}
}
database ? . add ( op )
}
2020-04-04 20:33:49 +02:00
// / S a v e t h e C K S u b s c r i p t i o n
func save ( _ subscription : CKSubscription , completion : @ escaping ( Result < CKSubscription , Error > ) -> Void ) {
database ? . save ( subscription ) { savedSubscription , error in
switch CloudKitZoneResult . resolve ( error ) {
case . success :
2020-04-04 22:04:38 +02:00
DispatchQueue . main . async {
completion ( . success ( ( savedSubscription ! ) ) )
}
case . zoneNotFound :
self . createZoneRecord ( ) { result in
switch result {
case . success :
self . save ( subscription , completion : completion )
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
2020-04-04 20:33:49 +02:00
case . retry ( let timeToWait ) :
self . retryIfPossible ( after : timeToWait ) {
self . save ( subscription , completion : completion )
}
default :
2020-04-04 22:04:38 +02:00
DispatchQueue . main . async {
completion ( . failure ( CloudKitError ( error ! ) ) )
}
2020-04-04 20:33:49 +02:00
}
}
}
// / D e l e t e a C K R e c o r d u s i n g i t s r e c o r d I D
func delete ( recordID : CKRecord . ID , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
modify ( recordsToSave : [ ] , recordIDsToDelete : [ recordID ] , completion : completion )
}
2020-04-23 23:39:09 +02:00
func delete ( recordIDs : [ CKRecord . ID ] , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
modify ( recordsToSave : [ ] , recordIDsToDelete : recordIDs , completion : completion )
}
2020-04-03 18:25:01 +02:00
// / D e l e t e a C K R e c o r d u s i n g i t s e x t e r n a l I D
2020-03-31 10:30:53 +02:00
func delete ( externalID : String ? , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
guard let externalID = externalID else {
completion ( . failure ( CloudKitZoneError . invalidParameter ) )
return
}
let recordID = CKRecord . ID ( recordName : externalID , zoneID : Self . zoneID )
modify ( recordsToSave : [ ] , recordIDsToDelete : [ recordID ] , completion : completion )
}
2020-04-04 20:33:49 +02:00
// / D e l e t e a C K S u b s c r i p t i o n
func delete ( subscriptionID : String , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
database ? . delete ( withSubscriptionID : subscriptionID ) { _ , error in
switch CloudKitZoneResult . resolve ( error ) {
case . success :
2020-04-04 22:04:38 +02:00
DispatchQueue . main . async {
completion ( . success ( ( ) ) )
}
2020-04-04 20:33:49 +02:00
case . retry ( let timeToWait ) :
self . retryIfPossible ( after : timeToWait ) {
self . delete ( subscriptionID : subscriptionID , completion : completion )
}
default :
2020-04-04 22:04:38 +02:00
DispatchQueue . main . async {
completion ( . failure ( CloudKitError ( error ! ) ) )
}
2020-04-04 20:33:49 +02:00
}
}
}
2020-04-05 17:49:15 +02:00
2020-04-03 18:25:01 +02:00
// / M o d i f y a n d d e l e t e t h e s u p p l i e d C K R e c o r d s a n d C K R e c o r d . I D s
2020-03-29 18:53:52 +02:00
func modify ( recordsToSave : [ CKRecord ] , recordIDsToDelete : [ CKRecord . ID ] , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
2020-03-29 15:52:59 +02:00
let op = CKModifyRecordsOperation ( recordsToSave : recordsToSave , recordIDsToDelete : recordIDsToDelete )
2020-03-27 19:59:42 +01:00
op . savePolicy = . changedKeys
op . isAtomic = true
2020-03-22 22:35:03 +01:00
2020-03-27 19:59:42 +01:00
op . modifyRecordsCompletionBlock = { [ weak self ] ( _ , _ , error ) in
2020-03-22 22:35:03 +01:00
guard let self = self else { return }
2020-03-29 10:43:20 +02:00
switch CloudKitZoneResult . resolve ( error ) {
2020-03-22 22:35:03 +01:00
case . success :
DispatchQueue . main . async {
2020-03-27 19:59:42 +01:00
completion ( . success ( ( ) ) )
2020-03-22 22:35:03 +01:00
}
2020-03-29 10:43:20 +02:00
case . zoneNotFound :
2020-03-28 14:30:25 +01:00
self . createZoneRecord ( ) { result in
switch result {
case . success :
2020-03-29 15:52:59 +02:00
self . modify ( recordsToSave : recordsToSave , recordIDsToDelete : recordIDsToDelete , completion : completion )
2020-03-28 14:30:25 +01:00
case . failure ( let error ) :
2020-03-31 18:07:54 +02:00
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
2020-03-28 14:30:25 +01:00
}
}
2020-03-29 10:43:20 +02:00
case . userDeletedZone :
DispatchQueue . main . async {
completion ( . failure ( CloudKitZoneError . userDeletedZone ) )
}
2020-03-27 19:59:42 +01:00
case . retry ( let timeToWait ) :
2020-03-31 10:30:53 +02:00
self . retryIfPossible ( after : timeToWait ) {
2020-03-29 15:52:59 +02:00
self . modify ( recordsToSave : recordsToSave , recordIDsToDelete : recordIDsToDelete , completion : completion )
2020-03-22 22:35:03 +01:00
}
2020-03-29 10:43:20 +02:00
case . limitExceeded :
2020-03-29 15:52:59 +02:00
let chunkedRecords = recordsToSave . chunked ( into : 300 )
2020-04-01 22:39:07 +02:00
let group = DispatchGroup ( )
var errorOccurred = false
2020-03-22 22:35:03 +01:00
for chunk in chunkedRecords {
2020-04-01 22:39:07 +02:00
group . enter ( )
self . modify ( recordsToSave : chunk , recordIDsToDelete : recordIDsToDelete ) { result in
if case . failure ( let error ) = result {
2020-04-05 00:35:09 +02:00
os_log ( . error , log : self . log , " %@ zone modify records error: %@ " , Self . zoneID . zoneName , error . localizedDescription )
2020-04-01 22:39:07 +02:00
errorOccurred = true
}
group . leave ( )
}
}
group . notify ( queue : DispatchQueue . main ) {
if errorOccurred {
completion ( . failure ( CloudKitZoneError . unknown ) )
} else {
completion ( . success ( ( ) ) )
}
2020-03-22 22:35:03 +01:00
}
2020-04-01 22:39:07 +02:00
2020-03-22 22:35:03 +01:00
default :
2020-03-29 10:43:20 +02:00
DispatchQueue . main . async {
2020-04-04 04:20:55 +02:00
completion ( . failure ( CloudKitError ( error ! ) ) )
2020-03-29 10:43:20 +02:00
}
2020-03-22 22:35:03 +01:00
}
}
2020-03-31 18:07:54 +02:00
2020-03-30 00:12:34 +02:00
database ? . add ( op )
2020-03-27 19:59:42 +01:00
}
2020-04-05 17:49:15 +02:00
2020-04-03 18:25:01 +02:00
// / F e t c h a l l t h e c h a n g e s i n t h e C K Z o n e s i n c e t h e l a s t t i m e w e c h e c k e d
2020-03-30 00:12:34 +02:00
func fetchChangesInZone ( completion : @ escaping ( Result < Void , Error > ) -> Void ) {
2020-04-16 01:30:39 +02:00
var savedChangeToken = changeToken
2020-04-01 19:22:59 +02:00
var changedRecords = [ CKRecord ] ( )
var deletedRecordKeys = [ CloudKitRecordKey ] ( )
2020-03-29 18:53:52 +02:00
let zoneConfig = CKFetchRecordZoneChangesOperation . ZoneConfiguration ( )
zoneConfig . previousServerChangeToken = changeToken
let op = CKFetchRecordZoneChangesOperation ( recordZoneIDs : [ Self . zoneID ] , configurationsByRecordZoneID : [ Self . zoneID : zoneConfig ] )
op . fetchAllChanges = true
2020-04-16 01:30:39 +02:00
op . recordZoneChangeTokensUpdatedBlock = { zoneID , token , _ in
savedChangeToken = token
2020-03-29 18:53:52 +02:00
}
2020-04-12 22:57:00 +02:00
op . recordChangedBlock = { record in
2020-04-01 19:22:59 +02:00
changedRecords . append ( record )
2020-03-29 18:53:52 +02:00
}
2020-04-12 22:57:00 +02:00
op . recordWithIDWasDeletedBlock = { recordID , recordType in
2020-04-01 19:22:59 +02:00
let recordKey = CloudKitRecordKey ( recordType : recordType , recordID : recordID )
deletedRecordKeys . append ( recordKey )
2020-03-29 18:53:52 +02:00
}
2020-04-16 01:30:39 +02:00
op . recordZoneFetchCompletionBlock = { zoneID , token , _ , _ , error in
if case . success = CloudKitZoneResult . resolve ( error ) {
savedChangeToken = token
2020-03-30 00:12:34 +02:00
}
2020-03-29 18:53:52 +02:00
}
2020-03-30 00:12:34 +02:00
op . fetchRecordZoneChangesCompletionBlock = { [ weak self ] error in
2020-04-01 21:55:40 +02:00
guard let self = self else { return }
switch CloudKitZoneResult . resolve ( error ) {
case . success :
DispatchQueue . main . async {
2020-04-16 01:30:39 +02:00
self . delegate ? . cloudKitDidModify ( changed : changedRecords , deleted : deletedRecordKeys ) { result in
switch result {
case . success :
self . changeToken = savedChangeToken
completion ( . success ( ( ) ) )
case . failure ( let error ) :
completion ( . failure ( error ) )
}
}
2020-03-30 00:12:34 +02:00
}
2020-04-01 21:55:40 +02:00
case . zoneNotFound :
self . createZoneRecord ( ) { result in
switch result {
case . success :
self . fetchChangesInZone ( completion : completion )
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
case . userDeletedZone :
DispatchQueue . main . async {
completion ( . failure ( CloudKitZoneError . userDeletedZone ) )
}
case . retry ( let timeToWait ) :
self . retryIfPossible ( after : timeToWait ) {
self . fetchChangesInZone ( completion : completion )
}
case . changeTokenExpired :
DispatchQueue . main . async {
self . changeToken = nil
self . fetchChangesInZone ( completion : completion )
}
default :
DispatchQueue . main . async {
2020-04-04 04:20:55 +02:00
completion ( . failure ( CloudKitError ( error ! ) ) )
2020-04-01 21:55:40 +02:00
}
2020-03-29 18:53:52 +02:00
}
2020-04-01 21:55:40 +02:00
2020-03-29 18:53:52 +02:00
}
2020-03-30 00:12:34 +02:00
database ? . add ( op )
2020-03-29 18:53:52 +02:00
}
}
private extension CloudKitZone {
var changeTokenKey : String {
return " cloudkit.server.token. \( Self . zoneID . zoneName ) "
}
var changeToken : CKServerChangeToken ? {
get {
guard let tokenData = UserDefaults . standard . object ( forKey : changeTokenKey ) as ? Data else { return nil }
return try ? NSKeyedUnarchiver . unarchivedObject ( ofClass : CKServerChangeToken . self , from : tokenData )
}
set {
guard let token = newValue , let data = try ? NSKeyedArchiver . archivedData ( withRootObject : token , requiringSecureCoding : false ) else {
UserDefaults . standard . removeObject ( forKey : changeTokenKey )
return
}
UserDefaults . standard . set ( data , forKey : changeTokenKey )
}
}
var zoneConfiguration : CKFetchRecordZoneChangesOperation . ZoneConfiguration {
let config = CKFetchRecordZoneChangesOperation . ZoneConfiguration ( )
config . previousServerChangeToken = changeToken
return config
}
}