Stub out fetching feed changes.
This commit is contained in:
parent
573cee0fd6
commit
3b31f2562d
|
@ -34,6 +34,8 @@
|
|||
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
|
||||
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */; };
|
||||
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9803237CD4270028BCAA /* FeedIdentifier.swift */; };
|
||||
512DD4CB2431000600C17B1F /* CKRecord+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512DD4CA2431000600C17B1F /* CKRecord+Extensions.swift */; };
|
||||
512DD4CD2431098700C17B1F /* CloudKitAccountZoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */; };
|
||||
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; };
|
||||
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; };
|
||||
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; };
|
||||
|
@ -264,6 +266,8 @@
|
|||
510BD110232C3801002692E4 /* AccountMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadataFile.swift; sourceTree = "<group>"; };
|
||||
510BD112232C3E9D002692E4 /* WebFeedMetadataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedMetadataFile.swift; sourceTree = "<group>"; };
|
||||
511B9803237CD4270028BCAA /* FeedIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIdentifier.swift; sourceTree = "<group>"; };
|
||||
512DD4CA2431000600C17B1F /* CKRecord+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKRecord+Extensions.swift"; sourceTree = "<group>"; };
|
||||
512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountZoneDelegate.swift; sourceTree = "<group>"; };
|
||||
513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = "<group>"; };
|
||||
513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = "<group>"; };
|
||||
5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = "<group>"; };
|
||||
|
@ -512,8 +516,10 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
51C034E0242D660D0014DC71 /* CKError+Extensions.swift */,
|
||||
512DD4CA2431000600C17B1F /* CKRecord+Extensions.swift */,
|
||||
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
||||
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
|
||||
512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */,
|
||||
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
||||
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */,
|
||||
);
|
||||
|
@ -1076,6 +1082,7 @@
|
|||
9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */,
|
||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
|
||||
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
|
||||
512DD4CB2431000600C17B1F /* CKRecord+Extensions.swift in Sources */,
|
||||
3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */,
|
||||
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */,
|
||||
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */,
|
||||
|
@ -1174,6 +1181,7 @@
|
|||
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
|
||||
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
|
||||
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */,
|
||||
512DD4CD2431098700C17B1F /* CloudKitAccountZoneDelegate.swift in Sources */,
|
||||
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */,
|
||||
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// CKRecord+Extensions.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 3/29/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
extension CKRecord.ID {
|
||||
|
||||
var externalID: String {
|
||||
return recordName
|
||||
}
|
||||
|
||||
}
|
|
@ -132,7 +132,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
self.accountZone.createFeed(url: urlString, editedName: name) { result in
|
||||
self.accountZone.createWebFeed(url: urlString, editedName: name) { result in
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
|
||||
|
@ -231,6 +231,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||
}
|
||||
|
||||
func accountDidInitialize(_ account: Account) {
|
||||
accountZone.delegate = CloudKitAcountZoneDelegate(account: account)
|
||||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
|
|
|
@ -17,6 +17,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||
|
||||
let container: CKContainer
|
||||
let database: CKDatabase
|
||||
var delegate: CloudKitZoneDelegate? = nil
|
||||
|
||||
struct CloudKitWebFeed {
|
||||
static let recordType = "WebFeed"
|
||||
|
@ -32,7 +33,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||
}
|
||||
|
||||
/// Persist a web feed record to iCloud and return the external key
|
||||
func createFeed(url: String, editedName: String?, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
func createWebFeed(url: String, editedName: String?, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let record = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: generateRecordID())
|
||||
record[CloudKitWebFeed.Fields.url] = url
|
||||
if let editedName = editedName {
|
||||
|
@ -42,7 +43,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||
save(record: record) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(record.recordID.recordName))
|
||||
completion(.success(record.recordID.externalID))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
@ -56,60 +57,5 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||
}
|
||||
delete(externalID: externalID, completion: completion)
|
||||
}
|
||||
// private func fetchChangesInZones(_ callback: ((Error?) -> Void)? = nil) {
|
||||
// let changesOp = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIds, optionsByRecordZoneID: zoneIdOptions)
|
||||
// changesOp.fetchAllChanges = true
|
||||
//
|
||||
// changesOp.recordZoneChangeTokensUpdatedBlock = { [weak self] zoneId, token, _ in
|
||||
// guard let self = self else { return }
|
||||
// guard let syncObject = self.syncObjects.first(where: { $0.zoneID == zoneId }) else { return }
|
||||
// syncObject.zoneChangesToken = token
|
||||
// }
|
||||
//
|
||||
// changesOp.recordChangedBlock = { [weak self] record in
|
||||
// /// The Cloud will return the modified record since the last zoneChangesToken, we need to do local cache here.
|
||||
// /// Handle the record:
|
||||
// guard let self = self else { return }
|
||||
// guard let syncObject = self.syncObjects.first(where: { $0.recordType == record.recordType }) else { return }
|
||||
// syncObject.add(record: record)
|
||||
// }
|
||||
//
|
||||
// changesOp.recordWithIDWasDeletedBlock = { [weak self] recordId, _ in
|
||||
// guard let self = self else { return }
|
||||
// guard let syncObject = self.syncObjects.first(where: { $0.zoneID == recordId.zoneID }) else { return }
|
||||
// syncObject.delete(recordID: recordId)
|
||||
// }
|
||||
//
|
||||
// changesOp.recordZoneFetchCompletionBlock = { [weak self](zoneId ,token, _, _, error) in
|
||||
// guard let self = self else { return }
|
||||
// switch ErrorHandler.shared.resultType(with: error) {
|
||||
// case .success:
|
||||
// guard let syncObject = self.syncObjects.first(where: { $0.zoneID == zoneId }) else { return }
|
||||
// syncObject.zoneChangesToken = token
|
||||
// case .retry(let timeToWait, _):
|
||||
// ErrorHandler.shared.retryOperationIfPossible(retryAfter: timeToWait, block: {
|
||||
// self.fetchChangesInZones(callback)
|
||||
// })
|
||||
// case .recoverableError(let reason, _):
|
||||
// switch reason {
|
||||
// case .changeTokenExpired:
|
||||
// /// The previousServerChangeToken value is too old and the client must re-sync from scratch
|
||||
// guard let syncObject = self.syncObjects.first(where: { $0.zoneID == zoneId }) else { return }
|
||||
// syncObject.zoneChangesToken = nil
|
||||
// self.fetchChangesInZones(callback)
|
||||
// default:
|
||||
// return
|
||||
// }
|
||||
// default:
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// changesOp.fetchRecordZoneChangesCompletionBlock = { error in
|
||||
// callback?(error)
|
||||
// }
|
||||
//
|
||||
// database.add(changesOp)
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// CloudKitAccountZoneDelegate.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 3/29/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
|
||||
weak var account: Account?
|
||||
|
||||
init(account: Account) {
|
||||
self.account = account
|
||||
}
|
||||
|
||||
func cloudKitDidChange(record: CKRecord) {
|
||||
switch record.recordType {
|
||||
case CloudKitAccountZone.CloudKitWebFeed.recordType:
|
||||
addWebFeed(record)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(record.recordType)")
|
||||
}
|
||||
}
|
||||
|
||||
func cloudKitDidDelete(recordType: CKRecord.RecordType, recordID: CKRecord.ID) {
|
||||
switch recordType {
|
||||
case CloudKitAccountZone.CloudKitWebFeed.recordType:
|
||||
removeWebFeed(recordID.externalID)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(recordID.externalID)")
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(_ record: CKRecord) {
|
||||
|
||||
}
|
||||
|
||||
func removeWebFeed(_ externalID: String) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -8,18 +8,24 @@
|
|||
|
||||
import CloudKit
|
||||
|
||||
public enum CloudKitZoneError: Error {
|
||||
enum CloudKitZoneError: Error {
|
||||
case userDeletedZone
|
||||
case invalidParameter
|
||||
case unknown
|
||||
}
|
||||
|
||||
public protocol CloudKitZone: class {
|
||||
protocol CloudKitZoneDelegate: class {
|
||||
func cloudKitDidChange(record: CKRecord);
|
||||
func cloudKitDidDelete(recordType: CKRecord.RecordType, recordID: CKRecord.ID)
|
||||
}
|
||||
|
||||
protocol CloudKitZone: class {
|
||||
|
||||
static var zoneID: CKRecordZone.ID { get }
|
||||
|
||||
var container: CKContainer { get }
|
||||
var database: CKDatabase { get }
|
||||
var delegate: CloudKitZoneDelegate? { get set }
|
||||
|
||||
// func prepare()
|
||||
|
||||
|
@ -38,57 +44,10 @@ public protocol CloudKitZone: class {
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
func generateRecordID() -> CKRecord.ID {
|
||||
return CKRecord.ID(recordName: UUID().uuidString, zoneID: Self.zoneID)
|
||||
}
|
||||
|
||||
func createZoneRecord(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
database.save(CKRecordZone(zoneID: Self.zoneID)) { (recordZone, error) in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func prepare() {
|
||||
// syncObjects.forEach {
|
||||
// $0.pipeToEngine = { [weak self] recordsToStore, recordIDsToDelete in
|
||||
// guard let self = self else { return }
|
||||
// self.syncRecordsToCloudKit(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func resumeLongLivedOperationIfPossible() {
|
||||
container.fetchAllLongLivedOperationIDs { [weak self]( opeIDs, error) in
|
||||
guard let self = self, error == nil, let ids = opeIDs else { return }
|
||||
|
@ -96,9 +55,6 @@ extension CloudKitZone {
|
|||
self.container.fetchLongLivedOperation(withID: id, completionHandler: { [weak self](ope, error) in
|
||||
guard let self = self, error == nil else { return }
|
||||
if let modifyOp = ope as? CKModifyRecordsOperation {
|
||||
modifyOp.modifyRecordsCompletionBlock = { (_,_,_) in
|
||||
print("Resume modify records success!")
|
||||
}
|
||||
self.container.add(modifyOp)
|
||||
}
|
||||
})
|
||||
|
@ -115,18 +71,16 @@ extension CloudKitZone {
|
|||
// })
|
||||
// }
|
||||
|
||||
public func save(record: CKRecord, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func save(record: CKRecord, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion)
|
||||
}
|
||||
|
||||
public func delete(externalID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func delete(externalID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID)
|
||||
modify(recordsToSave: [], recordIDsToDelete: [recordID], completion: completion)
|
||||
}
|
||||
|
||||
/// Sync local data to CloudKit
|
||||
/// For more about the savePolicy: https://developer.apple.com/documentation/cloudkit/ckrecordsavepolicy
|
||||
public func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let op = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete)
|
||||
|
||||
let config = CKOperation.Configuration()
|
||||
|
@ -169,8 +123,6 @@ extension CloudKitZone {
|
|||
self.modify(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||
}
|
||||
case .limitExceeded:
|
||||
/// CloudKit says maximum number of items in a single request is 400.
|
||||
/// So I think 300 should be fine by them.
|
||||
let chunkedRecords = recordsToSave.chunked(into: 300)
|
||||
for chunk in chunkedRecords {
|
||||
self.modify(recordsToSave: chunk, recordIDsToDelete: recordIDsToDelete, completion: completion)
|
||||
|
@ -185,12 +137,100 @@ extension CloudKitZone {
|
|||
database.add(op)
|
||||
}
|
||||
|
||||
func fetchChangesInZones(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let zoneConfig = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
|
||||
zoneConfig.previousServerChangeToken = changeToken
|
||||
let op = CKFetchRecordZoneChangesOperation(recordZoneIDs: [Self.zoneID], configurationsByRecordZoneID: [Self.zoneID: zoneConfig])
|
||||
op.fetchAllChanges = true
|
||||
|
||||
op.recordZoneChangeTokensUpdatedBlock = { [weak self] zoneId, token, _ in
|
||||
guard let self = self else { return }
|
||||
self.changeToken = token
|
||||
}
|
||||
|
||||
op.recordChangedBlock = { [weak self] record in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.cloudKitDidChange(record: record)
|
||||
}
|
||||
|
||||
op.recordWithIDWasDeletedBlock = { [weak self] recordId, recordType in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.cloudKitDidDelete(recordType: recordType, recordID: recordId)
|
||||
}
|
||||
|
||||
op.recordZoneFetchCompletionBlock = { [weak self](zoneId ,token, _, _, error) in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
self.changeToken = token
|
||||
case .retry(let timeToWait):
|
||||
self.retryOperationIfPossible(retryAfter: timeToWait) {
|
||||
self.fetchChangesInZones(completion: completion)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
op.fetchRecordZoneChangesCompletionBlock = { error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
database.add(op)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func createZoneRecord(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
database.save(CKRecordZone(zoneID: Self.zoneID)) { (recordZone, error) in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retryOperationIfPossible(retryAfter: Double, block: @escaping () -> ()) {
|
||||
let delayTime = DispatchTime.now() + retryAfter
|
||||
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: {
|
||||
block()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue