mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Add container handling code
This commit is contained in:
@@ -136,6 +136,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
public var topLevelWebFeeds = Set<WebFeed>()
|
||||
public var folders: Set<Folder>? = Set<Folder>()
|
||||
|
||||
public var externalID: String? {
|
||||
get {
|
||||
return metadata.externalID
|
||||
}
|
||||
set {
|
||||
metadata.externalID = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var sortedFolders: [Folder]? {
|
||||
if let folders = folders {
|
||||
return Array(folders).sorted(by: { $0.nameForDisplay < $1.nameForDisplay })
|
||||
|
||||
@@ -23,6 +23,7 @@ final class AccountMetadata: Codable {
|
||||
case lastArticleFetchStartTime = "lastArticleFetch"
|
||||
case lastArticleFetchEndTime
|
||||
case endpointURL
|
||||
case externalID
|
||||
}
|
||||
|
||||
var name: String? {
|
||||
@@ -81,6 +82,14 @@ final class AccountMetadata: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
var externalID: String? {
|
||||
didSet {
|
||||
if externalID != oldValue {
|
||||
valueDidChange(.externalID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: AccountMetadataDelegate?
|
||||
|
||||
func valueDidChange(_ key: CodingKeys) {
|
||||
|
||||
@@ -235,26 +235,60 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
}
|
||||
|
||||
func createFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
accountZone.createFolder(name: name) { result in
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
folder.externalID = externalID
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
folder.name = name
|
||||
completion(.success(()))
|
||||
accountZone.renameFolder(folder, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
folder.name = name
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account.removeFolder(folder)
|
||||
completion(.success(()))
|
||||
accountZone.removeFolder(folder) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
account.removeFolder(folder)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account.addFolder(folder)
|
||||
completion(.success(()))
|
||||
guard let name = folder.name else {
|
||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
accountZone.createFolder(name: name) { result in
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
folder.externalID = externalID
|
||||
account.addFolder(folder)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
@@ -263,6 +297,18 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
||||
|
||||
func accountDidInitialize(_ account: Account) {
|
||||
accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress)
|
||||
|
||||
if account.externalID == nil {
|
||||
accountZone.findOrCreateAccount() { result in
|
||||
switch result {
|
||||
case .success(let externalID):
|
||||
account.externalID = externalID
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Error adding account container: %@", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zones.forEach { zone in
|
||||
zone.resumeLongLivedOperationIfPossible()
|
||||
zone.subscribe()
|
||||
|
||||
@@ -32,6 +32,14 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
}
|
||||
}
|
||||
|
||||
struct CloudKitContainer {
|
||||
static let recordType = "Container"
|
||||
struct Fields {
|
||||
static let isAccount = "isAccount"
|
||||
static let name = "name"
|
||||
}
|
||||
}
|
||||
|
||||
init(container: CKContainer) {
|
||||
self.container = container
|
||||
self.database = container.privateCloudDatabase
|
||||
@@ -77,11 +85,68 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
|
||||
/// Deletes a web feed from iCloud
|
||||
func removeWebFeed(_ webFeed: WebFeed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let externalID = webFeed.externalID else {
|
||||
delete(externalID: webFeed.externalID , completion: completion)
|
||||
}
|
||||
|
||||
func findOrCreateAccount(completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let predicate = NSPredicate(format: "isAccount = true")
|
||||
let ckQuery = CKQuery(recordType: CloudKitContainer.recordType, predicate: predicate)
|
||||
|
||||
query(ckQuery) { result in
|
||||
switch result {
|
||||
case .success(let records):
|
||||
completion(.success(records[0].externalID))
|
||||
case .failure:
|
||||
self.createContainer(name: "Account", isAccount: true, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createFolder(name: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
createContainer(name: name, isAccount: false, completion: completion)
|
||||
}
|
||||
|
||||
func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let externalID = folder.externalID else {
|
||||
completion(.failure(CloudKitZoneError.invalidParameter))
|
||||
return
|
||||
}
|
||||
delete(externalID: externalID, completion: completion)
|
||||
|
||||
let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID)
|
||||
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: recordID)
|
||||
record[CloudKitContainer.Fields.name] = name
|
||||
|
||||
save(record: record) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delete(externalID: folder.externalID, completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension CloudKitAccountZone {
|
||||
|
||||
func createContainer(name: String, isAccount: Bool, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
let record = CKRecord(recordType: CloudKitContainer.recordType, recordID: generateRecordID())
|
||||
record[CloudKitContainer.Fields.name] = name
|
||||
record[CloudKitContainer.Fields.isAccount] = isAccount
|
||||
|
||||
save(record: record) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(record.externalID))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
switch record.recordType {
|
||||
case CloudKitAccountZone.CloudKitWebFeed.recordType:
|
||||
addOrUpdateWebFeed(record)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
addOrUpdateContainer(record)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(record.recordType)")
|
||||
}
|
||||
@@ -36,6 +38,8 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
switch recordType {
|
||||
case CloudKitAccountZone.CloudKitWebFeed.recordType:
|
||||
removeWebFeed(recordID.externalID)
|
||||
case CloudKitAccountZone.CloudKitContainer.recordType:
|
||||
removeContainer(recordID.externalID)
|
||||
default:
|
||||
assertionFailure("Unknown record type: \(recordID.externalID)")
|
||||
}
|
||||
@@ -63,6 +67,22 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func addOrUpdateContainer(_ record: CKRecord) {
|
||||
guard let account = account, let name = record[CloudKitAccountZone.CloudKitContainer.Fields.name] as? String else { return }
|
||||
|
||||
if let folder = account.existingFolder(withExternalID: record.externalID) {
|
||||
folder.name = name
|
||||
} else {
|
||||
account.ensureFolder(with: name)
|
||||
}
|
||||
}
|
||||
|
||||
func removeContainer(_ externalID: String) {
|
||||
if let folder = account?.existingFolder(withExternalID: externalID) {
|
||||
account?.removeFolder(folder)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension CloudKitAcountZoneDelegate {
|
||||
|
||||
@@ -100,11 +100,40 @@ extension CloudKitZone {
|
||||
modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion)
|
||||
}
|
||||
|
||||
func delete(externalID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func delete(externalID: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let externalID = externalID else {
|
||||
completion(.failure(CloudKitZoneError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
let recordID = CKRecord.ID(recordName: externalID, zoneID: Self.zoneID)
|
||||
modify(recordsToSave: [], recordIDsToDelete: [recordID], completion: completion)
|
||||
}
|
||||
|
||||
func query(_ query: CKQuery, completion: @escaping (Result<[CKRecord], Error>) -> Void) {
|
||||
guard let database = database else {
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
database.perform(query, inZoneWith: Self.zoneID) { records, error in
|
||||
switch CloudKitZoneResult.resolve(error) {
|
||||
case .success:
|
||||
if let records = records {
|
||||
completion(.success(records))
|
||||
} else {
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
}
|
||||
case .retry(let timeToWait):
|
||||
self.retryOperationIfPossible(retryAfter: timeToWait) {
|
||||
self.query(query, completion: completion)
|
||||
}
|
||||
default:
|
||||
completion(.failure(error!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let op = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete)
|
||||
|
||||
@@ -252,7 +281,12 @@ private extension CloudKitZone {
|
||||
}
|
||||
|
||||
func createZoneRecord(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
database?.save(CKRecordZone(zoneID: Self.zoneID)) { (recordZone, error) in
|
||||
guard let database = database else {
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
database.save(CKRecordZone(zoneID: Self.zoneID)) { (recordZone, error) in
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
|
||||
@@ -21,7 +21,8 @@ public protocol Container: class, ContainerIdentifiable {
|
||||
var account: Account? { get }
|
||||
var topLevelWebFeeds: Set<WebFeed> { get set }
|
||||
var folders: Set<Folder>? { get set }
|
||||
|
||||
var externalID: String? { get set }
|
||||
|
||||
func hasAtLeastOneWebFeed() -> Bool
|
||||
func objectIsChild(_ object: AnyObject) -> Bool
|
||||
|
||||
|
||||
Reference in New Issue
Block a user