Implement CloudKit feed add.
This commit is contained in:
parent
ecc20ad9e3
commit
6ce82fc28b
@ -545,7 +545,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
let feed = WebFeed(account: self, url: url, metadata: metadata)
|
let feed = WebFeed(account: self, url: url, metadata: metadata)
|
||||||
feed.name = name
|
feed.name = name
|
||||||
feed.homePageURL = homePageURL
|
feed.homePageURL = homePageURL
|
||||||
|
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -683,7 +682,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping DatabaseCompletionBlock) {
|
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping DatabaseCompletionBlock) {
|
||||||
// Used only by an On My Mac account.
|
// Used only by an On My Mac and iCloud accounts.
|
||||||
webFeed.takeSettings(from: parsedFeed)
|
webFeed.takeSettings(from: parsedFeed)
|
||||||
let webFeedIDsAndItems = [webFeed.webFeedID: parsedFeed.items]
|
let webFeedIDsAndItems = [webFeed.webFeedID: parsedFeed.items]
|
||||||
update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: false, completion: completion)
|
update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: false, completion: completion)
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
|
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
|
||||||
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
|
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
|
||||||
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; };
|
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; };
|
||||||
51C034DF242D65D20014DC71 /* CloudKitResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034DE242D65D20014DC71 /* CloudKitResult.swift */; };
|
51C034DF242D65D20014DC71 /* CloudKitZoneResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */; };
|
||||||
51C034E1242D660D0014DC71 /* CKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034E0242D660D0014DC71 /* CKError+Extensions.swift */; };
|
51C034E1242D660D0014DC71 /* CKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C034E0242D660D0014DC71 /* CKError+Extensions.swift */; };
|
||||||
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
|
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
|
||||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
|
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
|
||||||
@ -287,7 +287,7 @@
|
|||||||
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
|
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
|
||||||
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
||||||
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = "<group>"; };
|
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = "<group>"; };
|
||||||
51C034DE242D65D20014DC71 /* CloudKitResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitResult.swift; sourceTree = "<group>"; };
|
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitZoneResult.swift; sourceTree = "<group>"; };
|
||||||
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKError+Extensions.swift"; sourceTree = "<group>"; };
|
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKError+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
|
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
|
||||||
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
|
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
|
||||||
@ -514,8 +514,8 @@
|
|||||||
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */,
|
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */,
|
||||||
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
||||||
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
|
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
|
||||||
51C034DE242D65D20014DC71 /* CloudKitResult.swift */,
|
|
||||||
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
||||||
|
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */,
|
||||||
);
|
);
|
||||||
path = CloudKit;
|
path = CloudKit;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1184,7 +1184,7 @@
|
|||||||
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
|
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
|
||||||
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */,
|
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */,
|
||||||
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */,
|
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */,
|
||||||
51C034DF242D65D20014DC71 /* CloudKitResult.swift in Sources */,
|
51C034DF242D65D20014DC71 /* CloudKitZoneResult.swift in Sources */,
|
||||||
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */,
|
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */,
|
||||||
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */,
|
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */,
|
||||||
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,
|
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,
|
||||||
|
@ -45,14 +45,6 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
return refresher.progress
|
return refresher.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
// init() {
|
|
||||||
// accountZone.startUp() { result in
|
|
||||||
// if case .failure(let error) = result {
|
|
||||||
// os_log(.error, log: self.log, "Account zone startup error: %@.", error.localizedDescription)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
init(dataFolder: String) {
|
init(dataFolder: String) {
|
||||||
accountZone = CloudKitAccountZone(container: container)
|
accountZone = CloudKitAccountZone(container: container)
|
||||||
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
|
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
|
||||||
@ -140,22 +132,35 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
self.accountZone.createFeed(url: urlString, editedName: name) { result in
|
||||||
|
switch result {
|
||||||
InitialFeedDownloader.download(url) { parsedFeed in
|
case .success(let externalID):
|
||||||
self.refreshProgress.completeTask()
|
|
||||||
|
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
||||||
|
|
||||||
|
InitialFeedDownloader.download(url) { parsedFeed in
|
||||||
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
if let parsedFeed = parsedFeed {
|
if let parsedFeed = parsedFeed {
|
||||||
account.update(feed, with: parsedFeed, {_ in})
|
account.update(feed, with: parsedFeed, {_ in
|
||||||
|
|
||||||
|
feed.editedName = name
|
||||||
|
feed.externalID = externalID
|
||||||
|
|
||||||
|
container.addWebFeed(feed)
|
||||||
|
completion(.success(feed))
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
self.refreshProgress.completeTask()
|
||||||
|
completion(.failure(error)) // TODO: need to handle userDeletedZone
|
||||||
}
|
}
|
||||||
|
|
||||||
feed.editedName = name
|
|
||||||
|
|
||||||
container.addWebFeed(feed)
|
|
||||||
completion(.success(feed))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure:
|
case .failure:
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
completion(.failure(AccountError.createErrorNotFound))
|
completion(.failure(AccountError.createErrorNotFound))
|
||||||
|
@ -9,17 +9,17 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CloudKit
|
import CloudKit
|
||||||
|
|
||||||
enum CloudKitResult {
|
enum CloudKitZoneResult {
|
||||||
case success
|
case success
|
||||||
case retry(afterSeconds: Double)
|
case retry(afterSeconds: Double)
|
||||||
case chunk
|
case limitExceeded
|
||||||
case changeTokenExpired
|
case changeTokenExpired
|
||||||
case partialFailure
|
case partialFailure(errors: [CKRecord.ID: CKError])
|
||||||
case serverRecordChanged
|
case serverRecordChanged
|
||||||
case noZone
|
case noZone
|
||||||
case failure(error: Error)
|
case failure(error: Error)
|
||||||
|
|
||||||
static func resolve(_ error: Error?) -> CloudKitResult {
|
static func resolve(_ error: Error?) -> CloudKitZoneResult {
|
||||||
|
|
||||||
guard error != nil else { return .success }
|
guard error != nil else { return .success }
|
||||||
|
|
||||||
@ -39,11 +39,17 @@ enum CloudKitResult {
|
|||||||
case .serverRecordChanged:
|
case .serverRecordChanged:
|
||||||
return .serverRecordChanged
|
return .serverRecordChanged
|
||||||
case .partialFailure:
|
case .partialFailure:
|
||||||
return .partialFailure
|
if let partialErrors = ckError.userInfo[CKPartialErrorsByItemIDKey] as? [CKRecord.ID: CKError] {
|
||||||
|
if anyZoneErrors(partialErrors) {
|
||||||
|
return .noZone
|
||||||
|
} else {
|
||||||
|
return .partialFailure(errors: partialErrors)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .failure(error: error!)
|
||||||
|
}
|
||||||
case .limitExceeded:
|
case .limitExceeded:
|
||||||
return .chunk
|
return .limitExceeded
|
||||||
case .zoneNotFound, .userDeletedZone:
|
|
||||||
return .noZone
|
|
||||||
default:
|
default:
|
||||||
return .failure(error: error!)
|
return .failure(error: error!)
|
||||||
}
|
}
|
||||||
@ -51,3 +57,11 @@ enum CloudKitResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension CloudKitZoneResult {
|
||||||
|
|
||||||
|
static func anyZoneErrors(_ errors: [CKRecord.ID: CKError]) -> Bool {
|
||||||
|
return errors.values.contains(where: { $0.code == .zoneNotFound || $0.code == .userDeletedZone } )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import CloudKit
|
import CloudKit
|
||||||
|
|
||||||
public enum CloudKitZoneError: Error {
|
public enum CloudKitZoneError: Error {
|
||||||
|
case userDeletedZone
|
||||||
case unknown
|
case unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,14 +147,13 @@ extension CloudKitZone {
|
|||||||
|
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
switch CloudKitResult.resolve(error) {
|
switch CloudKitZoneResult.resolve(error) {
|
||||||
case .success:
|
case .success:
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
case .noZone:
|
case .zoneNotFound:
|
||||||
self.createZoneRecord() { result in
|
self.createZoneRecord() { result in
|
||||||
// TODO: Need to rebuild (push) zone data here...
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
self.modify(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
self.modify(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||||
@ -161,11 +161,15 @@ extension CloudKitZone {
|
|||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case .userDeletedZone:
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.failure(CloudKitZoneError.userDeletedZone))
|
||||||
|
}
|
||||||
case .retry(let timeToWait):
|
case .retry(let timeToWait):
|
||||||
self.retryOperationIfPossible(retryAfter: timeToWait) {
|
self.retryOperationIfPossible(retryAfter: timeToWait) {
|
||||||
self.modify(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
self.modify(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||||
}
|
}
|
||||||
case .chunk:
|
case .limitExceeded:
|
||||||
/// CloudKit says maximum number of items in a single request is 400.
|
/// CloudKit says maximum number of items in a single request is 400.
|
||||||
/// So I think 300 should be fine by them.
|
/// So I think 300 should be fine by them.
|
||||||
let chunkedRecords = recordsToStore.chunked(into: 300)
|
let chunkedRecords = recordsToStore.chunked(into: 300)
|
||||||
@ -173,7 +177,9 @@ extension CloudKitZone {
|
|||||||
self.modify(recordsToStore: chunk, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
self.modify(recordsToStore: chunk, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return
|
DispatchQueue.main.async {
|
||||||
|
completion(.failure(error!))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
74
Frameworks/Account/CloudKit/CloudKitZoneResult.swift
Normal file
74
Frameworks/Account/CloudKit/CloudKitZoneResult.swift
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
//
|
||||||
|
// CloudKitResult.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 3/26/20.
|
||||||
|
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CloudKit
|
||||||
|
|
||||||
|
enum CloudKitZoneResult {
|
||||||
|
case success
|
||||||
|
case retry(afterSeconds: Double)
|
||||||
|
case limitExceeded
|
||||||
|
case changeTokenExpired
|
||||||
|
case partialFailure(errors: [CKRecord.ID: CKError])
|
||||||
|
case serverRecordChanged
|
||||||
|
case zoneNotFound
|
||||||
|
case userDeletedZone
|
||||||
|
case failure(error: Error)
|
||||||
|
|
||||||
|
static func resolve(_ error: Error?) -> CloudKitZoneResult {
|
||||||
|
|
||||||
|
guard error != nil else { return .success }
|
||||||
|
|
||||||
|
guard let ckError = error as? CKError else {
|
||||||
|
return .failure(error: error!)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ckError.code {
|
||||||
|
case .serviceUnavailable, .requestRateLimited, .zoneBusy:
|
||||||
|
if let retry = ckError.userInfo[CKErrorRetryAfterKey] as? Double {
|
||||||
|
return .retry(afterSeconds: retry)
|
||||||
|
} else {
|
||||||
|
return .failure(error: error!)
|
||||||
|
}
|
||||||
|
case .changeTokenExpired:
|
||||||
|
return .changeTokenExpired
|
||||||
|
case .serverRecordChanged:
|
||||||
|
return .serverRecordChanged
|
||||||
|
case .partialFailure:
|
||||||
|
if let partialErrors = ckError.userInfo[CKPartialErrorsByItemIDKey] as? [CKRecord.ID: CKError] {
|
||||||
|
if let zoneResult = anyZoneErrors(partialErrors) {
|
||||||
|
return zoneResult
|
||||||
|
} else {
|
||||||
|
return .partialFailure(errors: partialErrors)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .failure(error: error!)
|
||||||
|
}
|
||||||
|
case .limitExceeded:
|
||||||
|
return .limitExceeded
|
||||||
|
default:
|
||||||
|
return .failure(error: error!)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CloudKitZoneResult {
|
||||||
|
|
||||||
|
static func anyZoneErrors(_ errors: [CKRecord.ID: CKError]) -> CloudKitZoneResult? {
|
||||||
|
if errors.values.contains(where: { $0.code == .zoneNotFound } ) {
|
||||||
|
return .zoneNotFound
|
||||||
|
}
|
||||||
|
if errors.values.contains(where: { $0.code == .userDeletedZone } ) {
|
||||||
|
return .userDeletedZone
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -58,16 +58,12 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
BatchUpdate.shared.start()
|
|
||||||
|
|
||||||
account.createWebFeed(url: url.absoluteString, name: title, container: container) { result in
|
account.createWebFeed(url: url.absoluteString, name: title, container: container) { result in
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.endShowingProgress()
|
self.endShowingProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
BatchUpdate.shared.end()
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let feed):
|
case .success(let feed):
|
||||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed])
|
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user