Handle edge case where the user deletes the iCloud data.
This commit is contained in:
parent
2ec56b52fd
commit
6364539608
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -59,17 +59,61 @@ 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):
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,9 +16,13 @@ enum CloudKitZoneError: LocalizedError {
|
|||
case unknown
|
||||
|
||||
var errorDescription: String? {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol CloudKitZoneDelegate: class {
|
||||
func cloudKitDidChange(record: CKRecord);
|
||||
|
@ -63,18 +67,11 @@ 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) {
|
||||
|
@ -92,6 +89,39 @@ extension CloudKitZone {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var recordFound = false
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue