Handle edge case where the user deletes the iCloud data.

This commit is contained in:
Maurice Parker 2020-04-06 02:15:28 -05:00
parent 2ec56b52fd
commit 6364539608
4 changed files with 186 additions and 43 deletions

View File

@ -103,6 +103,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
completion(.success(()))
case .failure(let error):
self.database.resetSelectedForProcessing(syncStatuses.map({ $0.articleID }) )
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -133,7 +134,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing article statuses...")
articlesZone.fetchChangesInZone() { result in
articlesZone.refreshArticleStatus() { result in
os_log(.debug, log: self.log, "Done refreshing article statuses.")
switch result {
case .success:
@ -284,6 +285,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
feed.editedName = name
completion(.success(()))
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -298,6 +300,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
container.removeWebFeed(feed)
completion(.success(()))
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -313,6 +316,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
toContainer.addWebFeed(feed)
completion(.success(()))
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -327,6 +331,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
container.addWebFeed(feed)
completion(.success(()))
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -342,6 +347,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
container.addWebFeed(feed)
completion(.success(()))
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -360,6 +366,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
}
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -374,6 +381,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
folder.name = name
completion(.success(()))
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -388,6 +396,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
account.removeFolder(folder)
completion(.success(()))
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -434,6 +443,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
}
case .failure(let error):
self.processAccountError(account, error)
completion(.failure(error))
}
}
@ -548,21 +558,35 @@ private extension CloudKitAccountDelegate {
}
case .failure(let error):
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
}
}
case .failure(let error):
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
}
}
case .failure(let error):
self.processAccountError(account, error)
self.refreshProgress.clear()
completion(.failure(error))
}
}
}
func processAccountError(_ account: Account, _ error: Error) {
if case CloudKitZoneError.userDeletedZone = error {
account.removeFeeds(account.topLevelWebFeeds)
for folder in account.folders ?? Set<Folder>() {
account.removeFolder(folder)
}
}
}
}

View File

@ -219,6 +219,40 @@ final class CloudKitAccountZone: CloudKitZone {
let predicate = NSPredicate(format: "isAccount = \"1\"")
let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate)
database?.perform(ckQuery, inZoneWith: Self.zoneID) { [weak self] records, error in
guard let self = self else { return }
switch CloudKitZoneResult.resolve(error) {
case .success:
DispatchQueue.main.async {
if records!.count > 0 {
completion(.success(records![0].externalID))
} else {
self.createContainer(name: "Account", isAccount: true, completion: completion)
}
}
case .retry(let timeToWait):
self.retryIfPossible(after: timeToWait) {
self.findOrCreateAccount(completion: completion)
}
case .zoneNotFound, .userDeletedZone:
self.createZoneRecord() { result in
switch result {
case .success:
self.findOrCreateAccount(completion: completion)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(CloudKitError(error)))
}
}
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
}
}
}
query(ckQuery) { result in
switch result {
case .success(let records):

View File

@ -59,19 +59,63 @@ final class CloudKitArticlesZone: CloudKitZone {
self.database = container.privateCloudDatabase
}
func refreshArticleStatus(completion: @escaping ((Result<Void, Error>) -> Void)) {
fetchChangesInZone() { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result {
case .success:
self.refreshArticleStatus(completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.failure(error))
}
}
}
}
func sendArticleStatus(_ syncStatuses: [SyncStatus], starredArticles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
var records = makeStatusRecords(syncStatuses)
makeArticleRecordsIfNecessary(starredArticles) { result in
switch result {
case .success(let articleRecords):
records.append(contentsOf: articleRecords)
self.modify(recordsToSave: records, recordIDsToDelete: [], completion: completion)
self.modify(recordsToSave: records, recordIDsToDelete: []) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: starredArticles, completion: completion)
}
}
case .failure(let error):
completion(.failure(error))
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: starredArticles, completion: completion)
}
}
}
func handleSendArticleStatusError(_ error: Error, syncStatuses: [SyncStatus], starredArticles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result {
case .success:
self.sendArticleStatus(syncStatuses, starredArticles: starredArticles, completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.failure(error))
}
}
}
private extension CloudKitArticlesZone {

View File

@ -16,7 +16,11 @@ enum CloudKitZoneError: LocalizedError {
case unknown
var errorDescription: String? {
return NSLocalizedString("An unexpected CloudKit error occurred.", comment: "An unexpected CloudKit error occurred.")
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.")
}
}
}
@ -63,19 +67,12 @@ extension CloudKitZone {
return CKRecord.ID(recordName: UUID().uuidString, zoneID: Self.zoneID)
}
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)
}
}
}
func retryIfPossible(after: Double, block: @escaping () -> ()) {
let delayTime = DispatchTime.now() + after
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
block()
})
}
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
let note = CKRecordZoneNotification(fromRemoteNotificationDictionary: userInfo)
@ -91,6 +88,39 @@ extension CloudKitZone {
completion()
}
}
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)
}
}
}
/// Checks to see if the record described in the query exists by retrieving only the testField parameter field.
func exists(_ query: CKQuery, completion: @escaping (Result<Bool, Error>) -> Void) {
@ -108,10 +138,25 @@ extension CloudKitZone {
DispatchQueue.main.async {
completion(.success(recordFound))
}
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))
}
}
}
case .retry(let timeToWait):
self?.retryIfPossible(after: timeToWait) {
self?.exists(query, completion: completion)
}
case .userDeletedZone:
DispatchQueue.main.async {
completion(.failure(CloudKitZoneError.userDeletedZone))
}
default:
DispatchQueue.main.async {
completion(.failure(CloudKitError(error!)))
@ -139,6 +184,17 @@ extension CloudKitZone {
completion(.failure(CloudKitZoneError.unknown))
}
}
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))
}
}
}
case .retry(let timeToWait):
self?.retryIfPossible(after: timeToWait) {
self?.query(query, completion: completion)
@ -174,6 +230,17 @@ extension CloudKitZone {
completion(.failure(CloudKitZoneError.unknown))
}
}
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))
}
}
}
case .retry(let timeToWait):
self?.retryIfPossible(after: timeToWait) {
self?.fetch(externalID: externalID, completion: completion)
@ -533,30 +600,4 @@ private extension CloudKitZone {
return config
}
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 retryIfPossible(after: Double, block: @escaping () -> ()) {
let delayTime = DispatchTime.now() + after
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
block()
})
}
}