@ -5,6 +5,7 @@
import Foundation
import SignalServiceKit
import CloudKit
import PromiseKit
// W e d o n ' t w o r r y a b o u t a t o m i c w r i t e s . E a c h b a c k u p e x p o r t
// w i l l d i f f a g a i n s t l a s t s u c c e s s f u l b a c k u p .
@ -42,16 +43,18 @@ import CloudKit
// MARK: - U p l o a d
@objc
public class func saveTestFileToCloudObjc ( recipientId : String ,
fileUrl : URL ) -> AnyPromise {
return AnyPromise ( saveTestFileToCloud ( recipientId : recipientId ,
fileUrl : fileUrl ) )
}
public class func saveTestFileToCloud ( recipientId : String ,
fileUrl : URL ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
fileUrl : URL ) -> Promise < String > {
let recordName = " \( recordNamePrefix ( forRecipientId : recipientId ) ) test- \( NSUUID ( ) . uuidString ) "
saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : signalBackupRecordType ,
success : success ,
failure : failure )
return saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : signalBackupRecordType )
}
// " E p h e m e r a l " f i l e s a r e s p e c i f i c t o t h i s b a c k u p e x p o r t a n d w i l l a l w a y s n e e d t o
@ -59,16 +62,18 @@ import CloudKit
// W e w o u l d n ' t w a n t t o o v e r w r i t e p r e v i o u s i m a g e s u n t i l t h e e n t i r e b a c k u p e x p o r t i s
// c o m p l e t e .
@objc
public class func saveEphemeralDatabaseFileToCloudObjc ( recipientId : String ,
fileUrl : URL ) -> AnyPromise {
return AnyPromise ( saveEphemeralDatabaseFileToCloud ( recipientId : recipientId ,
fileUrl : fileUrl ) )
}
public class func saveEphemeralDatabaseFileToCloud ( recipientId : String ,
fileUrl : URL ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
fileUrl : URL ) -> Promise < String > {
let recordName = " \( recordNamePrefix ( forRecipientId : recipientId ) ) ephemeralFile- \( NSUUID ( ) . uuidString ) "
saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : signalBackupRecordType ,
success : success ,
failure : failure )
return saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : signalBackupRecordType )
}
// " P e r s i s t e n t " f i l e s m a y b e s h a r e d b e t w e e n b a c k u p e x p o r t ; t h e y s h o u l d o n l y b e s a v e d
@ -124,9 +129,9 @@ import CloudKit
let firstRange = match . range ( at : 1 )
guard firstRange . location = = 0 ,
firstRange . length > 0 else {
// M a t c h m u s t b e a t s t a r t o f s t r i n g a n d n o n - e m p t y .
Logger . warn ( " invalid match: \( recordName ) \( firstRange ) " )
continue
// M a t c h m u s t b e a t s t a r t o f s t r i n g a n d n o n - e m p t y .
Logger . warn ( " invalid match: \( recordName ) \( firstRange ) " )
continue
}
let recipientId = ( recordName as NSString ) . substring ( with : firstRange ) as String
recipientIds . append ( recipientId )
@ -138,104 +143,120 @@ import CloudKit
// o n c e . F o r e x a m p l e , a t t a c h m e n t f i l e s s h o u l d o n l y b e u p l o a d e d o n c e . S u b s e q u e n t
// b a c k u p s c a n r e u s e t h e s a m e r e c o r d .
@objc
public class func savePersistentFileOnceToCloudObjc ( recipientId : String ,
fileId : String ,
fileUrlBlock : @ escaping ( ) -> URL ? ) -> AnyPromise {
return AnyPromise ( savePersistentFileOnceToCloud ( recipientId : recipientId ,
fileId : fileId ,
fileUrlBlock : fileUrlBlock ) )
}
public class func savePersistentFileOnceToCloud ( recipientId : String ,
fileId : String ,
fileUrlBlock : @ escaping ( ) -> URL ? ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
fileUrlBlock : @ escaping ( ) -> URL ? ) -> Promise < String > {
let recordName = recordNameForPersistentFile ( recipientId : recipientId , fileId : fileId )
saveFileOnceToCloud ( recordName : recordName ,
recordType : signalBackupRecordType ,
fileUrlBlock : fileUrlBlock ,
success : success ,
failure : failure )
return saveFileOnceToCloud ( recordName : recordName ,
recordType : signalBackupRecordType ,
fileUrlBlock : fileUrlBlock )
}
@objc
public class func upsertManifestFileToCloudObjc ( recipientId : String ,
fileUrl : URL ) -> AnyPromise {
return AnyPromise ( upsertManifestFileToCloud ( recipientId : recipientId ,
fileUrl : fileUrl ) )
}
public class func upsertManifestFileToCloud ( recipientId : String ,
fileUrl : URL ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
fileUrl : URL ) -> Promise < String > {
// W e w a n t t o u s e a w e l l - k n o w n r e c o r d i d a n d t y p e f o r m a n i f e s t f i l e s .
let recordName = recordNameForManifest ( recipientId : recipientId )
upsertFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : signalBackupRecordType ,
success : success ,
failure : failure )
return upsertFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : signalBackupRecordType )
}
@objc
public class func saveFileToCloudObjc ( fileUrl : URL ,
recordName : String ,
recordType : String ) -> AnyPromise {
return AnyPromise ( saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType ) )
}
public class func saveFileToCloud ( fileUrl : URL ,
recordName : String ,
recordType : String ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
recordType : String ) -> Promise < String > {
let recordID = CKRecordID ( recordName : recordName )
let record = CKRecord ( recordType : recordType , recordID : recordID )
let asset = CKAsset ( fileURL : fileUrl )
record [ payloadKey ] = asset
saveRecordToCloud ( record : record ,
success : success ,
failure : failure )
return saveRecordToCloud ( record : record )
}
@objc
public class func saveRecordToCloud ( record : CKRecord ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
saveRecordToCloud ( record : record ,
remainingRetries : maxRetries ,
success : success ,
failure : failure )
public class func saveRecordToCloudObjc ( record : CKRecord ) -> AnyPromise {
return AnyPromise ( saveRecordToCloud ( record : record ) )
}
private class func saveRecordToCloud ( record : CKRecord ,
remainingRetries : Int ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
let saveOperation = CKModifyRecordsOperation ( recordsToSave : [ record ] , recordIDsToDelete : nil )
saveOperation . modifyRecordsCompletionBlock = { ( records , recordIds , error ) in
public class func saveRecordToCloud ( record : CKRecord ) -> Promise < String > {
return saveRecordToCloud ( record : record ,
remainingRetries : maxRetries )
}
let outcome = outcomeForCloudKitError ( error : error ,
remainingRetries : remainingRetries ,
label : " Save Record " )
switch outcome {
case . success :
let recordName = record . recordID . recordName
success ( recordName )
case . failureDoNotRetry ( let outcomeError ) :
failure ( outcomeError )
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
saveRecordToCloud ( record : record ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
saveRecordToCloud ( record : record ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
private class func saveRecordToCloud ( record : CKRecord ,
remainingRetries : Int ) -> Promise < String > {
return Promise { resolver in
let saveOperation = CKModifyRecordsOperation ( recordsToSave : [ record ] , recordIDsToDelete : nil )
saveOperation . modifyRecordsCompletionBlock = { ( records , recordIds , error ) in
let outcome = outcomeForCloudKitError ( error : error ,
remainingRetries : remainingRetries ,
label : " Save Record " )
switch outcome {
case . success :
let recordName = record . recordID . recordName
resolver . fulfill ( recordName )
case . failureDoNotRetry ( let outcomeError ) :
resolver . reject ( outcomeError )
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
saveRecordToCloud ( record : record ,
remainingRetries : remainingRetries - 1 )
. done { ( recordName ) in
resolver . fulfill ( recordName )
} . catch { ( error ) in
resolver . reject ( error )
} . retainUntilComplete ( )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
saveRecordToCloud ( record : record ,
remainingRetries : remainingRetries - 1 )
. done { ( recordName ) in
resolver . fulfill ( recordName )
} . catch { ( error ) in
resolver . reject ( error )
} . retainUntilComplete ( )
}
case . unknownItem :
owsFailDebug ( " unexpected CloudKit response. " )
resolver . reject ( invalidServiceResponseError ( ) )
}
case . unknownItem :
owsFailDebug ( " unexpected CloudKit response. " )
failure ( invalidServiceResponseError ( ) )
}
}
saveOperation . isAtomic = false
saveOperation . isAtomic = false
// T h e s e A P I s a r e o n l y a v a i l a b l e i n i O S 9 . 3 a n d l a t e r .
if #available ( iOS 9.3 , * ) {
saveOperation . isLongLived = true
saveOperation . qualityOfService = . background
}
// T h e s e A P I s a r e o n l y a v a i l a b l e i n i O S 9 . 3 a n d l a t e r .
if #available ( iOS 9.3 , * ) {
saveOperation . isLongLived = true
saveOperation . qualityOfService = . background
}
database ( ) . add ( saveOperation )
database ( ) . add ( saveOperation )
}
}
// C o m p a r e :
@ -244,32 +265,33 @@ import CloudKit
// * A " s a v e o n c e " c r e a t e s a n e w r e c o r d i f n o n e e x i s t s a n d
// d o e s n o t h i n g i f t h e r e i s a n e x i s t i n g r e c o r d .
@objc
public class func upsertFileToCloudObjc ( fileUrl : URL ,
recordName : String ,
recordType : String ) -> AnyPromise {
return AnyPromise ( upsertFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType ) )
}
public class func upsertFileToCloud ( fileUrl : URL ,
recordName : String ,
recordType : String ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
checkForFileInCloud ( recordName : recordName ,
remainingRetries : maxRetries ,
success : { ( record ) in
if let record = record {
// R e c o r d f o u n d , u p d a t i n g e x i s t i n g r e c o r d .
let asset = CKAsset ( fileURL : fileUrl )
record [ payloadKey ] = asset
saveRecordToCloud ( record : record ,
success : success ,
failure : failure )
} else {
// N o r e c o r d f o u n d , s a v i n g n e w r e c o r d .
saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType ,
success : success ,
failure : failure )
}
} ,
failure : failure )
recordType : String ) -> Promise < String > {
return checkForFileInCloud ( recordName : recordName ,
remainingRetries : maxRetries )
. then { ( record : CKRecord ? ) -> Promise < String > in
if let record = record {
// R e c o r d f o u n d , u p d a t i n g e x i s t i n g r e c o r d .
let asset = CKAsset ( fileURL : fileUrl )
record [ payloadKey ] = asset
return saveRecordToCloud ( record : record )
}
// N o r e c o r d f o u n d , s a v i n g n e w r e c o r d .
return saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType )
}
}
// C o m p a r e :
@ -278,54 +300,55 @@ import CloudKit
// * A " s a v e o n c e " c r e a t e s a n e w r e c o r d i f n o n e e x i s t s a n d
// d o e s n o t h i n g i f t h e r e i s a n e x i s t i n g r e c o r d .
@objc
public class func saveFileOnceToCloudObjc ( recordName : String ,
recordType : String ,
fileUrlBlock : @ escaping ( ) -> URL ? ) -> AnyPromise {
return AnyPromise ( saveFileOnceToCloud ( recordName : recordName ,
recordType : recordType ,
fileUrlBlock : fileUrlBlock ) )
}
public class func saveFileOnceToCloud ( recordName : String ,
recordType : String ,
fileUrlBlock : @ escaping ( ) -> URL ? ,
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
fileUrlBlock : @ escaping ( ) -> URL ? ) -> Promise < String > {
return checkForFileInCloud ( recordName : recordName ,
remainingRetries : maxRetries )
. then { ( record : CKRecord ? ) -> Promise < String > in
if record != nil {
// R e c o r d f o u n d , s k i p p i n g s a v e .
return Promise . value ( recordName )
}
// N o r e c o r d f o u n d , s a v i n g n e w r e c o r d .
guard let fileUrl = fileUrlBlock ( ) else {
Logger . error ( " error preparing file for upload. " )
return Promise ( error : OWSErrorWithCodeDescription ( . exportBackupError ,
NSLocalizedString ( " BACKUP_EXPORT_ERROR_SAVE_FILE_TO_CLOUD_FAILED " ,
comment : " Error indicating the backup export failed to save a file to the cloud. " ) ) )
}
checkForFileInCloud ( recordName : recordName ,
remainingRetries : maxRetries ,
success : { ( record ) in
if record != nil {
// R e c o r d f o u n d , s k i p p i n g s a v e .
success ( recordName )
} else {
// N o r e c o r d f o u n d , s a v i n g n e w r e c o r d .
guard let fileUrl = fileUrlBlock ( ) else {
Logger . error ( " error preparing file for upload. " )
failure ( OWSErrorWithCodeDescription ( . exportBackupError ,
NSLocalizedString ( " BACKUP_EXPORT_ERROR_SAVE_FILE_TO_CLOUD_FAILED " ,
comment : " Error indicating the backup export failed to save a file to the cloud. " ) ) )
return
}
saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType ,
success : success ,
failure : failure )
}
} ,
failure : failure )
return saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType )
}
}
// MARK: - D e l e t e
@objc
public class func deleteRecordsFromCloud ( recordNames : [ String ] ,
success : @ escaping ( ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
success : @ escaping ( ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
deleteRecordsFromCloud ( recordNames : recordNames ,
remainingRetries : maxRetries ,
success : success ,
failure : failure )
remainingRetries : maxRetries ,
success : success ,
failure : failure )
}
private class func deleteRecordsFromCloud ( recordNames : [ String ] ,
remainingRetries : Int ,
success : @ escaping ( ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
remainingRetries : Int ,
success : @ escaping ( ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
let recordIDs = recordNames . map { CKRecordID ( recordName : $0 ) }
let deleteOperation = CKModifyRecordsOperation ( recordsToSave : nil , recordIDsToDelete : recordIDs )
@ -342,16 +365,16 @@ import CloudKit
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
deleteRecordsFromCloud ( recordNames : recordNames ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
deleteRecordsFromCloud ( recordNames : recordNames ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
}
case . unknownItem :
owsFailDebug ( " unexpected CloudKit response. " )
@ -364,9 +387,10 @@ import CloudKit
// MARK: - E x i s t s ?
private class func checkForFileInCloud ( recordName : String ,
remainingRetries : Int ,
success : @ escaping ( CKRecord ? ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
remainingRetries : Int ) -> Promise < CKRecord ? > {
let ( promise , resolver ) = Promise < CKRecord ? > . pending ( )
let recordId = CKRecordID ( recordName : recordName )
let fetchOperation = CKFetchRecordsOperation ( recordIDs : [ recordId ] )
// D o n ' t d o w n l o a d t h e f i l e ; w e ' r e j u s t u s i n g t h e f e t c h t o c h e c k w h e t h e r o r
@ -375,53 +399,61 @@ import CloudKit
fetchOperation . perRecordCompletionBlock = { ( record , recordId , error ) in
let outcome = outcomeForCloudKitError ( error : error ,
remainingRetries : remainingRetries ,
label : " Check for Record " )
remainingRetries : remainingRetries ,
label : " Check for Record " )
switch outcome {
case . success :
guard let record = record else {
owsFailDebug ( " missing fetching record. " )
failure ( invalidServiceResponseError ( ) )
resolver. reject ( invalidServiceResponseError ( ) )
return
}
// R e c o r d f o u n d .
success ( record )
resolver. fulfill ( record )
case . failureDoNotRetry ( let outcomeError ) :
failure ( outcomeError )
resolver. reject ( outcomeError )
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
checkForFileInCloud ( recordName : recordName ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
remainingRetries : remainingRetries - 1 )
. done { ( record ) in
resolver . fulfill ( record )
} . catch { ( error ) in
resolver . reject ( error )
} . retainUntilComplete ( )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
checkForFileInCloud ( recordName : recordName ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
remainingRetries : remainingRetries - 1 )
. done { ( record ) in
resolver . fulfill ( record )
} . catch { ( error ) in
resolver . reject ( error )
} . retainUntilComplete ( )
}
case . unknownItem :
// R e c o r d n o t f o u n d .
success ( nil )
resolver. fulfill ( nil )
}
}
database ( ) . add ( fetchOperation )
return promise
}
@objc
public class func checkForManifestInCloud ( recipientId : String ,
success : @ escaping ( Bool ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
public class func checkForManifestInCloudObjc ( recipientId : String ) -> AnyPromise {
return AnyPromise ( checkForManifestInCloud ( recipientId : recipientId ) )
}
public class func checkForManifestInCloud ( recipientId : String ) -> Promise < Bool > {
let recordName = recordNameForManifest ( recipientId : recipientId )
checkForFileInCloud ( recordName : recordName ,
remainingRetries : maxRetries ,
success : { ( record ) in
success ( record != nil )
} ,
failure : failure )
return checkForFileInCloud ( recordName : recordName ,
remainingRetries : maxRetries )
. map { ( record ) in
return record != nil
}
}
@objc
@ -499,8 +531,8 @@ import CloudKit
queryOperation . queryCompletionBlock = { ( cursor , error ) in
let outcome = outcomeForCloudKitError ( error : error ,
remainingRetries : remainingRetries ,
label : " Fetch All Records " )
remainingRetries : remainingRetries ,
label : " Fetch All Records " )
switch outcome {
case . success :
if let cursor = cursor {
@ -619,8 +651,8 @@ import CloudKit
fetchOperation . perRecordCompletionBlock = { ( record , recordId , error ) in
let outcome = outcomeForCloudKitError ( error : error ,
remainingRetries : remainingRetries ,
label : " Download Record " )
remainingRetries : remainingRetries ,
label : " Download Record " )
switch outcome {
case . success :
guard let record = record else {
@ -660,28 +692,59 @@ import CloudKit
// MARK: - A c c e s s
@objc public enum BackupError : Int , Error {
case couldNotDetermineAccountStatus
case noAccount
case restrictedAccountStatus
}
@objc
public class func checkCloudKitAccess ( completion : @ escaping ( Bool ) -> Void ) {
CKContainer . default ( ) . accountStatus ( completionHandler : { ( accountStatus , error ) in
DispatchQueue . main . async {
switch accountStatus {
case . couldNotDetermine :
Logger . error ( " could not determine CloudKit account status: \( String ( describing : error ) ) . " )
OWSAlerts . showErrorAlert ( message : NSLocalizedString ( " CLOUDKIT_STATUS_COULD_NOT_DETERMINE " , comment : " Error indicating that the app could not determine that user's CloudKit account status " ) )
completion ( false )
case . noAccount :
Logger . error ( " no CloudKit account. " )
OWSAlerts . showErrorAlert ( message : NSLocalizedString ( " CLOUDKIT_STATUS_NO_ACCOUNT " , comment : " Error indicating that user does not have an iCloud account. " ) )
completion ( false )
case . restricted :
Logger . error ( " restricted CloudKit account. " )
OWSAlerts . showErrorAlert ( message : NSLocalizedString ( " CLOUDKIT_STATUS_RESTRICTED " , comment : " Error indicating that the app was prevented from accessing the user's CloudKit account. " ) )
completion ( false )
case . available :
completion ( true )
}
public class func ensureCloudKitAccessObjc ( ) -> AnyPromise {
return AnyPromise ( ensureCloudKitAccess ( ) )
}
public class func ensureCloudKitAccess ( ) -> Promise < Void > {
let ( promise , resolver ) = Promise < Void > . pending ( )
CKContainer . default ( ) . accountStatus { ( accountStatus , error ) in
if let error = error {
Logger . error ( " Unknown error: \( String ( describing : error ) ) . " )
resolver . reject ( error )
return
}
switch accountStatus {
case . couldNotDetermine :
Logger . error ( " could not determine CloudKit account status: \( String ( describing : error ) ) . " )
resolver . reject ( BackupError . couldNotDetermineAccountStatus )
case . noAccount :
Logger . error ( " no CloudKit account. " )
resolver . reject ( BackupError . noAccount )
case . restricted :
Logger . error ( " restricted CloudKit account. " )
resolver . reject ( BackupError . restrictedAccountStatus )
case . available :
Logger . verbose ( " CloudKit access okay. " )
resolver . fulfill ( ( ) )
}
}
return promise
}
@objc
public class func errorMessage ( forCloudKitAccessError error : Error ) -> String {
if let backupError = error as ? BackupError {
Logger . error ( " Backup error: \( String ( describing : backupError ) ) . " )
switch backupError {
case . couldNotDetermineAccountStatus :
return NSLocalizedString ( " CLOUDKIT_STATUS_COULD_NOT_DETERMINE " , comment : " Error indicating that the app could not determine that user's iCloud account status " )
case . noAccount :
return NSLocalizedString ( " CLOUDKIT_STATUS_NO_ACCOUNT " , comment : " Error indicating that user does not have an iCloud account. " )
case . restrictedAccountStatus :
return NSLocalizedString ( " CLOUDKIT_STATUS_RESTRICTED " , comment : " Error indicating that the app was prevented from accessing the user's iCloud account. " )
}
} )
} else {
Logger . error ( " Unknown error: \( String ( describing : error ) ) . " )
return NSLocalizedString ( " CLOUDKIT_STATUS_COULD_NOT_DETERMINE " , comment : " Error indicating that the app could not determine that user's iCloud account status " )
}
}
// MARK: - R e t r y
@ -696,8 +759,8 @@ import CloudKit
}
private class func outcomeForCloudKitError ( error : Error ? ,
remainingRetries : Int ,
label : String ) -> APIOutcome {
remainingRetries : Int ,
label : String ) -> APIOutcome {
if let error = error as ? CKError {
if error . code = = CKError . unknownItem {
// T h i s i s n o t a l w a y s a n e r r o r f o r o u r p u r p o s e s .