Add models
This commit is contained in:
parent
5b3e66ed37
commit
f8fb7dd838
@ -39,11 +39,12 @@ struct Subtasks: App {
|
|||||||
## Cross-Platform Apps
|
## 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.
|
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.
|
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 protocol.
|
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 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
|
## 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
|
```swift
|
||||||
@Observable
|
struct ContentData {
|
||||||
class TaskModel {
|
|
||||||
|
|
||||||
var tasks: [String] = []
|
var count = 0
|
||||||
|
var label = "Hello"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
@State private var model = TaskModel()
|
@State private var data = ContentData()
|
||||||
|
|
||||||
var view: Body {
|
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
|
## 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 {
|
get {
|
||||||
guard let value = StateManager.getState(id: id) as? Value else {
|
guard let value = StateManager.getState(id: id) as? Value else {
|
||||||
let initialValue = getInitialValue()
|
let initialValue = getInitialValue()
|
||||||
|
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)
|
StateManager.setState(id: id, value: initialValue)
|
||||||
|
}
|
||||||
return initialValue
|
return initialValue
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
|||||||
@ -26,6 +26,8 @@ public enum StateManager {
|
|||||||
|
|
||||||
/// The state's identifier.
|
/// The state's identifier.
|
||||||
var id: UUID
|
var id: UUID
|
||||||
|
/// Old identifiers of the state which need to be saved.
|
||||||
|
var oldIDs: [UUID] = []
|
||||||
/// The state value.
|
/// The state value.
|
||||||
var value: Any?
|
var value: Any?
|
||||||
/// Whether to update in the next iteration.
|
/// Whether to update in the next iteration.
|
||||||
@ -35,7 +37,7 @@ public enum StateManager {
|
|||||||
/// - Parameter id: The identifier.
|
/// - Parameter id: The identifier.
|
||||||
/// - Returns: Whether the id is contained.
|
/// - Returns: Whether the id is contained.
|
||||||
func contains(id: UUID) -> Bool {
|
func contains(id: UUID) -> Bool {
|
||||||
id == self.id
|
id == self.id || oldIDs.contains(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the identifier to a new one.
|
/// 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 {
|
var view: Body {
|
||||||
Backend2.TestWidget4()
|
Backend2.TestWidget4()
|
||||||
Backend1.Button(test.test) {
|
Backend1.Button(test.test) {
|
||||||
test.test = "\(Int.random(in: 1...10))"
|
test.updateAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestModel {
|
struct TestModel: Model {
|
||||||
|
|
||||||
var test = "Label"
|
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