From 76db659137a66330ee5fc720aa5c96eb4572e9dc Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 19 Sep 2022 15:08:10 +0200 Subject: [PATCH] Transition from preferences to file storage --- Notes.xcodeproj/project.pbxproj | 40 +++++++-- Notes/AppDelegate.swift | 20 +++-- Notes/Base.lproj/Main.storyboard | 26 ------ Notes/NoteViewController.swift | 16 ++-- Notes/Notes.entitlements | 14 ++++ Notes/Preferences.swift | 29 ------- Notes/Storage/Document.swift | 13 +++ Notes/Storage/FileOperator.swift | 15 ++++ Notes/Storage/FileStorage.swift | 105 ++++++++++++++++++++++++ Notes/Storage/PreferencesStorage.swift | 31 +++++++ Notes/UIViewController+Extensions.swift | 20 +++++ 11 files changed, 256 insertions(+), 73 deletions(-) create mode 100644 Notes/Notes.entitlements delete mode 100644 Notes/Preferences.swift create mode 100644 Notes/Storage/Document.swift create mode 100644 Notes/Storage/FileOperator.swift create mode 100644 Notes/Storage/FileStorage.swift create mode 100644 Notes/Storage/PreferencesStorage.swift create mode 100644 Notes/UIViewController+Extensions.swift diff --git a/Notes.xcodeproj/project.pbxproj b/Notes.xcodeproj/project.pbxproj index bae7f91..750b56d 100644 --- a/Notes.xcodeproj/project.pbxproj +++ b/Notes.xcodeproj/project.pbxproj @@ -18,7 +18,11 @@ C41A31EA28C49B320019B951 /* NotesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41A31E928C49B320019B951 /* NotesUITests.swift */; }; C41A31EC28C49B320019B951 /* NotesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41A31EB28C49B320019B951 /* NotesUITestsLaunchTests.swift */; }; C41A31F928C49DC80019B951 /* NoteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41A31F828C49DC80019B951 /* NoteViewController.swift */; }; - C41A31FB28C4D8040019B951 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41A31FA28C4D8040019B951 /* Preferences.swift */; }; + C41A31FB28C4D8040019B951 /* PreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41A31FA28C4D8040019B951 /* PreferencesStorage.swift */; }; + C43417F428D4ADB10098C15A /* FileOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43417F328D4ADB10098C15A /* FileOperator.swift */; }; + C43417F628D4ADDB0098C15A /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43417F528D4ADDB0098C15A /* FileStorage.swift */; }; + C43417FA28D4B9370098C15A /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43417F928D4B9370098C15A /* Document.swift */; }; + C4EEE40728D89C46003DDC24 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE40628D89C46003DDC24 /* UIViewController+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,7 +58,12 @@ C41A31E928C49B320019B951 /* NotesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesUITests.swift; sourceTree = ""; }; C41A31EB28C49B320019B951 /* NotesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesUITestsLaunchTests.swift; sourceTree = ""; }; C41A31F828C49DC80019B951 /* NoteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteViewController.swift; sourceTree = ""; }; - C41A31FA28C4D8040019B951 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + C41A31FA28C4D8040019B951 /* PreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesStorage.swift; sourceTree = ""; }; + C43417F128D4AACD0098C15A /* Notes.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Notes.entitlements; sourceTree = ""; }; + C43417F328D4ADB10098C15A /* FileOperator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperator.swift; sourceTree = ""; }; + C43417F528D4ADDB0098C15A /* FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = ""; }; + C43417F928D4B9370098C15A /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; + C4EEE40628D89C46003DDC24 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -105,11 +114,13 @@ C41A31C428C49B310019B951 /* Notes */ = { isa = PBXGroup; children = ( + C43417F128D4AACD0098C15A /* Notes.entitlements */, C41A31C528C49B310019B951 /* AppDelegate.swift */, C41A31C728C49B310019B951 /* SceneDelegate.swift */, C41A31C928C49B310019B951 /* ViewController.swift */, C41A31F828C49DC80019B951 /* NoteViewController.swift */, - C41A31FA28C4D8040019B951 /* Preferences.swift */, + C4EEE40628D89C46003DDC24 /* UIViewController+Extensions.swift */, + C43417F228D4AD9C0098C15A /* Storage */, C41A31CB28C49B310019B951 /* Main.storyboard */, C41A31D128C49B320019B951 /* Assets.xcassets */, C41A31D328C49B320019B951 /* LaunchScreen.storyboard */, @@ -136,6 +147,17 @@ path = NotesUITests; sourceTree = ""; }; + C43417F228D4AD9C0098C15A /* Storage */ = { + isa = PBXGroup; + children = ( + C41A31FA28C4D8040019B951 /* PreferencesStorage.swift */, + C43417F328D4ADB10098C15A /* FileOperator.swift */, + C43417F528D4ADDB0098C15A /* FileStorage.swift */, + C43417F928D4B9370098C15A /* Document.swift */, + ); + path = Storage; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -269,8 +291,12 @@ files = ( C41A31CA28C49B310019B951 /* ViewController.swift in Sources */, C41A31C628C49B310019B951 /* AppDelegate.swift in Sources */, - C41A31FB28C4D8040019B951 /* Preferences.swift in Sources */, + C4EEE40728D89C46003DDC24 /* UIViewController+Extensions.swift in Sources */, + C41A31FB28C4D8040019B951 /* PreferencesStorage.swift in Sources */, C41A31C828C49B310019B951 /* SceneDelegate.swift in Sources */, + C43417F428D4ADB10098C15A /* FileOperator.swift in Sources */, + C43417FA28D4B9370098C15A /* Document.swift in Sources */, + C43417F628D4ADDB0098C15A /* FileStorage.swift in Sources */, C41A31F928C49DC80019B951 /* NoteViewController.swift in Sources */, C41A31D028C49B310019B951 /* Notes.xcdatamodeld in Sources */, ); @@ -378,7 +404,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -432,7 +458,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -448,6 +474,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_ENTITLEMENTS = Notes/Notes.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 526E96RFNP; @@ -478,6 +505,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_ENTITLEMENTS = Notes/Notes.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 526E96RFNP; diff --git a/Notes/AppDelegate.swift b/Notes/AppDelegate.swift index 15f7de4..8bcf003 100644 --- a/Notes/AppDelegate.swift +++ b/Notes/AppDelegate.swift @@ -14,13 +14,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - for fontFamilyName in UIFont.familyNames { - print("family: \(fontFamilyName)\n") - - for fontName in UIFont.fontNames(forFamilyName: fontFamilyName) { - print("font: \(fontName)") - } - } +// let fn = "main" +// if let content = PreferencesStorage.main.getContent(filename: fn) { +// FileStorage.main.requestStorage(filename: "main.txt", content: content) +// } + + +// for fontFamilyName in UIFont.familyNames { +// print("family: \(fontFamilyName)\n") +// +// for fontName in UIFont.fontNames(forFamilyName: fontFamilyName) { +// print("font: \(fontName)") +// } +// } return true } diff --git a/Notes/Base.lproj/Main.storyboard b/Notes/Base.lproj/Main.storyboard index 831170f..de325f4 100644 --- a/Notes/Base.lproj/Main.storyboard +++ b/Notes/Base.lproj/Main.storyboard @@ -9,32 +9,6 @@ - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - diff --git a/Notes/NoteViewController.swift b/Notes/NoteViewController.swift index 199d4c6..e8aa05c 100644 --- a/Notes/NoteViewController.swift +++ b/Notes/NoteViewController.swift @@ -10,7 +10,7 @@ import UIKit class NoteViewController : UIViewController, UITextViewDelegate { - var filename: String = "main" + var filename: String = "main.txt" @IBOutlet weak var textView: UITextView! @@ -27,8 +27,13 @@ class NoteViewController : UIViewController, UITextViewDelegate { let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addHandler)) self.navigationItem.rightBarButtonItems = [addButton, shareButton] - self.textView.text = Preferences.getContent(filename: self.filename) - self.textView.font = UIFont.systemFont(ofSize: 18.0, weight: .medium) + do { + self.textView.text = try FileStorage.main.getContent(filename: self.filename) + } catch { + self.showAlert(message: error.localizedDescription, title: "Error :(") + } + + self.textView.font = UIFont.systemFont(ofSize: 18.0, weight: .regular) self.textView.delegate = self self.textView.inputAccessoryView = self._inputAccessoryView() @@ -51,12 +56,13 @@ class NoteViewController : UIViewController, UITextViewDelegate { } func textViewDidChange(_ textView: UITextView) { - Preferences.store(filename: self.filename, content: textView.text) + FileStorage.main.requestStorage(filename: self.filename, content: textView.text) self._updateLastEdit() } fileprivate func _updateLastEdit() { - if let date = Preferences.lastEditDate(filename: self.filename) { + + if let date = try? FileStorage.main.lastEditDate(filename: self.filename) { let formattedDate: String = date.formatted() self._lastEditLabel?.text = "last edit: \(formattedDate)" } diff --git a/Notes/Notes.entitlements b/Notes/Notes.entitlements new file mode 100644 index 0000000..1d888bd --- /dev/null +++ b/Notes/Notes.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.icloud-container-identifiers + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.ubiquity-container-identifiers + + + diff --git a/Notes/Preferences.swift b/Notes/Preferences.swift deleted file mode 100644 index eab453a..0000000 --- a/Notes/Preferences.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Preferences.swift -// Notes -// -// Created by Laurent Morvillier on 04/09/2022. -// - -import Foundation - -class Preferences { - - static func store(filename: String, content: String) { - UserDefaults.standard.set(content, forKey: filename) - Preferences._fileChanged(filename: filename) - } - - fileprivate static func _fileChanged(filename: String) { - UserDefaults.standard.set(Date(), forKey: filename + "_date") - } - - static func getContent(filename: String) -> String? { - return UserDefaults.standard.object(forKey: filename) as? String - } - - static func lastEditDate(filename: String) -> Date? { - return UserDefaults.standard.object(forKey: filename + "_date") as? Date - } - -} diff --git a/Notes/Storage/Document.swift b/Notes/Storage/Document.swift new file mode 100644 index 0000000..be4d6f5 --- /dev/null +++ b/Notes/Storage/Document.swift @@ -0,0 +1,13 @@ +// +// Document.swift +// Notes +// +// Created by Laurent Morvillier on 16/09/2022. +// + +import Foundation +import UIKit + +class Document : UIDocument { + +} diff --git a/Notes/Storage/FileOperator.swift b/Notes/Storage/FileOperator.swift new file mode 100644 index 0000000..618a1ce --- /dev/null +++ b/Notes/Storage/FileOperator.swift @@ -0,0 +1,15 @@ +// +// FileOperator.swift +// Notes +// +// Created by Laurent Morvillier on 16/09/2022. +// + +import Foundation + +protocol FileOperator { + func requestStorage(filename: String, content: String) + func getContent(filename: String) throws -> String? + func lastEditDate(filename: String) throws -> Date? +} + diff --git a/Notes/Storage/FileStorage.swift b/Notes/Storage/FileStorage.swift new file mode 100644 index 0000000..7bdf7b9 --- /dev/null +++ b/Notes/Storage/FileStorage.swift @@ -0,0 +1,105 @@ +// +// CloudFileStorage.swift +// Notes +// +// Created by Laurent Morvillier on 16/09/2022. +// + +import Foundation +import UIKit + +struct StorageRequest { + var filename: String + var content: String +} + +let idleTimeBeforeSaving = 2.0 + +/// Should we have a way to go from local to iCloud? +/// https://stackoverflow.com/questions/33886846/best-way-to-use-icloud-documents-storage +/// Should we store the files locally whatever the iCloud choice is? +class FileStorage : FileOperator { + + static var main: FileStorage = FileStorage() + + fileprivate var _containerURL: URL? + + fileprivate var _cloudStorageDetermined: Bool = false + + fileprivate let containerIdentifier = "notes" + + fileprivate var _timer: Timer? = nil + + fileprivate var _storageRequests: [String : String] = [:] + + init() { + DispatchQueue.global(qos: .userInteractive).async { + self._containerURL = FileManager.default.url(forUbiquityContainerIdentifier: self.containerIdentifier) + self._cloudStorageDetermined = true + print("Cloud container URL is : \(String(describing: self._containerURL?.absoluteString))") + } + } + + fileprivate func _directoryURL() throws -> URL { + let documentDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + return documentDirectory + } + + // MARK: - FileOperator + + func requestStorage(filename: String, content: String) { + + self._storageRequests[filename] = content + + self._timer?.invalidate() + self._timer = Timer.scheduledTimer(timeInterval: idleTimeBeforeSaving, target: self, selector: #selector(self._storageRequested), userInfo: nil, repeats: false) + + } + + @objc fileprivate func _storageRequested() { + + for (filename, content) in self._storageRequests { + do { + try self._store(filename: filename, content: content) + } catch { + // TODO show errors to users, possibly by notifications + print("error: \(error.localizedDescription)") + } + } + self._storageRequests.removeAll() + } + + fileprivate func _store(filename: String, content: String) throws { + let fileURL = try self._directoryURL().appending(path: filename) + print("Store file to: \(fileURL.absoluteString)") + + try content.write(to: fileURL, atomically: true, encoding: .utf8) + + try self._copyToCloudContainerIfNecessary(fileURL: fileURL, filename: filename) + } + + func getContent(filename: String) throws -> String? { + let fileURL = try self._directoryURL().appending(path: filename) + return try String(contentsOf: fileURL) + } + + func lastEditDate(filename: String) throws -> Date? { + let fileURL = try self._directoryURL().appending(path: filename) + let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.absoluteString) + return attributes[FileAttributeKey.modificationDate] as? Date + } + + fileprivate func _copyToCloudContainerIfNecessary(fileURL: URL, filename: String) throws { + guard let containerURL = self._containerURL else { + return + } + let cloudURL = containerURL.appending(path: filename) + try FileManager.default.copyItem(at: fileURL, to: cloudURL) + print("cloud copy to: \(cloudURL)") + } + + deinit { + self._timer?.invalidate() + } + +} diff --git a/Notes/Storage/PreferencesStorage.swift b/Notes/Storage/PreferencesStorage.swift new file mode 100644 index 0000000..75b9969 --- /dev/null +++ b/Notes/Storage/PreferencesStorage.swift @@ -0,0 +1,31 @@ +// +// Preferences.swift +// Notes +// +// Created by Laurent Morvillier on 04/09/2022. +// + +import Foundation + +//class PreferencesStorage : FileOperator { +// +// static var main = PreferencesStorage() +// +// func requestStorage(filename: String, content: String) { +// UserDefaults.standard.set(content, forKey: filename) +// PreferencesStorage._fileChanged(filename: filename) +// } +// +// fileprivate static func _fileChanged(filename: String) { +// UserDefaults.standard.set(Date(), forKey: filename + "_date") +// } +// +// func getContent(filename: String) -> String? { +// return UserDefaults.standard.object(forKey: filename) as? String +// } +// +// func lastEditDate(filename: String) -> Date? { +// return UserDefaults.standard.object(forKey: filename + "_date") as? Date +// } +// +//} diff --git a/Notes/UIViewController+Extensions.swift b/Notes/UIViewController+Extensions.swift new file mode 100644 index 0000000..653de91 --- /dev/null +++ b/Notes/UIViewController+Extensions.swift @@ -0,0 +1,20 @@ +// +// UIViewController+Extensions.swift +// Notes +// +// Created by Laurent Morvillier on 19/09/2022. +// + +import Foundation +import UIKit + +extension UIViewController { + + @objc public func showAlert(message: String, title: String?, completion: (() -> ())? = nil) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let action = UIAlertAction(title: NSLocalizedString("OK", comment: "OK"), style: .default, handler: nil) + alertController.addAction(action) + self.present(alertController, animated: true, completion: completion) + } + +}