Add models
This commit is contained in:
parent
5b3e66ed37
commit
f8fb7dd838
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
80
Sources/Model/Data Flow/Model.swift
Normal file
80
Sources/Model/Data Flow/Model.swift
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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))" }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user