Add option to save state between app launches

This commit is contained in:
david-swift 2024-01-02 18:30:31 +01:00
parent 350580269f
commit 021f0f231f
13 changed files with 141 additions and 5 deletions

View File

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

View File

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

View File

@ -9,6 +9,10 @@ A class storing the value.
The stored value.
### `key`
The storage key.
## Methods
### `init(value:)`

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ public struct State<Value>: StateProtocol {
}
nonmutating set {
content.storage.value = newValue
writeValue?()
}
}
// swiftlint:enable force_cast
@ -46,6 +47,9 @@ public struct State<Value>: StateProtocol {
/// The stored value.
public let content: State<Any>.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<Value>: 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<Value>: 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())
}
}

View File

@ -58,6 +58,7 @@ extension App {
appInstance.app.sceneStorage.remove(at: index)
}
}
GTUIApp.appID = appInstance.id
appInstance.app.run()
}

View File

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

View File

@ -12,7 +12,8 @@ import Libadwaita
struct CounterDemo: View {
@State private var count = 0
@State("count")
private var count = 0
var view: Body {
VStack {

View File

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

View File

@ -10,7 +10,7 @@
import Adwaita
import Libadwaita
enum Page: String, Identifiable, CaseIterable {
enum Page: String, Identifiable, CaseIterable, Codable {
case welcome
case counter

View File

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