mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Remove centralized CloudKit syncing code.
This commit is contained in:
@@ -42,7 +42,6 @@
|
||||
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; };
|
||||
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; };
|
||||
514BF5202391B0DB00902FE8 /* SingleArticleFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BF51F2391B0DB00902FE8 /* SingleArticleFetcher.swift */; };
|
||||
515000002438682300C1A442 /* CloudKitPublicZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5150FFFF2438682300C1A442 /* CloudKitPublicZone.swift */; };
|
||||
5150FFFE243823B800C1A442 /* CloudKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5150FFFD243823B800C1A442 /* CloudKitError.swift */; };
|
||||
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */; };
|
||||
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */; };
|
||||
@@ -60,7 +59,6 @@
|
||||
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 /* CKContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B544662438F410003F03BF /* CKContainer+Extensions.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 */; };
|
||||
@@ -280,7 +278,6 @@
|
||||
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
514BF51F2391B0DB00902FE8 /* SingleArticleFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleArticleFetcher.swift; sourceTree = "<group>"; };
|
||||
5150FFFD243823B800C1A442 /* CloudKitError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitError.swift; sourceTree = "<group>"; };
|
||||
5150FFFF2438682300C1A442 /* CloudKitPublicZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitPublicZone.swift; sourceTree = "<group>"; };
|
||||
5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinImportResult.swift; sourceTree = "<group>"; };
|
||||
515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
|
||||
515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+RSWeb.swift"; sourceTree = "<group>"; };
|
||||
@@ -298,7 +295,6 @@
|
||||
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 /* CKContainer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKContainer+Extensions.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>"; };
|
||||
@@ -525,7 +521,6 @@
|
||||
5103A9D7242253DC00410853 /* CloudKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51B544662438F410003F03BF /* CKContainer+Extensions.swift */,
|
||||
512DD4CA2431000600C17B1F /* CKRecord+Extensions.swift */,
|
||||
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
||||
51E4DB2F2426353D0091EB5B /* CloudKitAccountZone.swift */,
|
||||
@@ -533,7 +528,6 @@
|
||||
519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */,
|
||||
519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */,
|
||||
5150FFFD243823B800C1A442 /* CloudKitError.swift */,
|
||||
5150FFFF2438682300C1A442 /* CloudKitPublicZone.swift */,
|
||||
51E4DB2D242633ED0091EB5B /* CloudKitZone.swift */,
|
||||
51C034DE242D65D20014DC71 /* CloudKitZoneResult.swift */,
|
||||
);
|
||||
@@ -1116,7 +1110,6 @@
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
|
||||
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
|
||||
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */,
|
||||
515000002438682300C1A442 /* CloudKitPublicZone.swift in Sources */,
|
||||
5103A9D92422546800410853 /* CloudKitAccountDelegate.swift in Sources */,
|
||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
|
||||
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */,
|
||||
@@ -1159,7 +1152,6 @@
|
||||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
|
||||
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */,
|
||||
846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */,
|
||||
51B544672438F410003F03BF /* CKContainer+Extensions.swift in Sources */,
|
||||
9EA643CF2391D3560018A28C /* FeedlyAddExistingFeedOperation.swift in Sources */,
|
||||
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// CKContainer+Extensions.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/4/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
extension CKContainer {
|
||||
|
||||
private static let userRecordIDKey = "cloudkit.server.userRecordID"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUserRecordID() {
|
||||
guard userRecordID == nil else { return }
|
||||
fetchUserRecordID { recordID, error in
|
||||
guard let recordID = recordID, error == nil else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.userRecordID = recordID.recordName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,10 +30,9 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
return CKContainer(identifier: "iCloud.\(orgID).NetNewsWire")
|
||||
}()
|
||||
|
||||
private lazy var zones: [CloudKitZone] = [accountZone, articlesZone, publicZone]
|
||||
private lazy var zones: [CloudKitZone] = [accountZone, articlesZone]
|
||||
private let accountZone: CloudKitAccountZone
|
||||
private let articlesZone: CloudKitArticlesZone
|
||||
private let publicZone: CloudKitPublicZone
|
||||
|
||||
private let refresher = LocalAccountRefresher()
|
||||
|
||||
@@ -49,7 +48,6 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
init(dataFolder: String) {
|
||||
accountZone = CloudKitAccountZone(container: container)
|
||||
articlesZone = CloudKitArticlesZone(container: container)
|
||||
publicZone = CloudKitPublicZone(container: container)
|
||||
|
||||
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3")
|
||||
database = SyncDatabase(databaseFilePath: databaseFilePath)
|
||||
@@ -195,17 +193,10 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
publicZone.manageSubscriptions(webFeedURLs) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success:
|
||||
self.accountZone.importOPML(rootExternalID: rootExternalID, items: normalizedItems) { _ in
|
||||
self.refreshAll(for: account, downloadFeeds: false, completion: completion)
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
// Add one task here to show we started immediately. We don't need to complete is because refreshAll clears everything at the end.
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
self.accountZone.importOPML(rootExternalID: rootExternalID, items: normalizedItems) { _ in
|
||||
self.refreshAll(for: account, downloadFeeds: false, completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -219,7 +210,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(4)
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(3)
|
||||
FeedFinder.find(url: url) { result in
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
@@ -250,13 +241,6 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
feed.externalID = externalID
|
||||
container.addWebFeed(feed)
|
||||
|
||||
self.publicZone.manageSubscriptions(account.flattenedWebFeedURLs) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
if case .failure(let error) = result {
|
||||
os_log(.error, log: self.log, "An error occurred while creating the subscription: %@", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
InitialFeedDownloader.download(url) { parsedFeed in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
@@ -302,25 +286,13 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
accountZone.removeWebFeed(feed, from: container) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success(let deleted):
|
||||
case .success:
|
||||
container.removeWebFeed(feed)
|
||||
if deleted {
|
||||
self.publicZone.manageSubscriptions(account.flattenedWebFeedURLs) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -484,7 +456,6 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
|
||||
// Check to see if this is a new account and initialize anything we need
|
||||
if account.externalID == nil {
|
||||
container.fetchUserRecordID()
|
||||
accountZone.findOrCreateAccount() { result in
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// CloudKitPublicZone.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Maurice Parker on 4/4/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
import os.log
|
||||
|
||||
final class CloudKitPublicZone: CloudKitZone {
|
||||
|
||||
static var zoneID: CKRecordZone.ID {
|
||||
return CKRecordZone.default().zoneID
|
||||
}
|
||||
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||
|
||||
weak var container: CKContainer?
|
||||
weak var database: CKDatabase?
|
||||
var delegate: CloudKitZoneDelegate?
|
||||
|
||||
struct CloudKitWebFeed {
|
||||
static let recordType = "WebFeed"
|
||||
struct Fields {
|
||||
static let url = "url"
|
||||
static let httpLastModified = "httpLastModified"
|
||||
static let httpEtag = "httpEtag"
|
||||
}
|
||||
}
|
||||
|
||||
struct CloudKitWebFeedCheck {
|
||||
static let recordType = "WebFeedCheck"
|
||||
struct Fields {
|
||||
static let webFeed = "webFeed"
|
||||
static let lastCheck = "lastCheck"
|
||||
}
|
||||
}
|
||||
|
||||
struct CloudKitUserSubscription {
|
||||
static let recordType = "UserSubscription"
|
||||
struct Fields {
|
||||
static let userRecordID = "userRecordID"
|
||||
static let webFeed = "webFeed"
|
||||
static let subscriptionID = "subscriptionID"
|
||||
}
|
||||
}
|
||||
|
||||
init(container: CKContainer) {
|
||||
self.container = container
|
||||
self.database = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
func subscribeToZoneChanges() {}
|
||||
|
||||
func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
/// Create any new subscriptions and delete any old ones
|
||||
func manageSubscriptions(_ webFeedURLs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
var webFeedRecords = [CKRecord]()
|
||||
for webFeedURL in webFeedURLs {
|
||||
let webFeedRecordID = CKRecord.ID(recordName: webFeedURL.md5String, zoneID: Self.zoneID)
|
||||
let webFeedRecord = CKRecord(recordType: CloudKitWebFeed.recordType, recordID: webFeedRecordID)
|
||||
webFeedRecord[CloudKitWebFeed.Fields.url] = webFeedURL
|
||||
webFeedRecord[CloudKitWebFeed.Fields.httpLastModified] = ""
|
||||
webFeedRecord[CloudKitWebFeed.Fields.httpEtag] = ""
|
||||
webFeedRecords.append(webFeedRecord)
|
||||
}
|
||||
|
||||
self.saveIfNew(webFeedRecords) { _ in
|
||||
|
||||
var subscriptions = [CKSubscription]()
|
||||
let webFeedURLChunks = Array(webFeedURLs).chunked(into: 20)
|
||||
for webFeedURLChunk in webFeedURLChunks {
|
||||
|
||||
let predicate = NSPredicate(format: "url in %@", webFeedURLChunk)
|
||||
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
|
||||
subscriptions.append(subscription)
|
||||
|
||||
}
|
||||
|
||||
self.fetchAllUserSubscriptions() { result in
|
||||
switch result {
|
||||
case .success(let subscriptionsToDelete):
|
||||
let subscriptionToDeleteIDs = subscriptionsToDelete.map({ $0.subscriptionID })
|
||||
self.modify(subscriptionsToSave: subscriptions, subscriptionIDsToDelete: subscriptionToDeleteIDs, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -337,32 +337,6 @@ extension CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bulk add (or modify I suppose) and delete of subscriptions
|
||||
func modify(subscriptionsToSave: [CKSubscription], subscriptionIDsToDelete: [CKSubscription.ID], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let op = CKModifySubscriptionsOperation(subscriptionsToSave: subscriptionsToSave, subscriptionIDsToDelete: subscriptionIDsToDelete)
|
||||
|
||||
op.modifySubscriptionsCompletionBlock = { [weak self] (_, _, error) in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
case .retry(let timeToWait):
|
||||
self.retryIfPossible(after: timeToWait) {
|
||||
self.modify(subscriptionsToSave: subscriptionsToSave, subscriptionIDsToDelete: subscriptionIDsToDelete, completion: completion)
|
||||
}
|
||||
default:
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(CloudKitError(error!)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
database?.add(op)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -432,26 +406,6 @@ extension CloudKitZone {
|
||||
database?.add(op)
|
||||
}
|
||||
|
||||
/// Fetch all the subscriptions that a user has in the current database in all zones
|
||||
func fetchAllUserSubscriptions(completion: @escaping (Result<[CKSubscription], Error>) -> Void) {
|
||||
database?.fetchAllSubscriptions() { subscriptions, error in
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
completion(.success((subscriptions!)))
|
||||
}
|
||||
case .retry(let timeToWait):
|
||||
self.retryIfPossible(after: timeToWait) {
|
||||
self.fetchAllUserSubscriptions(completion: completion)
|
||||
}
|
||||
default:
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(CloudKitError(error!)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch all the changes in the CKZone since the last time we checked
|
||||
func fetchChangesInZone(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user