Add container handling code

This commit is contained in:
Maurice Parker
2020-03-30 15:15:45 -05:00
parent 53e947ee4c
commit 766eb507bf
7 changed files with 199 additions and 15 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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