// // DraftsManager.swift // ComposeUI // // Created by Shadowfacts on 10/22/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import Foundation import Combine import OSLog private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsManager") public class DraftsManager: Codable, ObservableObject { public private(set) static var shared: DraftsManager = load() private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")! private static var archiveURL = appGroupDirectory.appendingPathComponent("drafts").appendingPathExtension("plist") private static let saveQueue = DispatchQueue(label: "DraftsManager", qos: .utility) public static func save() { saveQueue.async { let encoder = PropertyListEncoder() do { let data = try encoder.encode(shared) try data.write(to: archiveURL, options: .noFileProtection) } catch { logger.error("Save failed: \(String(describing: error))") } } } static func load() -> DraftsManager { let decoder = PropertyListDecoder() do { let data = try Data(contentsOf: archiveURL) let draftsManager = try decoder.decode(DraftsManager.self, from: data) return draftsManager } catch { logger.error("Load failed: \(String(describing: error))") return DraftsManager() } } public static func migrate(from url: URL) -> Result { do { try? FileManager.default.removeItem(at: archiveURL) try FileManager.default.moveItem(at: url, to: archiveURL) } catch { return .failure(error) } shared = load() return .success(()) } private init() {} public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if let dict = try? container.decode([UUID: SafeDraft].self, forKey: .drafts) { self.drafts = dict.compactMapValues { $0.draft } } else if let array = try? container.decode([SafeDraft].self, forKey: .drafts) { self.drafts = array.reduce(into: [:], { partialResult, safeDraft in if let draft = safeDraft.draft { partialResult[draft.id] = draft } }) } else { throw DecodingError.dataCorruptedError(forKey: .drafts, in: container, debugDescription: "expected drafts to be a dict or array of drafts") } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(drafts, forKey: .drafts) } @Published private var drafts: [UUID: Draft] = [:] var sorted: [Draft] { return drafts.values.sorted(by: { $0.lastModified > $1.lastModified }) } public func add(_ draft: Draft) { drafts[draft.id] = draft } public func remove(_ draft: Draft) { drafts.removeValue(forKey: draft.id) } public func getBy(id: UUID) -> Draft? { return drafts[id] } enum CodingKeys: String, CodingKey { case drafts } // a container that always succeeds at decoding // so if a single draft can't be decoded, we don't lose all drafts struct SafeDraft: Decodable { let draft: Draft? init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.draft = try? container.decode(Draft.self) } } }