From 021f0f231f4be97ec01feccede08e8ea774cc654 Mon Sep 17 00:00:00 2001 From: david-swift Date: Tue, 2 Jan 2024 18:30:31 +0100 Subject: [PATCH] Add option to save state between app launches --- Documentation/Reference/README.md | 1 + Documentation/Reference/classes/GTUIApp.md | 4 ++ .../Reference/classes/State.Storage.md | 4 ++ Documentation/Reference/extensions/State.md | 23 +++++++ Documentation/Reference/extensions/View.md | 6 ++ Documentation/Reference/structs/State.md | 14 ++++ Sources/Adwaita/Model/Data Flow/State.swift | 65 +++++++++++++++++++ .../Model/User Interface/App/App.swift | 1 + .../Model/User Interface/App/GTUIApp.swift | 2 + Tests/CounterDemo.swift | 3 +- Tests/Demo.swift | 12 +++- Tests/Page.swift | 2 +- user-manual/Basics/CreatingViews.md | 9 +++ 13 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 Documentation/Reference/extensions/State.md diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md index 984ea0c..d6f0535 100644 --- a/Documentation/Reference/README.md +++ b/Documentation/Reference/README.md @@ -71,6 +71,7 @@ - [MenuItem](extensions/MenuItem.md) - [MenuItemGroup](extensions/MenuItemGroup.md) - [NativeWidgetPeer](extensions/NativeWidgetPeer.md) +- [State](extensions/State.md) - [String](extensions/String.md) - [View](extensions/View.md) - [Widget](extensions/Widget.md) diff --git a/Documentation/Reference/classes/GTUIApp.md b/Documentation/Reference/classes/GTUIApp.md index c2ec704..a88b8e5 100644 --- a/Documentation/Reference/classes/GTUIApp.md +++ b/Documentation/Reference/classes/GTUIApp.md @@ -9,6 +9,10 @@ The GTUI application. The handlers which are called when a state changes. +### `appID` + +The app's id for the file name for storing the data. + ### `body` The app's content. diff --git a/Documentation/Reference/classes/State.Storage.md b/Documentation/Reference/classes/State.Storage.md index b82c692..e9a378a 100644 --- a/Documentation/Reference/classes/State.Storage.md +++ b/Documentation/Reference/classes/State.Storage.md @@ -9,6 +9,10 @@ A class storing the value. The stored value. +### `key` + +The storage key. + ## Methods ### `init(value:)` diff --git a/Documentation/Reference/extensions/State.md b/Documentation/Reference/extensions/State.md new file mode 100644 index 0000000..b83e9ae --- /dev/null +++ b/Documentation/Reference/extensions/State.md @@ -0,0 +1,23 @@ +**EXTENSION** + +# `State` + +## Methods +### `init(wrappedValue:_:)` + +Initialize a property representing a state in the view. +- Parameters: + - key: The unique storage key of the property. + - wrappedValue: The wrapped value. + +### `checkFile()` + +Check whether the settings file exists, and, if not, create it. + +### `readValue()` + +Update the local value with the value from the file. + +### `writeCodableValue()` + +Update the value on the file with the local value. diff --git a/Documentation/Reference/extensions/View.md b/Documentation/Reference/extensions/View.md index 6d2be92..2f4d80f 100644 --- a/Documentation/Reference/extensions/View.md +++ b/Documentation/Reference/extensions/View.md @@ -24,6 +24,12 @@ Get a storage. ### `getModified(modifiers:)` +### `inspectOnAppear(_:)` + +Run a function on the widget when it appears for the first time. +- Parameter closure: The function. +- Returns: A view. + ### `onAppear(_:)` Run a function when the view appears for the first time. diff --git a/Documentation/Reference/structs/State.md b/Documentation/Reference/structs/State.md index 5871809..5016c85 100644 --- a/Documentation/Reference/structs/State.md +++ b/Documentation/Reference/structs/State.md @@ -21,6 +21,10 @@ Get and set the value without updating the views. The stored value. +### `writeValue` + +The function for updating the value in the settings file. + ### `value` The value with an erased type. @@ -35,3 +39,13 @@ Initialize a property representing a state in the view. ### `updateViews()` Update all of the views. + +### `dirPath()` + +Get the settings directory path. +- Returns: The path. + +### `filePath()` + +Get the settings file path. +- Returns: The path. diff --git a/Sources/Adwaita/Model/Data Flow/State.swift b/Sources/Adwaita/Model/Data Flow/State.swift index e2651b7..090dcea 100644 --- a/Sources/Adwaita/Model/Data Flow/State.swift +++ b/Sources/Adwaita/Model/Data Flow/State.swift @@ -39,6 +39,7 @@ public struct State: StateProtocol { } nonmutating set { content.storage.value = newValue + writeValue?() } } // swiftlint:enable force_cast @@ -46,6 +47,9 @@ public struct State: StateProtocol { /// The stored value. public let content: State.Content + /// The function for updating the value in the settings file. + private var writeValue: (() -> Void)? + /// The value with an erased type. public var value: Any { get { @@ -84,6 +88,8 @@ public struct State: StateProtocol { /// The stored value. public var value: Any + /// The storage key. + public var key: String? /// Initialize the storage. /// - Parameters: @@ -101,4 +107,63 @@ public struct State: StateProtocol { } } + /// Get the settings directory path. + /// - Returns: The path. + private func dirPath() -> String { + "\(NSHomeDirectory())/.config/\(GTUIApp.appID)/" + } + + /// Get the settings file path. + /// - Returns: The path. + private func filePath() -> URL { + .init(fileURLWithPath: dirPath() + "\(content.storage.key ?? "temporary").json") + } + +} + +extension State where Value: Codable { + + /// Initialize a property representing a state in the view. + /// - Parameters: + /// - key: The unique storage key of the property. + /// - wrappedValue: The wrapped value. + public init(wrappedValue: Value, _ key: String) { + content = .init(storage: .init(value: wrappedValue)) + content.storage.key = key + checkFile() + readValue() + self.writeValue = writeCodableValue + } + + /// Check whether the settings file exists, and, if not, create it. + private func checkFile() { + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: dirPath()) { + try? fileManager.createDirectory( + at: .init(fileURLWithPath: dirPath()), + withIntermediateDirectories: true, + attributes: nil + ) + } + if !fileManager.fileExists(atPath: filePath().path) { + fileManager.createFile(atPath: filePath().path, contents: .init(), attributes: nil) + } + } + + /// Update the local value with the value from the file. + private func readValue() { + let data = try? Data(contentsOf: filePath()) + if let data, let value = try? JSONDecoder().decode(Value.self, from: data) { + rawValue = value + } + } + + /// Update the value on the file with the local value. + private func writeCodableValue() { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let data = try? encoder.encode(rawValue) + try? data?.write(to: filePath()) + } + } diff --git a/Sources/Adwaita/Model/User Interface/App/App.swift b/Sources/Adwaita/Model/User Interface/App/App.swift index 500f042..95a5040 100644 --- a/Sources/Adwaita/Model/User Interface/App/App.swift +++ b/Sources/Adwaita/Model/User Interface/App/App.swift @@ -58,6 +58,7 @@ extension App { appInstance.app.sceneStorage.remove(at: index) } } + GTUIApp.appID = appInstance.id appInstance.app.run() } diff --git a/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift b/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift index a179d37..687d814 100644 --- a/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift +++ b/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift @@ -12,6 +12,8 @@ public class GTUIApp: Application { /// The handlers which are called when a state changes. static var updateHandlers: [() -> Void] = [] + /// The app's id for the file name for storing the data. + static var appID = "temporary" /// The app's content. var body: () -> App diff --git a/Tests/CounterDemo.swift b/Tests/CounterDemo.swift index 2c42e84..2ddb674 100644 --- a/Tests/CounterDemo.swift +++ b/Tests/CounterDemo.swift @@ -12,7 +12,8 @@ import Libadwaita struct CounterDemo: View { - @State private var count = 0 + @State("count") + private var count = 0 var view: Body { VStack { diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 1e0de86..89e9321 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -55,9 +55,11 @@ struct Demo: App { struct DemoContent: View { - @State private var selection: Page = .welcome + @State("selection") + private var selection: Page = .welcome @State private var toast: Signal = .init() - @State private var sidebarVisible = true + @State("sidebar-visible") + private var sidebarVisible = true var window: GTUIApplicationWindow var app: GTUIApp! @@ -75,8 +77,12 @@ struct Demo: App { HeaderBar.end { menu } + .headerBarTitle { + Text("Demo") + .style("heading") + .transition(.crossfade) + } } - .navigationTitle("Demo") } content: { StatusPage( selection.label, diff --git a/Tests/Page.swift b/Tests/Page.swift index ed81738..b5b73fb 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -10,7 +10,7 @@ import Adwaita import Libadwaita -enum Page: String, Identifiable, CaseIterable { +enum Page: String, Identifiable, CaseIterable, Codable { case welcome case counter diff --git a/user-manual/Basics/CreatingViews.md b/user-manual/Basics/CreatingViews.md index 84b2a66..33741c9 100644 --- a/user-manual/Basics/CreatingViews.md +++ b/user-manual/Basics/CreatingViews.md @@ -133,3 +133,12 @@ struct ChangeTextView: View { Whenever you modify a state property (directly or indirectly through bindings), the user interface gets automatically updated to reflect that change. + +## Save State Values Between App Launches +It's possible to automatically save a value that conforms to `Codable` whenever it changes to a file. +The value in the file is read when the view containing the state value appears for the first time (e.g. when the user launches the app). + +Use the following syntax, where `"text"` is a unique identifier. +```swift +@State("text") private var text = "world" +```