Add user feed subscription management.
This commit is contained in:
parent
c01cc7cb05
commit
71b5c8bc86
|
@ -60,6 +60,7 @@
|
|||
519E84A62433D49000D238B0 /* OPMLNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A52433D49000D238B0 /* OPMLNormalizer.swift */; };
|
||||
519E84A82434C5EF00D238B0 /* CloudKitArticlesZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */; };
|
||||
519E84AC2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */; };
|
||||
51B544672438F410003F03BF /* CloudKitContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B544662438F410003F03BF /* CloudKitContainer.swift */; };
|
||||
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
|
||||
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
|
||||
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; };
|
||||
|
@ -297,6 +298,7 @@
|
|||
519E84A52433D49000D238B0 /* OPMLNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLNormalizer.swift; sourceTree = "<group>"; };
|
||||
519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZone.swift; sourceTree = "<group>"; };
|
||||
519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZoneDelegate.swift; sourceTree = "<group>"; };
|
||||
51B544662438F410003F03BF /* CloudKitContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitContainer.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>"; };
|
||||
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = "<group>"; };
|
||||
|
@ -529,6 +531,7 @@
|
|||
512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */,
|
||||
519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */,
|
||||
519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */,
|
||||
51B544662438F410003F03BF /* CloudKitContainer.swift */,
|
||||
5150FFFD243823B800C1A442 /* CloudKitError.swift */,
|
||||
5150FFFF2438682300C1A442 /* CloudKitPublicZone.swift */,
|
||||
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
||||
|
@ -1156,6 +1159,7 @@
|
|||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
|
||||
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */,
|
||||
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */,
|
||||
51B544672438F410003F03BF /* CloudKitContainer.swift in Sources */,
|
||||
9EA643CF2391D3560018A28C /* FeedlyAddExistingFeedOperation.swift in Sources */,
|
||||
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,
|
||||
|
|
|
@ -436,7 +436,9 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||
accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress)
|
||||
articlesZone.delegate = CloudKitArticlesZoneDelegate(account: account, database: database, articlesZone: articlesZone)
|
||||
|
||||
// Check to see if this is a new account and initialize anything we need
|
||||
if account.externalID == nil {
|
||||
CloudKitContainer.fetchUserRecordID()
|
||||
accountZone.findOrCreateAccount() { result in
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
|
@ -451,7 +453,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||
}
|
||||
}
|
||||
zones.forEach { zone in
|
||||
zone.subscribe()
|
||||
zone.subscribeToZoneChanges()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// CloudKitContainer.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/4/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
struct CloudKitContainer {
|
||||
|
||||
private static let userRecordIDKey = "cloudkit.server.userRecordID"
|
||||
|
||||
static var userRecordID: String? {
|
||||
get {
|
||||
return UserDefaults.standard.string(forKey: Self.userRecordIDKey)
|
||||
}
|
||||
set {
|
||||
guard let userRecordID = newValue else {
|
||||
UserDefaults.standard.removeObject(forKey: Self.userRecordIDKey)
|
||||
return
|
||||
}
|
||||
UserDefaults.standard.set(userRecordID, forKey: Self.userRecordIDKey)
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchUserRecordID() {
|
||||
guard Self.userRecordID == nil else { return }
|
||||
CKContainer.default().fetchUserRecordID { recordID, error in
|
||||
guard let recordID = recordID, error == nil else {
|
||||
return
|
||||
}
|
||||
Self.userRecordID = recordID.recordName
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -31,10 +31,18 @@ final class CloudKitPublicZone: CloudKitZone {
|
|||
}
|
||||
}
|
||||
|
||||
struct CloudKitUserWebFeedCheck {
|
||||
static let recordType = "WebFeedCheck"
|
||||
struct Fields {
|
||||
static let webFeed = "webFeed"
|
||||
static let lastCheck = "lastCheck"
|
||||
}
|
||||
}
|
||||
|
||||
struct CloudKitUserSubscription {
|
||||
static let recordType = "UserSubscription"
|
||||
struct Fields {
|
||||
static let user = "user"
|
||||
static let userRecordID = "userRecordID"
|
||||
static let webFeed = "webFeed"
|
||||
static let subscriptionID = "subscriptionID"
|
||||
}
|
||||
|
@ -45,18 +53,86 @@ final class CloudKitPublicZone: CloudKitZone {
|
|||
self.database = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
func subscribe() {}
|
||||
func subscribeToZoneChanges() {}
|
||||
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func createSubscription(_ webFeed: WebFeed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
completion(.success(()))
|
||||
let webFeedRecordID = CKRecord.ID(recordName: webFeed.url.md5String, zoneID: Self.zoneID)
|
||||
let webFeedRecord = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: webFeedRecordID)
|
||||
|
||||
save(webFeedRecord) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
let webFeedRecordRef = CKRecord.Reference(recordID: webFeedRecordID, action: .none)
|
||||
let predicate = NSPredicate(format: "webFeed = %@", webFeedRecordRef)
|
||||
let subscription = CKQuerySubscription(recordType: CloudKitWebFeed.recordType, predicate: predicate, options: [.firesOnRecordUpdate])
|
||||
|
||||
let info = CKSubscription.NotificationInfo()
|
||||
info.shouldSendContentAvailable = true
|
||||
info.desiredKeys = [CloudKitWebFeed.Fields.httpLastModified, CloudKitWebFeed.Fields.httpEtag]
|
||||
subscription.notificationInfo = info
|
||||
|
||||
self.save(subscription) { result in
|
||||
switch result {
|
||||
case .success(let subscription):
|
||||
|
||||
let userSubscriptionRecord = CKRecord(recordType: CloudKitUserSubscription.recordType, recordID: self.generateRecordID())
|
||||
userSubscriptionRecord[CloudKitUserSubscription.Fields.userRecordID] = CloudKitContainer.userRecordID
|
||||
userSubscriptionRecord[CloudKitUserSubscription.Fields.webFeed] = webFeedRecordRef
|
||||
userSubscriptionRecord[CloudKitUserSubscription.Fields.subscriptionID] = subscription.subscriptionID
|
||||
|
||||
self.save(userSubscriptionRecord, completion: completion)
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the subscription for the given feed along with its supporting record
|
||||
func removeSubscription(_ webFeed: WebFeed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
completion(.success(()))
|
||||
guard let userRecordID = CloudKitContainer.userRecordID else {
|
||||
completion(.failure(CloudKitZoneError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
let webFeedRecordID = CKRecord.ID(recordName: webFeed.url.md5String, zoneID: Self.zoneID)
|
||||
let webFeedRecordRef = CKRecord.Reference(recordID: webFeedRecordID, action: .none)
|
||||
let predicate = NSPredicate(format: "user = %@ AND webFeed = %@", userRecordID, webFeedRecordRef)
|
||||
let ckQuery = CKQuery(recordType: CloudKitUserSubscription.recordType, predicate: predicate)
|
||||
|
||||
query(ckQuery) { result in
|
||||
switch result {
|
||||
case .success(let records):
|
||||
|
||||
if records.count > 0, let subscriptionID = records[0][CloudKitUserSubscription.Fields.subscriptionID] as? String {
|
||||
self.delete(subscriptionID: subscriptionID) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.delete(recordID: records[0].recordID, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ protocol CloudKitZone: class {
|
|||
func generateRecordID() -> CKRecord.ID
|
||||
|
||||
/// Subscribe to changes at a zone level
|
||||
func subscribe()
|
||||
func subscribeToZoneChanges()
|
||||
|
||||
/// Process a remove notification
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void)
|
||||
|
@ -59,27 +59,18 @@ extension CloudKitZone {
|
|||
return CKRecord.ID(recordName: UUID().uuidString, zoneID: Self.zoneID)
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
|
||||
func subscribeToZoneChanges() {
|
||||
let subscription = CKRecordZoneSubscription(zoneID: Self.zoneID)
|
||||
|
||||
let info = CKSubscription.NotificationInfo()
|
||||
info.shouldSendContentAvailable = true
|
||||
subscription.notificationInfo = info
|
||||
|
||||
database?.save(subscription) { _, error in
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
break
|
||||
case .retry(let timeToWait):
|
||||
self.retryIfPossible(after: timeToWait) {
|
||||
self.subscribe()
|
||||
}
|
||||
default:
|
||||
os_log(.error, log: self.log, "%@ zone fetch changes error: %@.", Self.zoneID.zoneName, error?.localizedDescription ?? "Unknown")
|
||||
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 receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
|
@ -200,6 +191,27 @@ extension CloudKitZone {
|
|||
modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion)
|
||||
}
|
||||
|
||||
/// Save the CKSubscription
|
||||
func save(_ subscription: CKSubscription, completion: @escaping (Result<CKSubscription, Error>) -> Void) {
|
||||
database?.save(subscription) { savedSubscription, error in
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
completion(.success((savedSubscription!)))
|
||||
case .retry(let timeToWait):
|
||||
self.retryIfPossible(after: timeToWait) {
|
||||
self.save(subscription, completion: completion)
|
||||
}
|
||||
default:
|
||||
completion(.failure(CloudKitError(error!)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a CKRecord using its recordID
|
||||
func delete(recordID: CKRecord.ID, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
modify(recordsToSave: [], recordIDsToDelete: [recordID], completion: completion)
|
||||
}
|
||||
|
||||
/// Delete a CKRecord using its externalID
|
||||
func delete(externalID: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let externalID = externalID else {
|
||||
|
@ -211,6 +223,22 @@ extension CloudKitZone {
|
|||
modify(recordsToSave: [], recordIDsToDelete: [recordID], completion: completion)
|
||||
}
|
||||
|
||||
/// Delete a CKSubscription
|
||||
func delete(subscriptionID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
database?.delete(withSubscriptionID: subscriptionID) { _, error in
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .retry(let timeToWait):
|
||||
self.retryIfPossible(after: timeToWait) {
|
||||
self.delete(subscriptionID: subscriptionID, completion: completion)
|
||||
}
|
||||
default:
|
||||
completion(.failure(CloudKitError(error!)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify and delete the supplied CKRecords and CKRecord.IDs
|
||||
func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let op = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete)
|
||||
|
|
Loading…
Reference in New Issue