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
2020-04-26 09:28:19 +02:00
// / C r e a t e s t h e z o n e r e c o r d
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 ( ( ) ) )
}
}
}
}
2020-04-26 09:28:19 +02:00
// / S u b s c r i b e s t o z o n e c h a n g e s
2020-04-06 09:15:28 +02:00
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-04-03 18:25:01 +02:00
// / 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-04-26 17:05:26 +02:00
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
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 :
2020-04-26 17:05:26 +02:00
self . createZoneRecord ( ) { result in
2020-04-06 09:15:28 +02:00
switch result {
case . success :
2020-04-26 17:05:26 +02:00
self . query ( query , completion : completion )
2020-04-06 09:15:28 +02:00
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
2020-03-30 22:15:45 +02:00
case . retry ( let timeToWait ) :
2020-04-26 17:05:26 +02:00
os_log ( . error , log : self . log , " %@ zone query retry in %@ seconds. " , Self . zoneID . zoneName , timeToWait )
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-04-26 17:05:26 +02:00
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
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 :
2020-04-26 17:05:26 +02:00
self . createZoneRecord ( ) { result in
2020-04-06 09:15:28 +02:00
switch result {
case . success :
2020-04-26 17:05:26 +02:00
self . fetch ( externalID : externalID , completion : completion )
2020-04-06 09:15:28 +02:00
case . failure ( let error ) :
DispatchQueue . main . async {
completion ( . failure ( error ) )
}
}
}
2020-03-31 10:30:53 +02:00
case . retry ( let timeToWait ) :
2020-04-26 17:05:26 +02:00
os_log ( . error , log : self . log , " %@ zone fetch retry in %@ seconds. " , Self . zoneID . zoneName , timeToWait )
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
2020-04-26 17:05:26 +02:00
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
2020-04-05 17:49:15 +02:00
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 ) :
2020-04-26 17:05:26 +02:00
os_log ( . error , log : self . log , " %@ zone save if new retry in %@ seconds. " , Self . zoneID . zoneName , timeToWait )
2020-04-05 17:49:15 +02:00
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 ) {
2020-04-26 17:05:26 +02:00
database ? . save ( subscription ) { [ weak self ] savedSubscription , error in
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
2020-04-04 20:33:49 +02:00
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 ) :
2020-04-26 17:05:26 +02:00
os_log ( . error , log : self . log , " %@ zone save subscription retry in %@ seconds. " , Self . zoneID . zoneName , timeToWait )
2020-04-04 20:33:49 +02:00
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
}
}
}
2020-04-27 09:11:20 +02:00
// / D e l e t e C K R e c o r d s u s i n g a C K Q u e r y
func delete ( ckQuery : CKQuery , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
var records = [ CKRecord ] ( )
let op = CKQueryOperation ( query : ckQuery )
op . recordFetchedBlock = { record in
records . append ( record )
}
op . queryCompletionBlock = { [ weak self ] ( cursor , error ) in
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
if let cursor = cursor {
self . delete ( cursor : cursor , carriedRecords : records , completion : completion )
} else {
guard ! records . isEmpty else {
completion ( . success ( ( ) ) )
return
}
let recordIDs = records . map { $0 . recordID }
self . modify ( recordsToSave : [ ] , recordIDsToDelete : recordIDs , completion : completion )
}
}
database ? . add ( op )
}
// / D e l e t e C K R e c o r d s u s i n g a C K Q u e r y
func delete ( cursor : CKQueryOperation . Cursor , carriedRecords : [ CKRecord ] , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
var records = [ CKRecord ] ( )
let op = CKQueryOperation ( cursor : cursor )
op . recordFetchedBlock = { record in
records . append ( record )
}
op . queryCompletionBlock = { [ weak self ] ( cursor , error ) in
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
records . append ( contentsOf : carriedRecords )
if let cursor = cursor {
self . delete ( cursor : cursor , carriedRecords : records , completion : completion )
} else {
let recordIDs = records . map { $0 . recordID }
self . modify ( recordsToSave : [ ] , recordIDsToDelete : recordIDs , completion : completion )
}
}
database ? . add ( op )
}
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-26 09:28:19 +02:00
// / D e l e t e C K R e c o r d s
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 ) {
2020-04-26 17:05:26 +02:00
database ? . delete ( withSubscriptionID : subscriptionID ) { [ weak self ] _ , error in
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
2020-04-04 20:33:49 +02:00
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 ) :
2020-04-26 17:05:26 +02:00
os_log ( . error , log : self . log , " %@ zone delete subscription retry in %@ seconds. " , Self . zoneID . zoneName , timeToWait )
2020-04-04 20:33:49 +02:00
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
2020-04-26 17:05:26 +02:00
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
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-04-26 17:05:26 +02:00
os_log ( . error , log : self . log , " %@ zone modify retry in %@ seconds. " , Self . zoneID . zoneName , 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-26 17:05:26 +02:00
guard let self = self else {
completion ( . failure ( CloudKitZoneError . unknown ) )
return
}
2020-04-01 21:55:40 +02:00
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 ) :
2020-04-26 17:05:26 +02:00
os_log ( . error , log : self . log , " %@ zone fetch changes retry in %@ seconds. " , Self . zoneID . zoneName , timeToWait )
2020-04-01 21:55:40 +02:00
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
}
}