diff --git a/Sources/Meta.docc/Principles/Backends.md b/Sources/Meta.docc/Principles/Backends.md index 91e4fee..feec31d 100644 --- a/Sources/Meta.docc/Principles/Backends.md +++ b/Sources/Meta.docc/Principles/Backends.md @@ -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 diff --git a/Sources/Meta.docc/Principles/StateConcept.md b/Sources/Meta.docc/Principles/StateConcept.md index f7a7236..850ad3a 100644 --- a/Sources/Meta.docc/Principles/StateConcept.md +++ b/Sources/Meta.docc/Principles/StateConcept.md @@ -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 diff --git a/Sources/Model/Data Flow/Model.swift b/Sources/Model/Data Flow/Model.swift new file mode 100644 index 0000000..8bb2fa2 --- /dev/null +++ b/Sources/Model/Data Flow/Model.swift @@ -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) + } + +} diff --git a/Sources/Model/Data Flow/State.swift b/Sources/Model/Data Flow/State.swift index 7c15f05..393d2a4 100644 --- a/Sources/Model/Data Flow/State.swift +++ b/Sources/Model/Data Flow/State.swift @@ -37,7 +37,13 @@ public struct State: 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 diff --git a/Sources/Model/Data Flow/StateManager.swift b/Sources/Model/Data Flow/StateManager.swift index 2607f01..a6b051f 100644 --- a/Sources/Model/Data Flow/StateManager.swift +++ b/Sources/Model/Data Flow/StateManager.swift @@ -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) + } + } diff --git a/Tests/DemoApp/DemoApp.swift b/Tests/DemoApp/DemoApp.swift index 8d7fcc9..d07ce77 100644 --- a/Tests/DemoApp/DemoApp.swift +++ b/Tests/DemoApp/DemoApp.swift @@ -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))" } + } + } + +}