Skip to content
This repository has been archived by the owner on Oct 17, 2024. It is now read-only.

Commit

Permalink
Add option to save state between app launches
Browse files Browse the repository at this point in the history
  • Loading branch information
david-swift committed Jan 2, 2024
1 parent 3505802 commit 021f0f2
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 5 deletions.
1 change: 1 addition & 0 deletions Documentation/Reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Documentation/Reference/classes/GTUIApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions Documentation/Reference/classes/State.Storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ A class storing the value.

The stored value.

### `key`

The storage key.

## Methods
### `init(value:)`

Expand Down
23 changes: 23 additions & 0 deletions Documentation/Reference/extensions/State.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions Documentation/Reference/extensions/View.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions Documentation/Reference/structs/State.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
65 changes: 65 additions & 0 deletions Sources/Adwaita/Model/Data Flow/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,17 @@ public struct State<Value>: StateProtocol {
}
nonmutating set {
content.storage.value = newValue
writeValue?()
}
}
// swiftlint:enable force_cast

/// 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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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())
}

}
1 change: 1 addition & 0 deletions Sources/Adwaita/Model/User Interface/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ extension App {
appInstance.app.sceneStorage.remove(at: index)
}
}
GTUIApp.appID = appInstance.id
appInstance.app.run()
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/Adwaita/Model/User Interface/App/GTUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Tests/CounterDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import Libadwaita

struct CounterDemo: View {

@State private var count = 0
@State("count")
private var count = 0

var view: Body {
VStack {
Expand Down
12 changes: 9 additions & 3 deletions Tests/Demo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -75,8 +77,12 @@ struct Demo: App {
HeaderBar.end {
menu
}
.headerBarTitle {
Text("Demo")
.style("heading")
.transition(.crossfade)
}
}
.navigationTitle("Demo")
} content: {
StatusPage(
selection.label,
Expand Down
2 changes: 1 addition & 1 deletion Tests/Page.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import Adwaita
import Libadwaita

enum Page: String, Identifiable, CaseIterable {
enum Page: String, Identifiable, CaseIterable, Codable {

case welcome
case counter
Expand Down
9 changes: 9 additions & 0 deletions user-manual/Basics/CreatingViews.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```

0 comments on commit 021f0f2

Please sign in to comment.