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

Commit

Permalink
Add models
Browse files Browse the repository at this point in the history
  • Loading branch information
david-swift committed Jul 19, 2024
1 parent 5b3e66e commit f8fb7dd
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 16 deletions.
7 changes: 4 additions & 3 deletions Sources/Meta.docc/Principles/Backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ struct Subtasks: App {
## Cross-Platform Apps

Even though the backend is set to the TermKitBackend, you can use any view conforming to ``AnyView`` and any scene conforming to ``SceneElement`` in your definition.
This is enabled by another concept: backends have their own view protocol and their own scene protocol, which conform to ``Widget`` or ``SceneElement``, respectively.
All the concrete UI elements specific to a backend conform to the backend's protocol.
This is enabled by another concept: backends have their own view protocols and their own scene protocol, which conform to ``Widget`` or ``SceneElement``, respectively.
All the concrete UI elements specific to a backend conform to the backend's protocols.
The conformance to the protocol can therefore be used to identify widgets that should be rendered. If you were to define a platform-independent widget, a so-called convenience widget, make it conform to the ``ConvenienceWidget`` protocol instead, so it will always be rendered.

The app storage of the backend contains the widget and scene element protocols (``AppStorage/WidgetType`` and ``AppStorage/SceneElementType``) which will be used for rendering.
The app storage of the backend contains the scene element protocols (``AppStorage/SceneElementType``) which will be used for rendering scene elements.
Pass the correct view render data type (``ViewRenderData``) containing the widget type as well as the default wrapper widget when interacting with child views of scene elements or views.

## Umbrella Backends

Expand Down
27 changes: 19 additions & 8 deletions Sources/Meta.docc/Principles/StateConcept.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,30 +134,41 @@ struct ContentView: View {
}
```

## Observation
## Organization

State can manage either value types (as seen in the examples above), or [observable reference types](https://developer.apple.com/documentation/observation).
Instead of having many state variables in one view, you can combine them to one value type if sensible.

```swift
@Observable
class TaskModel {
struct ContentData {

var tasks: [String] = []
var count = 0
var label = "Hello"

}

struct ContentView: View {

@State private var model = TaskModel()
@State private var data = ContentData()

var view: Body {
TaskList(tasks: $model.tasks)
Button(data.label) { // Directly access properties
data.count -= 1 // Directly update properties
}
IncreaseButton(count: $data.count) // Pass a property as a binding
}

}
```

Observable reference types can be handy when, e.g., synchronizing state with a server.
### Models

Often when creating complex value types, it is a good idea to provide functions inside of the type manipulating its properties instead of defining them on the views.
Remember to use the `mutating` keyword to enable the mutation.

In rare cases, you want to update a complex value type in the same way from an asynchronous context.
As this is not directly possible with value types, a workaround is needed.
Take a look at the documentation for the ``Model`` for more information,
but remember that this should only be used when needed.

## State in Backends

Expand Down
80 changes: 80 additions & 0 deletions Sources/Model/Data Flow/Model.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// Model.swift
// Meta
//
// Created by david-swift on 19.07.2024.
//

import Foundation

/// A model is a special type of state which can be updated from within itself.
/// This is useful for complex asynchronous operations such as networking.
///
/// Use the model protocol in the following way:
/// ```swift
/// struct TestView: View {
///
/// @State private var test = TestModel()
///
/// var view: Body {
/// Button(test.test) {
/// test.updateAsync()
/// // You can also update via
/// // test.test = "hello"
/// // as with any state values
/// }
/// }
///
/// }
///
/// struct TestModel: Model {
///
/// var test = "Label"
///
/// var model: ModelData? // Use exactly this line in every model
///
/// func updateAsync() {
/// Task {
/// // Do something asynchronously
/// // Remember to execute the following line in the correct context, depending on the backend
/// // As an example, you might have to run it on the main thread in some cases
/// setModel { $0.test = "\(Int.random(in: 1...10))" }
/// }
/// }
///
/// }
///
/// ```
public protocol Model {

/// Data about the model's state value.
var model: ModelData? { get set }

}

/// Data about a model's state value.
public struct ModelData {

/// The state value's identifier.
var id: UUID
/// Whether to force update the views when this value changes.
var force: Bool

}

extension Model {

/// Update the model.
/// - Parameter setModel: Update the model in this closure.
public func setModel(_ setModel: (inout Self) -> Void) {
guard let data = model else {
return
}
var model = self
setModel(&model)
StateManager.setState(id: data.id, value: model)
StateManager.updateState(id: data.id)
StateManager.updateViews(force: data.force)
}

}
8 changes: 7 additions & 1 deletion Sources/Model/Data Flow/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ public struct State<Value>: StateProtocol {
get {
guard let value = StateManager.getState(id: id) as? Value else {
let initialValue = getInitialValue()
StateManager.setState(id: id, value: initialValue)
if var model = initialValue as? Model {
model.model = .init(id: id, force: forceUpdates)
StateManager.setState(id: id, value: model)
StateManager.addConstantID(id)
} else {
StateManager.setState(id: id, value: initialValue)
}
return initialValue
}
return value
Expand Down
10 changes: 9 additions & 1 deletion Sources/Model/Data Flow/StateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public enum StateManager {

/// The state's identifier.
var id: UUID
/// Old identifiers of the state which need to be saved.
var oldIDs: [UUID] = []
/// The state value.
var value: Any?
/// Whether to update in the next iteration.
Expand All @@ -35,7 +37,7 @@ public enum StateManager {
/// - Parameter id: The identifier.
/// - Returns: Whether the id is contained.
func contains(id: UUID) -> Bool {
id == self.id
id == self.id || oldIDs.contains(id)
}

/// Change the identifier to a new one.
Expand Down Expand Up @@ -118,4 +120,10 @@ public enum StateManager {
}
}

/// Save a state's identifier until the program ends.
/// - Parameter id: The identifier.
static func addConstantID(_ id: UUID) {
state[safe: state.firstIndex { $0.id == id }]?.oldIDs.append(id)
}

}
14 changes: 11 additions & 3 deletions Tests/DemoApp/DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,23 @@ struct TestView: View {
var view: Body {
Backend2.TestWidget4()
Backend1.Button(test.test) {
test.test = "\(Int.random(in: 1...10))"
test.updateAsync()
}
}

}

struct TestModel {
struct TestModel: Model {

var test = "Label"

}
var model: ModelData?

func updateAsync() {
Task {
// Do something
setModel { $0.test = "\(Int.random(in: 1...10))" }
}
}

}

0 comments on commit f8fb7dd

Please sign in to comment.