Stub out fetching feed changes.

This commit is contained in:
Maurice Parker 2020-03-29 11:53:52 -05:00
parent 573cee0fd6
commit 3b31f2562d
6 changed files with 178 additions and 119 deletions

View File

@ -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 */,

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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)
// }
}

View File

@ -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) {
}
}

View File

@ -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()
})
}
}
}