From bc03112d2d3542ff1034007350ccca5e4541a589 Mon Sep 17 00:00:00 2001 From: david-swift Date: Thu, 19 Sep 2024 12:40:46 +0200 Subject: [PATCH] Migrate package to strict concurrency --- Package.swift | 2 +- Sources/Model/Data Flow/Binding.swift | 17 +-- Sources/Model/Data Flow/Model.swift | 35 +++--- Sources/Model/Data Flow/State.swift | 6 +- Sources/Model/Data Flow/StateContent.swift | 4 +- Sources/Model/Data Flow/StateManager.swift | 10 +- Sources/Model/Data Flow/StateProtocol.swift | 2 +- Sources/Model/Extensions/Array.swift | 67 +++++++++++- Sources/Model/Extensions/KeyPath.swift | 8 ++ Sources/Model/User Interface/App/App.swift | 24 ++-- .../Model/User Interface/App/AppStorage.swift | 49 +++++++-- .../App/StandardAppStorage.swift | 4 +- .../User Interface/Scene/SceneStorage.swift | 66 +++++++++-- .../Model/User Interface/View/AnyView.swift | 10 +- .../User Interface/View/Pointer/Pointer.swift | 25 +++++ .../View/Properties/BindingProperty.swift | 32 +++--- .../View/Properties/Property.swift | 103 ++++++++++-------- .../View/Properties/ViewProperty.swift | 8 +- .../User Interface/View/ViewStorage.swift | 77 +++++++++++-- .../Model/User Interface/View/Widget.swift | 8 +- Sources/View/AppearObserver.swift | 14 +-- Sources/View/ContentModifier.swift | 12 +- Sources/View/DummyEitherView.swift | 8 +- Sources/View/Freeze.swift | 8 +- Sources/View/InspectorWrapper.swift | 14 +-- Sources/View/ModifierStopper.swift | 8 +- Sources/View/StateWrapper.swift | 22 ++-- Tests/DemoApp/DemoApp.swift | 4 +- Tests/SampleBackends/Backend1.swift | 76 +++++++------ Tests/SampleBackends/Backend2.swift | 40 ++++--- 30 files changed, 510 insertions(+), 253 deletions(-) create mode 100644 Sources/Model/Extensions/KeyPath.swift create mode 100644 Sources/Model/User Interface/View/Pointer/Pointer.swift diff --git a/Package.swift b/Package.swift index f10cee9..f7cef12 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // // Package.swift // Meta diff --git a/Sources/Model/Data Flow/Binding.swift b/Sources/Model/Data Flow/Binding.swift index 2431c85..c89c4ec 100644 --- a/Sources/Model/Data Flow/Binding.swift +++ b/Sources/Model/Data Flow/Binding.swift @@ -39,7 +39,7 @@ /// ``` @propertyWrapper @dynamicMemberLookup -public struct Binding { +public struct Binding: Sendable where Value: Sendable { /// The value. public var wrappedValue: Value { @@ -64,11 +64,11 @@ public struct Binding { } /// The closure for getting the value. - private let getValue: () -> Value + private let getValue: @Sendable () -> Value /// The closure for settings the value. - private let setValue: (Value) -> Void + private let setValue: @Sendable (Value) -> Void /// Handlers observing whether the binding changes. - private var handlers: [(Value) -> Void] = [] + private var handlers: [@Sendable (Value) -> Void] = [] /// Get a property of any content of a `Binding` as a `Binding`. /// - Parameter keyPath: The path to the member. @@ -85,7 +85,7 @@ public struct Binding { /// - Parameters: /// - get: The closure for getting the value. /// - set: The closure for setting the value. - public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { + public init(get: @Sendable @escaping () -> Value, set: @Sendable @escaping (Value) -> Void) { self.getValue = get self.setValue = set } @@ -104,7 +104,7 @@ public struct Binding { /// Observe whether data is changed over this binding. /// - Parameter handler: The handler. /// - Returns: The binding. - public func onSet(_ handler: @escaping (Value) -> Void) -> Self { + public func onSet(_ handler: @Sendable @escaping (Value) -> Void) -> Self { var newSelf = self newSelf.handlers.append(handler) return newSelf @@ -113,7 +113,7 @@ public struct Binding { } /// Extend bindings. -extension Binding where Value: MutableCollection { +extension Binding where Value: MutableCollection, Value.Index: Sendable, Value.Element: Sendable { /// Get a child at a certain index of the array as a binding. /// - Parameters: @@ -136,7 +136,8 @@ extension Binding where Value: MutableCollection { } /// Extend bindings. -extension Binding where Value: MutableCollection, Value.Element: Identifiable { +extension Binding +where Value: MutableCollection, Value.Element: Identifiable, Value.Index: Sendable, Value.Element: Sendable { /// Get a child of the array with a certain id as a binding. /// - Parameters: diff --git a/Sources/Model/Data Flow/Model.swift b/Sources/Model/Data Flow/Model.swift index 331691d..2f82678 100644 --- a/Sources/Model/Data Flow/Model.swift +++ b/Sources/Model/Data Flow/Model.swift @@ -58,7 +58,7 @@ public protocol Model { } /// Data about a model's state value. -public struct ModelData { +public struct ModelData: Sendable { /// The state value's identifier. var storage: StateContent.Storage @@ -67,23 +67,8 @@ public struct ModelData { } -/// Extend the model. extension Model { - /// Get the value as a binding. - public var binding: Binding { - .init { - getModel() - } set: { newValue in - guard let data = model else { - return - } - data.storage.value = newValue - data.storage.update = true - StateManager.updateViews(force: data.force) - } - } - /// Set the model up. /// /// At the point this function gets called, the model data is available. @@ -116,3 +101,21 @@ extension Model { } } + +extension Model where Self: Sendable { + + /// Get the value as a binding. + public var binding: Binding { + .init { + getModel() + } set: { newValue in + guard let data = model else { + return + } + data.storage.value = newValue + data.storage.update = true + StateManager.updateViews(force: data.force) + } + } + +} diff --git a/Sources/Model/Data Flow/State.swift b/Sources/Model/Data Flow/State.swift index a4d27cf..cfc3d06 100644 --- a/Sources/Model/Data Flow/State.swift +++ b/Sources/Model/Data Flow/State.swift @@ -9,7 +9,7 @@ import Foundation /// A property wrapper for properties in a view that should be stored throughout view updates. @propertyWrapper -public struct State: StateProtocol { +public struct State: StateProtocol, Sendable where Value: Sendable { /// Access the stored value. This updates the views when being changed. public var wrappedValue: Value { @@ -49,7 +49,7 @@ public struct State: StateProtocol { var forceUpdates: Bool /// The closure for initializing the state property's value. - var getInitialValue: () -> Value + var getInitialValue: @Sendable () -> Value /// The content. let content: StateContent = .init() @@ -59,7 +59,7 @@ public struct State: StateProtocol { /// - wrappedValue: The wrapped value. /// - id: An explicit identifier. /// - forceUpdates: Whether to force update all available views when the property gets modified. - public init(wrappedValue: @autoclosure @escaping () -> Value, forceUpdates: Bool = false) { + public init(wrappedValue: @Sendable @autoclosure @escaping () -> Value, forceUpdates: Bool = false) { getInitialValue = wrappedValue self.forceUpdates = forceUpdates } diff --git a/Sources/Model/Data Flow/StateContent.swift b/Sources/Model/Data Flow/StateContent.swift index aff3663..509f9de 100644 --- a/Sources/Model/Data Flow/StateContent.swift +++ b/Sources/Model/Data Flow/StateContent.swift @@ -6,7 +6,7 @@ // /// A class storing the state's content. -class StateContent { +final class StateContent: @unchecked Sendable { /// The storage. var storage: Storage? @@ -39,7 +39,7 @@ class StateContent { init() { } /// A class storing the value. - class Storage { + class Storage: @unchecked Sendable { /// The stored value. var value: Any diff --git a/Sources/Model/Data Flow/StateManager.swift b/Sources/Model/Data Flow/StateManager.swift index 147acb2..16b3a19 100644 --- a/Sources/Model/Data Flow/StateManager.swift +++ b/Sources/Model/Data Flow/StateManager.swift @@ -8,14 +8,14 @@ import Foundation /// This type manages view updates. -public enum StateManager { +public actor StateManager { /// Whether to block updates in general. public static var blockUpdates = false /// The application identifier. static var appID: String? /// The functions handling view updates. - static var updateHandlers: [(Bool) -> Void] = [] + static var updateHandlers: [@Sendable (Bool) async -> Void] = [] /// Update all of the views. /// - Parameter force: Whether to force all views to update. @@ -24,14 +24,16 @@ public enum StateManager { public static func updateViews(force: Bool = false) { if !blockUpdates { for handler in updateHandlers { - handler(force) + Task { + await handler(force) + } } } } /// Add a handler that is called when the user interface should update. /// - Parameter handler: The handler. The parameter defines whether the whole UI should be force updated. - static func addUpdateHandler(handler: @escaping (Bool) -> Void) { + static func addUpdateHandler(handler: @Sendable @escaping (Bool) async -> Void) { updateHandlers.append(handler) } diff --git a/Sources/Model/Data Flow/StateProtocol.swift b/Sources/Model/Data Flow/StateProtocol.swift index 54f1e6d..d3fe44b 100644 --- a/Sources/Model/Data Flow/StateProtocol.swift +++ b/Sources/Model/Data Flow/StateProtocol.swift @@ -8,7 +8,7 @@ import Foundation /// An interface for accessing `State` without specifying the generic type. -protocol StateProtocol { +protocol StateProtocol: Sendable { /// The state content. var content: StateContent { get } diff --git a/Sources/Model/Extensions/Array.swift b/Sources/Model/Extensions/Array.swift index 6aee8bd..c369e6c 100644 --- a/Sources/Model/Extensions/Array.swift +++ b/Sources/Model/Extensions/Array.swift @@ -44,10 +44,10 @@ extension Array: AnyView where Element == AnyView { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { + ) async where Data: ViewRenderData { for (index, element) in filter({ $0.renderable(type: type, data: data) }).enumerated() { if let storage = storages[safe: index] { - element + await element .widget(data: data, type: type) .updateStorage(storage, data: data, updateProperties: updateProperties, type: type) } @@ -62,9 +62,9 @@ extension Array: AnyView where Element == AnyView { public func storages( data: WidgetData, type: Data.Type - ) -> [ViewStorage] where Data: ViewRenderData { - compactMap { view in - view.renderable(type: type, data: data) ? view.storage(data: data, type: type) : nil + ) async -> [ViewStorage] where Data: ViewRenderData { + await compactMap { view in + await view.renderable(type: type, data: data) ? view.storage(data: data, type: type) : nil } } @@ -96,6 +96,63 @@ extension Array { } } + /// Returns the first element of the sequence that satisfies the given predicate. + /// - Parameter predicate: A closure that takes an element of the sequence as its argument + /// and returns a Boolean value indicating whether the element is a match. + /// - Returns: The first element of the sequence that satisfies `predicate`, + /// or `nil` if there is no element that satisfies `predicate`. + public func first(where predicate: (Element) async throws -> Bool) async rethrows -> Element? { + for element in self { + let matches = try await predicate(element) + if matches { + return element + } + } + return nil + } + + /// Returns the last element of the sequence that satisfies the given predicate. + /// - Parameter predicate: A closure that takes an element of the sequence as its argument + /// and returns a Boolean value indicating whether the element is a match. + /// - Returns: The last element of the sequence that satisfies `predicate`, + /// or `nil` if there is no element that satisfies `predicate`. + public func last(where predicate: (Element) async throws -> Bool) async rethrows -> Element? { + try await reversed().first(where: predicate) + } + + /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. + /// - Parameter predicate: A closure that takes an element of the sequence as its argument + /// and returns a Boolean value indicating whether the element is a match. + /// - Returns: Whether the sequence contains an element that satisfies `predicate`. + public func contains(where predicate: (Element) async throws -> Bool) async rethrows -> Bool { + try await first(where: predicate) != nil + } + + /// Returns an array containing the results of mapping the given closure over the sequence’s elements. + /// Remove elements that are `nil`. + /// - Parameter transform: Transforms each element. + /// - Returns: The result. + public func compactMap( + _ transform: (Element) async throws -> ElementOfResult? + ) async rethrows -> [ElementOfResult] { + var result: [ElementOfResult] = [] + for element in self { + if let element = try await transform(element) { + result.append(element) + } + } + return result + } + + /// Returns an array containing the results of mapping the given closure over the sequence’s elements. + /// - Parameter transform: Transforms each element. + /// - Returns: The result. + public func map( + _ transform: (Element) async throws -> ElementOfResult + ) async rethrows -> [ElementOfResult] { + try await compactMap { try await transform($0) } + } + } /// Extend arrays. diff --git a/Sources/Model/Extensions/KeyPath.swift b/Sources/Model/Extensions/KeyPath.swift new file mode 100644 index 0000000..7753759 --- /dev/null +++ b/Sources/Model/Extensions/KeyPath.swift @@ -0,0 +1,8 @@ +// +// KeyPath.swift +// Meta +// +// Created by david-swift on 18.09.24. +// + +extension KeyPath: @retroactive @unchecked Sendable { } diff --git a/Sources/Model/User Interface/App/App.swift b/Sources/Model/User Interface/App/App.swift index 976a2e4..c3c43c4 100644 --- a/Sources/Model/User Interface/App/App.swift +++ b/Sources/Model/User Interface/App/App.swift @@ -21,7 +21,7 @@ /// } /// ``` /// -public protocol App { +public protocol App: Sendable { /// The app storage type. associatedtype Storage: AppStorage @@ -60,26 +60,30 @@ extension App { public static func setupApp() -> Self { var appInstance = self.init() appInstance.app = Storage(id: appInstance.id) - appInstance.app.storage.app = { appInstance } - StateManager.addUpdateHandler { force in + StateManager.addUpdateHandler { [appInstance] force in let updateProperties = force || appInstance.getState().contains { $0.value.content.update } var removeIndices: [Int] = [] - for (index, element) in appInstance.app.storage.sceneStorage.enumerated() { - if element.destroy { + for (index, element) in await appInstance.app.storage.sceneStorage.enumerated() { + if await element.destroy { removeIndices.insert(index, at: 0) - } else if let scene = appInstance.scene.first( - where: { $0.id == element.id } + } else if let scene = await appInstance.scene.first( + where: { await $0.id == element.id } ) as? Storage.SceneElementType as? SceneElement { scene.update(element, app: appInstance.app, updateProperties: updateProperties) } } for index in removeIndices { - appInstance.app.storage.sceneStorage.remove(at: index) + await appInstance.app.modifyStandardAppStorage { $0.sceneStorage.remove(at: index) } } } StateManager.appID = appInstance.id - let state = appInstance.getState() - appInstance.app.storage.stateStorage = state + Task { [appInstance] in + let state = appInstance.getState() + await appInstance.app.modifyStandardAppStorage { storage in + storage.stateStorage = state + storage.app = { appInstance } + } + } return appInstance } diff --git a/Sources/Model/User Interface/App/AppStorage.swift b/Sources/Model/User Interface/App/AppStorage.swift index 3b8a6ce..93f4df4 100644 --- a/Sources/Model/User Interface/App/AppStorage.swift +++ b/Sources/Model/User Interface/App/AppStorage.swift @@ -6,7 +6,7 @@ // /// The app storage protocol. -public protocol AppStorage: AnyObject { +public protocol AppStorage: Actor, Sendable { /// The type of scene elements (which should be backend-specific). associatedtype SceneElementType @@ -20,10 +20,28 @@ public protocol AppStorage: AnyObject { /// Run the application. /// - Parameter setup: A closure that is expected to be executed right at the beginning. - func run(setup: @escaping () -> Void) + nonisolated func run(setup: @escaping () -> Void) /// Terminate the application. - func quit() + nonisolated func quit() + +} + +extension AppStorage { + + /// Modify the app storage. + /// - Parameter modify: The modifications. + func modifyStandardAppStorage(_ modify: (inout StandardAppStorage) -> Void) { + var copy = storage + modify(©) + self.storage = copy + } + + /// Append a scene. + /// - Parameter scene: The scene. + public func appendScene(_ scene: SceneStorage) { + modifyStandardAppStorage { $0.sceneStorage.append(scene) } + } } @@ -32,13 +50,28 @@ extension AppStorage { /// Focus the scene element with a certain id (if supported). Create the element if it doesn't already exist. /// - Parameter id: The element's id. - public func showSceneElement(_ id: String) { - storage.sceneStorage.last { $0.id == id && !$0.destroy }?.show() ?? addSceneElement(id) + nonisolated public func showSceneElement(_ id: String) { + Task { + await storage.sceneStorage + .last { scene in + let destroy = await scene.destroy + return await scene.id == id && !destroy + }? + .show() ?? addSceneElement(id) + } } /// Add a new scene element with the content of the scene element with a certain id. /// - Parameter id: The element's id. - public func addSceneElement(_ id: String) { + nonisolated public func addSceneElement(_ id: String) { + Task { + await internalAddSceneElement(id) + } + } + + /// Add a new scene element with the content of the scene element with a certain id. + /// - Parameter id: The element's id. + func internalAddSceneElement(_ id: String) { if let element = storage.app?().scene.last(where: { $0.id == id }) { let container = element.container(app: self) storage.sceneStorage.append(container) @@ -48,13 +81,13 @@ extension AppStorage { /// Focus the window with a certain id (if supported). Create the window if it doesn't already exist. /// - Parameter id: The window's id. - public func showWindow(_ id: String) { + nonisolated public func showWindow(_ id: String) { showSceneElement(id) } /// Add a new window with the content of the window template with a certain id. /// - Parameter id: The window template's id. - public func addWindow(_ id: String) { + nonisolated public func addWindow(_ id: String) { addSceneElement(id) } diff --git a/Sources/Model/User Interface/App/StandardAppStorage.swift b/Sources/Model/User Interface/App/StandardAppStorage.swift index da9dc32..38805a8 100644 --- a/Sources/Model/User Interface/App/StandardAppStorage.swift +++ b/Sources/Model/User Interface/App/StandardAppStorage.swift @@ -6,7 +6,7 @@ // /// The app storage protocol. -public struct StandardAppStorage { +public struct StandardAppStorage: Sendable { /// The scene storage. public var sceneStorage: [SceneStorage] = [] @@ -15,7 +15,7 @@ public struct StandardAppStorage { var stateStorage: [String: StateProtocol] = [:] /// The scene. - var app: (() -> any App)? + var app: (@Sendable () -> any App)? /// Initialize the standard app storage. public init() { } diff --git a/Sources/Model/User Interface/Scene/SceneStorage.swift b/Sources/Model/User Interface/Scene/SceneStorage.swift index 1f26763..edcb9ab 100644 --- a/Sources/Model/User Interface/Scene/SceneStorage.swift +++ b/Sources/Model/User Interface/Scene/SceneStorage.swift @@ -6,29 +6,29 @@ // /// Store a reference to a rendered scene element in a view storage. -public class SceneStorage { +public actor SceneStorage { /// The scene element's identifier. public var id: String /// The pointer. /// /// It can be a C pointer, a Swift class, or other information depending on the backend. - public var pointer: Any? + public var pointer: Sendable? /// The scene element's view content. - public var content: [String: [ViewStorage]] + var content: [String: [ViewStorage]] /// Various properties of a scene element. - public var fields: [String: Any] = [:] + var fields: [String: Sendable] = [:] /// Whether the reference to the window should disappear in the next update. public var destroy = false /// Show the scene element (including moving into the foreground, if possible). - public var show: () -> Void + public var show: @Sendable () -> Void /// The previous state of the scene element. public var previousState: SceneElement? /// The pointer as an opaque pointer, as this may be needed with backends interoperating with C or C++. - public var opaquePointer: OpaquePointer? { + public var opaquePointer: Pointer? { get { - pointer as? OpaquePointer + pointer as? Pointer } set { pointer = newValue @@ -43,9 +43,9 @@ public class SceneStorage { /// - show: Function called when the scene element should be displayed. public init( id: String, - pointer: Any?, + pointer: Sendable?, content: [String: [ViewStorage]] = [:], - show: @escaping () -> Void + show: @Sendable @escaping () -> Void ) { self.id = id self.pointer = pointer @@ -53,4 +53,52 @@ public class SceneStorage { self.show = show } + /// Set the pointer. + /// - Parameter value: The new pointer. + public func setPointer(_ value: Sendable?) { + pointer = value + } + + /// Set the element of a certain field. + /// - Parameters: + /// - key: The key. + /// - value: The field. + public func setField(key: String, value: Sendable) { + fields[key] = value + } + + /// Get the element of a certain field. + /// - Parameter key: The key. + /// - Returns: The field. + public func getField(key: String) -> Sendable? { + fields[key] + } + + /// Set the content elements under a certain key. + /// - Parameters: + /// - key: The key. + /// - value: The content elements. + public func setContent(key: String, value: [ViewStorage]) { + content[key] = value + } + + /// Get the content elements under a certain key. + /// - Parameter key: The key. + /// - Returns: The content elements. + public func getContent(key: String) -> [ViewStorage] { + content[key] ?? [] + } + + /// Set the previous state. + /// - Parameter state: The state. + public func setPreviousState(_ state: SceneElement?) { + previousState = state + } + + /// Set whether the scene will be destroyed. + /// - Parameter destroy: Whether the scene will be destroyed. + public func setDestroy(_ destroy: Bool) { + self.destroy = destroy + } + } diff --git a/Sources/Model/User Interface/View/AnyView.swift b/Sources/Model/User Interface/View/AnyView.swift index 12a6d66..85092b9 100644 --- a/Sources/Model/User Interface/View/AnyView.swift +++ b/Sources/Model/User Interface/View/AnyView.swift @@ -6,7 +6,7 @@ // /// The view type used for any form of a view. -public protocol AnyView { +public protocol AnyView: Sendable { /// The view's content. @ViewBuilder var viewContent: Body { get } @@ -38,8 +38,8 @@ extension AnyView { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { - widget(data: data, type: type) + ) async where Data: ViewRenderData { + await widget(data: data, type: type) .update(storage, data: data, updateProperties: updateProperties, type: type) } @@ -51,8 +51,8 @@ extension AnyView { public func storage( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - widget(data: data, type: type).container(data: data, type: type) + ) async -> ViewStorage where Data: ViewRenderData { + await widget(data: data, type: type).container(data: data, type: type) } /// Wrap the view into a widget. diff --git a/Sources/Model/User Interface/View/Pointer/Pointer.swift b/Sources/Model/User Interface/View/Pointer/Pointer.swift new file mode 100644 index 0000000..8bac184 --- /dev/null +++ b/Sources/Model/User Interface/View/Pointer/Pointer.swift @@ -0,0 +1,25 @@ +// +// Pointer.swift +// Meta +// +// Created by david-swift on 10.07.24. +// + +/// A sendable pointer type. +public struct Pointer: Sendable { + + /// The pointer's bit pattern. + var bitPattern: Int + + /// Get the opaque pointer. + public var opaquePointer: OpaquePointer? { + .init(bitPattern: bitPattern) + } + + /// Initialize the pointer. + /// - Parameter pointer: The opaque pointer. + public init(_ pointer: OpaquePointer) { + bitPattern = .init(bitPattern: pointer) + } + +} diff --git a/Sources/Model/User Interface/View/Properties/BindingProperty.swift b/Sources/Model/User Interface/View/Properties/BindingProperty.swift index 156a583..c729544 100644 --- a/Sources/Model/User Interface/View/Properties/BindingProperty.swift +++ b/Sources/Model/User Interface/View/Properties/BindingProperty.swift @@ -10,14 +10,14 @@ /// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method /// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method. @propertyWrapper -public struct BindingProperty: BindingPropertyProtocol { +public struct BindingProperty: BindingPropertyProtocol where Value: Sendable { /// The wrapped binding. public var wrappedValue: Binding /// Observe the UI element. - var observe: (Pointer, Binding, ViewStorage) -> Void + var observe: @Sendable (Pointer, Binding, ViewStorage) async -> Void /// Set the UI element's property. - var setValue: (Pointer, Value, ViewStorage) -> Void + var setValue: @Sendable (Pointer, Value, ViewStorage) async -> Void /// Initialize a property. /// - Parameters: @@ -27,8 +27,8 @@ public struct BindingProperty: BindingPropertyProtocol { /// - pointer: The type of the pointer. public init( wrappedValue: Binding, - observe: @escaping (Pointer, Binding, ViewStorage) -> Void, - set setValue: @escaping (Pointer, Value, ViewStorage) -> Void, + observe: @Sendable @escaping (Pointer, Binding, ViewStorage) async -> Void, + set setValue: @Sendable @escaping (Pointer, Value, ViewStorage) async -> Void, pointer: Pointer.Type ) { self.wrappedValue = wrappedValue @@ -44,14 +44,14 @@ public struct BindingProperty: BindingPropertyProtocol { /// - pointer: The type of the pointer. public init( wrappedValue: Binding, - observe: @escaping (Pointer, Binding) -> Void, - set setValue: @escaping (Pointer, Value) -> Void, + observe: @Sendable @escaping (Pointer, Binding) async -> Void, + set setValue: @Sendable @escaping (Pointer, Value) async -> Void, pointer: Pointer.Type ) { self.init( wrappedValue: wrappedValue, - observe: { pointer, value, _ in observe(pointer, value) }, - set: { pointer, value, _ in setValue(pointer, value) }, + observe: { pointer, value, _ in await observe(pointer, value) }, + set: { pointer, value, _ in await setValue(pointer, value) }, pointer: pointer ) } @@ -59,19 +59,19 @@ public struct BindingProperty: BindingPropertyProtocol { } /// The binding property protocol. -protocol BindingPropertyProtocol { +protocol BindingPropertyProtocol: Sendable { /// The binding's wrapped value. - associatedtype Value + associatedtype Value: Sendable /// The storage's pointer. associatedtype Pointer /// The wrapped value. var wrappedValue: Binding { get } /// Observe the property. - var observe: (Pointer, Binding, ViewStorage) -> Void { get } + var observe: @Sendable (Pointer, Binding, ViewStorage) async -> Void { get } /// Set the property. - var setValue: (Pointer, Value, ViewStorage) -> Void { get } + var setValue: @Sendable (Pointer, Value, ViewStorage) async -> Void { get } } @@ -84,12 +84,12 @@ extension Widget { func setBindingProperty( property: Property, storage: ViewStorage - ) where Property: BindingPropertyProtocol { + ) async where Property: BindingPropertyProtocol { if let optional = property.wrappedValue.wrappedValue as? any OptionalProtocol, optional.optionalValue == nil { return } - if let pointer = storage.pointer as? Property.Pointer { - property.setValue(pointer, property.wrappedValue.wrappedValue, storage) + if let pointer = await storage.pointer as? Property.Pointer { + await property.setValue(pointer, property.wrappedValue.wrappedValue, storage) } } diff --git a/Sources/Model/User Interface/View/Properties/Property.swift b/Sources/Model/User Interface/View/Properties/Property.swift index 719ebfe..4321f3b 100644 --- a/Sources/Model/User Interface/View/Properties/Property.swift +++ b/Sources/Model/User Interface/View/Properties/Property.swift @@ -10,10 +10,10 @@ /// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method /// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method. @propertyWrapper -public struct Property: PropertyProtocol { +public struct Property: PropertyProtocol where Value: Sendable { /// The function applying the property to the UI. - public var setProperty: (Pointer, Value, ViewStorage) -> Void + public var setProperty: @Sendable (Pointer, Value, ViewStorage) async -> Void /// The wrapped value. public var wrappedValue: Value /// The update strategy. @@ -27,7 +27,7 @@ public struct Property: PropertyProtocol { /// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases. public init( wrappedValue: Value, - set setProperty: @escaping (Pointer, Value, ViewStorage) -> Void, + set setProperty: @Sendable @escaping (Pointer, Value, ViewStorage) async -> Void, pointer: Pointer.Type, updateStrategy: UpdateStrategy = .automatic ) { @@ -44,13 +44,13 @@ public struct Property: PropertyProtocol { /// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases. public init( wrappedValue: Value, - set setProperty: @escaping (Pointer, Value) -> Void, + set setProperty: @Sendable @escaping (Pointer, Value) async -> Void, pointer: Pointer.Type, updateStrategy: UpdateStrategy = .automatic ) { self.init( wrappedValue: wrappedValue, - set: { pointer, value, _ in setProperty(pointer, value) }, + set: { pointer, value, _ in await setProperty(pointer, value) }, pointer: pointer, updateStrategy: updateStrategy ) @@ -66,13 +66,13 @@ extension Property where Value: OptionalProtocol { /// - pointer: The type of the pointer. /// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases. public init( - set setProperty: @escaping (Pointer, Value.Wrapped, ViewStorage) -> Void, + set setProperty: @Sendable @escaping (Pointer, Value.Wrapped, ViewStorage) async -> Void, pointer: Pointer.Type, updateStrategy: UpdateStrategy = .automatic ) { self.setProperty = { pointer, value, storage in if let value = value.optionalValue { - setProperty(pointer, value, storage) + await setProperty(pointer, value, storage) } } wrappedValue = nil @@ -86,12 +86,12 @@ extension Property where Value: OptionalProtocol { /// - pointer: The type of the pointer. /// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases. public init( - set setProperty: @escaping (Pointer, Value.Wrapped) -> Void, + set setProperty: @Sendable @escaping (Pointer, Value.Wrapped) async -> Void, pointer: Pointer.Type, updateStrategy: UpdateStrategy = .automatic ) { self.init( - set: { pointer, value, _ in setProperty(pointer, value) }, + set: { pointer, value, _ in await setProperty(pointer, value) }, pointer: pointer, updateStrategy: updateStrategy ) @@ -100,7 +100,7 @@ extension Property where Value: OptionalProtocol { } /// The property protocol. -protocol PropertyProtocol { +protocol PropertyProtocol: Sendable { /// The type of the wrapped value. associatedtype Value @@ -110,14 +110,14 @@ protocol PropertyProtocol { /// The wrapped value. var wrappedValue: Value { get } /// Set the property. - var setProperty: (Pointer, Value, ViewStorage) -> Void { get } + var setProperty: @Sendable (Pointer, Value, ViewStorage) async -> Void { get } /// The update strategy. var updateStrategy: UpdateStrategy { get } } /// The update strategy for properties. -public enum UpdateStrategy { +public enum UpdateStrategy: Sendable { /// If equatable, update only when the value changed. /// If not equatable, this is equivalent to ``UpdateStrategy/always``. @@ -140,10 +140,10 @@ extension Widget { public func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { + ) async -> ViewStorage where Data: ViewRenderData { let storage = ViewStorage(initializeWidget()) - initProperties(storage, data: data, type: type) - update(storage, data: data, updateProperties: true, type: type) + await initProperties(storage, data: data, type: type) + await update(storage, data: data, updateProperties: true, type: type) return storage } @@ -160,10 +160,10 @@ extension Widget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { - self.updateProperties(storage, data: data, updateProperties: updateProperties, type: type) + ) async where Data: ViewRenderData { + await self.updateProperties(storage, data: data, updateProperties: updateProperties, type: type) if updateProperties { - storage.previousState = self + await storage.setPreviousState(self) } } @@ -176,16 +176,16 @@ extension Widget { _ storage: ViewStorage, data: WidgetData, type: Data.Type - ) where Data: ViewRenderData { + ) async where Data: ViewRenderData { let mirror = Mirror(reflecting: self) for property in mirror.children { if let value = property.value as? any ViewPropertyProtocol { - let subview = value.wrappedValue.storage(data: data, type: type) - initViewProperty(value, view: subview, parent: storage) - storage.content[property.label ?? .mainContent] = [subview] + let subview = await value.wrappedValue.storage(data: data, type: type) + await initViewProperty(value, view: subview, parent: storage) + await storage.setContent(key: property.label ?? .mainContent, value: [subview]) } if let value = property.value as? any BindingPropertyProtocol { - initBindingProperty(value, parent: storage) + await initBindingProperty(value, parent: storage) } } } @@ -199,9 +199,10 @@ extension Widget { _ value: Property, view: ViewStorage, parent: ViewStorage - ) where Property: ViewPropertyProtocol { - if let view = view.pointer as? Property.ViewPointer, let pointer = parent.pointer as? Property.Pointer { - value.setView(pointer, view) + ) async where Property: ViewPropertyProtocol { + if let view = await view.pointer as? Property.ViewPointer, + let pointer = await parent.pointer as? Property.Pointer { + await value.setView(pointer, view) } } @@ -209,9 +210,12 @@ extension Widget { /// - Parameters: /// - value: The property. /// - parent: The view storage. - func initBindingProperty(_ value: Property, parent: ViewStorage) where Property: BindingPropertyProtocol { - if let view = parent.pointer as? Property.Pointer { - value.observe( + func initBindingProperty( + _ value: Property, + parent: ViewStorage + ) async where Property: BindingPropertyProtocol { + if let view = await parent.pointer as? Property.Pointer { + await value.observe( view, .init { value.wrappedValue.wrappedValue @@ -237,14 +241,20 @@ extension Widget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { + ) async where Data: ViewRenderData { let mirror = Mirror(reflecting: self) - updateNotEquatable(mirror: mirror, storage: storage, data: data, updateProperties: updateProperties, type: type) + await updateNotEquatable( + mirror: mirror, + storage: storage, + data: data, + updateProperties: updateProperties, + type: type + ) guard updateProperties else { return } - updateAlwaysWhenStateUpdate(mirror: mirror, storage: storage) - updateEquatable(mirror: mirror, storage: storage) + await updateAlwaysWhenStateUpdate(mirror: mirror, storage: storage) + await updateEquatable(mirror: mirror, storage: storage) } /// Update the properties which are not equatable and should always be updated (e.g. closures). @@ -260,20 +270,21 @@ extension Widget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { + ) async where Data: ViewRenderData { for property in mirror.children { if let value = property.value as? any PropertyProtocol { if value.updateStrategy == .always || value.wrappedValue as? any Equatable == nil && value.updateStrategy != .alwaysWhenStateUpdate { - setProperty(property: value, storage: storage) + await setProperty(property: value, storage: storage) } } if let value = property.value as? any ViewPropertyProtocol, - let storage = storage.content[property.label ?? .mainContent]?.first { - value.wrappedValue.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) + let storage = await storage.getContent(key: property.label ?? .mainContent).first { + await value.wrappedValue + .updateStorage(storage, data: data, updateProperties: updateProperties, type: type) } if let value = property.value as? any BindingPropertyProtocol { - setBindingProperty(property: value, storage: storage) + await setBindingProperty(property: value, storage: storage) } } } @@ -285,11 +296,11 @@ extension Widget { /// - storage: The view storage. /// /// Initialize the ``Property`` property wrapper with the ``UpdateStrategy/alwaysWhenStateUpdate``. - func updateAlwaysWhenStateUpdate(mirror: Mirror, storage: ViewStorage) { + func updateAlwaysWhenStateUpdate(mirror: Mirror, storage: ViewStorage) async { for property in mirror.children { if let value = property.value as? any PropertyProtocol { if value.updateStrategy == .alwaysWhenStateUpdate { - setProperty(property: value, storage: storage) + await setProperty(property: value, storage: storage) } } } @@ -299,8 +310,8 @@ extension Widget { /// - Parameters: /// - mirror: A mirror of the widget. /// - storage: The view storage. - func updateEquatable(mirror: Mirror, storage: ViewStorage) { - let previousState: Mirror.Children? = if let previousState = storage.previousState { + func updateEquatable(mirror: Mirror, storage: ViewStorage) async { + let previousState: Mirror.Children? = if let previousState = await storage.previousState { Mirror(reflecting: previousState).children } else { nil @@ -317,7 +328,7 @@ extension Widget { update = false } if update { - setProperty(property: value, storage: storage) + await setProperty(property: value, storage: storage) } } } @@ -354,12 +365,12 @@ extension Widget { /// - Parameters: /// - property: The property. /// - storage: The view storage. - func setProperty(property: Property, storage: ViewStorage) where Property: PropertyProtocol { + func setProperty(property: Property, storage: ViewStorage) async where Property: PropertyProtocol { if let optional = property.wrappedValue as? any OptionalProtocol, optional.optionalValue == nil { return } - if let pointer = storage.pointer as? Property.Pointer { - property.setProperty(pointer, property.wrappedValue, storage) + if let pointer = await storage.pointer as? Property.Pointer { + await property.setProperty(pointer, property.wrappedValue, storage) } } diff --git a/Sources/Model/User Interface/View/Properties/ViewProperty.swift b/Sources/Model/User Interface/View/Properties/ViewProperty.swift index dc3c0f6..3b09846 100644 --- a/Sources/Model/User Interface/View/Properties/ViewProperty.swift +++ b/Sources/Model/User Interface/View/Properties/ViewProperty.swift @@ -15,7 +15,7 @@ public struct ViewProperty: ViewPropertyProtocol { /// The wrapped value. public var wrappedValue: Body = [] /// Set the view. - var setView: (Pointer, ViewPointer) -> Void + var setView: @Sendable (Pointer, ViewPointer) async -> Void /// Initialize a property. /// - Parameters: @@ -23,7 +23,7 @@ public struct ViewProperty: ViewPropertyProtocol { /// - pointer: The pointer type of the parent view (usually a concrete view type). /// - subview: The pointer type of the child view (usually a protocol, view class, or similar). public init( - set setView: @escaping (Pointer, ViewPointer) -> Void, + set setView: @Sendable @escaping (Pointer, ViewPointer) async -> Void, pointer: Pointer.Type, subview: ViewPointer.Type ) { @@ -35,7 +35,7 @@ public struct ViewProperty: ViewPropertyProtocol { /// The view property protocol. /// /// Do not use for wrapper widgets. -protocol ViewPropertyProtocol { +protocol ViewPropertyProtocol: Sendable { /// The type of the view's pointer. associatedtype Pointer @@ -45,6 +45,6 @@ protocol ViewPropertyProtocol { /// The wrapped value. var wrappedValue: Body { get } /// Set the view. - var setView: (Pointer, ViewPointer) -> Void { get } + var setView: @Sendable (Pointer, ViewPointer) async -> Void { get } } diff --git a/Sources/Model/User Interface/View/ViewStorage.swift b/Sources/Model/User Interface/View/ViewStorage.swift index 5ef9d0e..e94a012 100644 --- a/Sources/Model/User Interface/View/ViewStorage.swift +++ b/Sources/Model/User Interface/View/ViewStorage.swift @@ -6,25 +6,25 @@ // /// Store a reference to a rendered view in a view storage. -public class ViewStorage { +public actor ViewStorage: Sendable { /// The pointer. /// /// It can be a C pointer, a Swift class, or other information depending on the backend. - public var pointer: Any? + public var pointer: Sendable? /// The view's content for container widgets. - public var content: [String: [ViewStorage]] + var content: [String: [ViewStorage]] /// The view's state (used in `StateWrapper`). var state: [String: StateProtocol] = [:] /// Various properties of a widget. - public var fields: [String: Any] = [:] + var fields: [String: Sendable] = [:] /// The previous state of the widget. public var previousState: Widget? /// The pointer as an opaque pointer, as this is needed with backends interoperating with C or C++. - public var opaquePointer: OpaquePointer? { + public var opaquePointer: Pointer? { get { - pointer as? OpaquePointer + pointer as? Pointer } set { pointer = newValue @@ -36,7 +36,7 @@ public class ViewStorage { /// - pointer: The pointer to the widget, its type depends on the backend. /// - content: The view's content for container widgets. public init( - _ pointer: Any?, + _ pointer: Sendable?, content: [String: [ViewStorage]] = [:], state: Widget? = nil ) { @@ -45,4 +45,67 @@ public class ViewStorage { self.previousState = state } + /// Set the state under a certain key. + /// - Parameters: + /// - key: The key. + /// - value: The state. + func setState(key: String, value: StateProtocol) { + state[key] = value + } + + /// Set the state. + /// - Parameter state: The state. + func setState(_ state: [String: StateProtocol]) { + self.state = state + } + + /// Get the state under a certain key. + /// - Parameter key: The key. + /// - Returns: The state. + func getState(key: String) -> StateProtocol? { + state[key] + } + + /// Set the pointer. + /// - Parameter value: The new pointer. + public func setPointer(_ value: Sendable?) { + pointer = value + } + + /// Set the element of a certain field. + /// - Parameters: + /// - key: The key. + /// - value: The field. + public func setField(key: String, value: Sendable) { + fields[key] = value + } + + /// Get the element of a certain field. + /// - Parameter key: The key. + /// - Returns: The field. + public func getField(key: String) -> Sendable? { + fields[key] + } + + /// Set the content elements under a certain key. + /// - Parameters: + /// - key: The key. + /// - value: The content elements. + public func setContent(key: String, value: [ViewStorage]) { + content[key] = value + } + + /// Get the content elements under a certain key. + /// - Parameter key: The key. + /// - Returns: The content elements. + public func getContent(key: String) -> [ViewStorage] { + content[key] ?? [] + } + + /// Set the previous state. + /// - Parameter state: The state. + public func setPreviousState(_ state: Widget?) { + previousState = state + } + } diff --git a/Sources/Model/User Interface/View/Widget.swift b/Sources/Model/User Interface/View/Widget.swift index 97b129c..92d007b 100644 --- a/Sources/Model/User Interface/View/Widget.swift +++ b/Sources/Model/User Interface/View/Widget.swift @@ -19,7 +19,7 @@ public protocol Widget: AnyView { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData + ) async -> ViewStorage where Data: ViewRenderData /// Update the stored content. /// - Parameters: @@ -32,14 +32,14 @@ public protocol Widget: AnyView { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData + ) async where Data: ViewRenderData /// Get the widget. /// - Returns: The widget. /// /// Define this function only if you do not define ``Widget/container(data:type:)``. /// Otherwise, it will not have an effect. - func initializeWidget() -> Any + func initializeWidget() -> Sendable } @@ -51,7 +51,7 @@ extension Widget { /// Print a warning if the widget does not set this function but it gets accessed. /// - Returns: A dummy pointer. - public func initializeWidget() -> Any { + public func initializeWidget() -> Sendable { print("Warning: Define initialize widget function or container function for \(Self.self)") return "" } diff --git a/Sources/View/AppearObserver.swift b/Sources/View/AppearObserver.swift index 035021a..8a724a0 100644 --- a/Sources/View/AppearObserver.swift +++ b/Sources/View/AppearObserver.swift @@ -9,7 +9,7 @@ struct AppearObserver: ConvenienceWidget { /// The custom code to edit the widget. - var modify: (ViewStorage) -> Void + var modify: @Sendable (ViewStorage) -> Void /// The wrapped view. var content: AnyView @@ -21,8 +21,8 @@ struct AppearObserver: ConvenienceWidget { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - let storage = content.storage(data: data, type: type) + ) async -> ViewStorage where Data: ViewRenderData { + let storage = await content.storage(data: data, type: type) modify(storage) return storage } @@ -38,8 +38,8 @@ struct AppearObserver: ConvenienceWidget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { - content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) + ) async where Data: ViewRenderData { + await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) } } @@ -50,14 +50,14 @@ extension AnyView { /// Run a function on the widget when it appears for the first time. /// - Parameter closure: The function. /// - Returns: A view. - public func inspectOnAppear(_ closure: @escaping (ViewStorage) -> Void) -> AnyView { + public func inspectOnAppear(_ closure: @Sendable @escaping (ViewStorage) -> Void) -> AnyView { AppearObserver(modify: closure, content: self) } /// Run a function when the view appears for the first time. /// - Parameter closure: The function. /// - Returns: A view. - public func onAppear(_ closure: @escaping () -> Void) -> AnyView { + public func onAppear(_ closure: @Sendable @escaping () -> Void) -> AnyView { inspectOnAppear { _ in closure() } } diff --git a/Sources/View/ContentModifier.swift b/Sources/View/ContentModifier.swift index d9708e9..2313e19 100644 --- a/Sources/View/ContentModifier.swift +++ b/Sources/View/ContentModifier.swift @@ -11,7 +11,7 @@ struct ContentModifier: ConvenienceWidget where Content: AnyView { /// The wrapped view. var content: AnyView /// The closure for the modification. - var modify: (Content) -> AnyView + var modify: @Sendable (Content) -> AnyView /// The view storage. /// - Parameters: @@ -21,8 +21,8 @@ struct ContentModifier: ConvenienceWidget where Content: AnyView { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - content.storage(data: data.modify { $0.modifiers += [modifyView] }, type: type) + ) async -> ViewStorage where Data: ViewRenderData { + await content.storage(data: data.modify { $0.modifiers += [modifyView] }, type: type) } /// Update the stored content. @@ -36,8 +36,8 @@ struct ContentModifier: ConvenienceWidget where Content: AnyView { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { - content + ) async where Data: ViewRenderData { + await content .updateStorage( storage, data: data.modify { $0.modifiers += [modifyView] }, @@ -68,7 +68,7 @@ extension AnyView { /// - Returns: A view. public func modifyContent( _ type: Content.Type, - modify: @escaping (Content) -> AnyView + modify: @Sendable @escaping (Content) -> AnyView ) -> AnyView where Content: AnyView { ContentModifier(content: self, modify: modify) } diff --git a/Sources/View/DummyEitherView.swift b/Sources/View/DummyEitherView.swift index f8b5841..c7fded5 100644 --- a/Sources/View/DummyEitherView.swift +++ b/Sources/View/DummyEitherView.swift @@ -23,9 +23,9 @@ struct DummyEitherView: ConvenienceWidget { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { + ) async -> ViewStorage where Data: ViewRenderData { let content = type.EitherViewType(condition) { view1 ?? [] } else: { view2 ?? [] } - let storage = content.storage(data: data, type: type) + let storage = await content.storage(data: data, type: type) return storage } @@ -40,9 +40,9 @@ struct DummyEitherView: ConvenienceWidget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { + ) async where Data: ViewRenderData { let content = type.EitherViewType(condition) { view1 ?? [] } else: { view2 ?? [] } - content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) + await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) } } diff --git a/Sources/View/Freeze.swift b/Sources/View/Freeze.swift index 0d2ffda..b9debc2 100644 --- a/Sources/View/Freeze.swift +++ b/Sources/View/Freeze.swift @@ -21,8 +21,8 @@ struct Freeze: ConvenienceWidget { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - content.storage(data: data, type: type) + ) async -> ViewStorage where Data: ViewRenderData { + await content.storage(data: data, type: type) } /// Update the stored content. @@ -36,11 +36,11 @@ struct Freeze: ConvenienceWidget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { + ) async where Data: ViewRenderData { guard !freeze else { return } - content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) + await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) } } diff --git a/Sources/View/InspectorWrapper.swift b/Sources/View/InspectorWrapper.swift index 1cd3bfb..36cc809 100644 --- a/Sources/View/InspectorWrapper.swift +++ b/Sources/View/InspectorWrapper.swift @@ -9,7 +9,7 @@ struct InspectorWrapper: ConvenienceWidget { /// The custom code to edit the widget. - var modify: (ViewStorage, Bool) -> Void + var modify: @Sendable (ViewStorage, Bool) -> Void /// The wrapped view. var content: AnyView @@ -21,8 +21,8 @@ struct InspectorWrapper: ConvenienceWidget { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - let storage = content.storage(data: data, type: type) + ) async -> ViewStorage where Data: ViewRenderData { + let storage = await content.storage(data: data, type: type) modify(storage, true) return storage } @@ -38,8 +38,8 @@ struct InspectorWrapper: ConvenienceWidget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { - content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) + ) async where Data: ViewRenderData { + await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) modify(storage, updateProperties) } @@ -51,14 +51,14 @@ extension AnyView { /// Run a custom code accessing the view's storage when initializing and updating the view. /// - Parameter modify: Modify the storage. The boolean indicates whether state in parent views changed. /// - Returns: A view. - public func inspect(_ modify: @escaping (ViewStorage, Bool) -> Void) -> AnyView { + public func inspect(_ modify: @Sendable @escaping (ViewStorage, Bool) -> Void) -> AnyView { InspectorWrapper(modify: modify, content: self) } /// Run a function when the view gets updated. /// - Parameter onUpdate: The function. /// - Returns: A view. - public func onUpdate(_ onUpdate: @escaping () -> Void) -> AnyView { + public func onUpdate(_ onUpdate: @Sendable @escaping () -> Void) -> AnyView { inspect { _, _ in onUpdate() } } diff --git a/Sources/View/ModifierStopper.swift b/Sources/View/ModifierStopper.swift index e93c753..5dc4e82 100644 --- a/Sources/View/ModifierStopper.swift +++ b/Sources/View/ModifierStopper.swift @@ -19,8 +19,8 @@ struct ModifierStopper: ConvenienceWidget { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - content.storage(data: data.noModifiers, type: type) + ) async -> ViewStorage where Data: ViewRenderData { + await content.storage(data: data.noModifiers, type: type) } /// Update the stored content. @@ -34,8 +34,8 @@ struct ModifierStopper: ConvenienceWidget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { - content.updateStorage(storage, data: data.noModifiers, updateProperties: updateProperties, type: type) + ) async where Data: ViewRenderData { + await content.updateStorage(storage, data: data.noModifiers, updateProperties: updateProperties, type: type) } } diff --git a/Sources/View/StateWrapper.swift b/Sources/View/StateWrapper.swift index 0991eb8..91573ff 100644 --- a/Sources/View/StateWrapper.swift +++ b/Sources/View/StateWrapper.swift @@ -9,13 +9,13 @@ struct StateWrapper: ConvenienceWidget { /// The content. - var content: () -> Body + var content: @Sendable () -> Body /// The state information (from properties with the `State` wrapper). var state: [String: StateProtocol] = [:] /// Initialize a `StateWrapper`. /// - Parameter content: The view content. - init(@ViewBuilder content: @escaping () -> Body) { + init(@ViewBuilder content: @Sendable @escaping () -> Body) { self.content = content } @@ -23,7 +23,7 @@ struct StateWrapper: ConvenienceWidget { /// - Parameters: /// - content: The view content. /// - state: The state information. - init(content: @escaping () -> Body, state: [String: StateProtocol]) { + init(content: @Sendable @escaping () -> Body, state: [String: StateProtocol]) { self.content = content self.state = state } @@ -40,10 +40,10 @@ struct StateWrapper: ConvenienceWidget { data: WidgetData, updateProperties: Bool, type: Data.Type - ) where Data: ViewRenderData { + ) async where Data: ViewRenderData { var updateProperties = updateProperties for property in state { - if let storage = storage.state[property.key]?.content.storage { + if let storage = await storage.getState(key: property.key)?.content.storage { property.value.content.storage = storage } if property.value.content.update { @@ -51,10 +51,10 @@ struct StateWrapper: ConvenienceWidget { property.value.content.update = false } } - guard let storage = storage.content[.mainContent]?.first else { + guard let storage = await storage.getContent(key: .mainContent).first else { return } - content().updateStorage(storage, data: data, updateProperties: updateProperties, type: type) + await content().updateStorage(storage, data: data, updateProperties: updateProperties, type: type) } /// Get a view storage. @@ -65,10 +65,10 @@ struct StateWrapper: ConvenienceWidget { func container( data: WidgetData, type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - let content = content().storage(data: data, type: type) - let storage = ViewStorage(content.pointer, content: [.mainContent: [content]]) - storage.state = state + ) async -> ViewStorage where Data: ViewRenderData { + let content = await content().storage(data: data, type: type) + let storage = ViewStorage(await content.pointer, content: [.mainContent: [content]]) + await storage.setState(state) for element in state { element.value.setup() } diff --git a/Tests/DemoApp/DemoApp.swift b/Tests/DemoApp/DemoApp.swift index 9471803..6f60d13 100644 --- a/Tests/DemoApp/DemoApp.swift +++ b/Tests/DemoApp/DemoApp.swift @@ -1,5 +1,5 @@ import Foundation -@testable import Meta +import Meta import SampleBackends @main @@ -73,7 +73,7 @@ struct TestView: View { } -struct TestModel: Model { +struct TestModel: Sendable, Model { var test = "Label" diff --git a/Tests/SampleBackends/Backend1.swift b/Tests/SampleBackends/Backend1.swift index a95d397..ef8c7ea 100644 --- a/Tests/SampleBackends/Backend1.swift +++ b/Tests/SampleBackends/Backend1.swift @@ -6,16 +6,16 @@ public enum Backend1 { public init() { } - public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { + public func container(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData { print("Init test widget 1") let storage = ViewStorage(nil) - storage.fields["test"] = 0 + await storage.setField(key: "test", value: 0) return storage } - public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) { - print("Update test widget 1 (#\(storage.fields["test"] ?? ""))") - storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1 + public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async { + print("Update test widget 1 (#\(await storage.getField(key: "test") ?? ""))") + await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1) } } @@ -24,16 +24,16 @@ public enum Backend1 { public init() { } - public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { + public func container(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData { print("Init test widget 3") let storage = ViewStorage(nil) - storage.fields["test"] = 0 + await storage.setField(key: "test", value: 0) return storage } - public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) { - print("Update test widget 3 (#\(storage.fields["test"] ?? ""))") - storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1 + public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async { + print("Update test widget 3 (#\(await storage.getField(key: "test") ?? ""))") + await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1) } } @@ -42,28 +42,28 @@ public enum Backend1 { @Property(set: { print("Update button (label = \($1))") }, pointer: Any.self) var label = "" - @Property(set: { $2.fields["action"] = $1 }, pointer: Any.self) - var action: () -> Void = { } + @Property(set: { $2.setField(key: "action", value: $1) }, pointer: Any.self) + var action: @Sendable () -> Void = { } - public init(_ label: String, action: @escaping () -> Void) { + public init(_ label: String, action: @Sendable @escaping () -> Void) { self.label = label self.action = action } - public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { + public func container(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData { print("Init button") let storage = ViewStorage(nil) Task { try await Task.sleep(nanoseconds: 1_000_000_000) - (storage.fields["action"] as? () -> Void)?() + (await storage.getField(key: "action") as? @Sendable () -> Void)?() } - storage.fields["action"] = action - storage.previousState = self + await storage.setField(key: "action", value: action) + await storage.setPreviousState(self) return storage } } - public struct Window: BackendSceneElement { + public struct Window: BackendSceneElement, Sendable { public var id: String var spawn: Int @@ -77,7 +77,9 @@ public enum Backend1 { public func setupInitialContainers(app: Storage) where Storage: AppStorage { for _ in 0..(_ storage: SceneStorage, app: Storage, updateProperties: Bool) where Storage: AppStorage { - print("Update \(id)") - guard let viewStorage = storage.content[.mainContent]?.first else { - return + Task { + print("Update \(id)") + guard let viewStorage = await storage.getContent(key: .mainContent).first else { + return + } + await content.updateStorage(viewStorage, data: .init(sceneStorage: storage, appStorage: app), updateProperties: updateProperties, type: MainViewRenderData.self) } - content.updateStorage(viewStorage, data: .init(sceneStorage: storage, appStorage: app), updateProperties: updateProperties, type: MainViewRenderData.self) } } @@ -109,17 +115,15 @@ public enum Backend1 { self.content = content() } - public func container(data: WidgetData, type: Data.Type) -> Meta.ViewStorage where Data: ViewRenderData { + public func container(data: WidgetData, type: Data.Type) async -> Meta.ViewStorage where Data: ViewRenderData { let storage = ViewStorage(nil) - storage.content = [.mainContent: content.storages(data: data, type: type)] + await storage.setContent(key: .mainContent, value: content.storages(data: data, type: type)) return storage } - public func update(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData { - guard let storages = storage.content[.mainContent] else { - return - } - content.update(storages, data: data, updateProperties: updateProperties, type: type) + public func update(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async where Data: ViewRenderData { + let storages = await storage.getContent(key: .mainContent) + await content.update(storages, data: data, updateProperties: updateProperties, type: type) } } @@ -150,19 +154,19 @@ public enum Backend1 { public protocol BackendSceneElement: SceneElement { } - public class Backend1App: AppStorage { + public actor Backend1App: AppStorage { public typealias SceneElementType = BackendSceneElement public var storage: StandardAppStorage = .init() - public required init(id: String) { } + public init(id: String) { } - public func run(setup: @escaping () -> Void) { + nonisolated public func run(setup: @escaping () -> Void) { setup() } - public func quit() { + nonisolated public func quit() { fatalError("Quit") } diff --git a/Tests/SampleBackends/Backend2.swift b/Tests/SampleBackends/Backend2.swift index b5b276d..ab54469 100644 --- a/Tests/SampleBackends/Backend2.swift +++ b/Tests/SampleBackends/Backend2.swift @@ -6,16 +6,16 @@ public enum Backend2 { public init() { } - public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { + public func container(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData { print("Init test widget 2") let storage = ViewStorage(nil) - storage.fields["test"] = 0 + await storage.setField(key: "test", value: 0) return storage } - public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) { - print("Update test widget 2 (#\(storage.fields["test"] ?? ""))") - storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1 + public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async { + print("Update test widget 2 (#\(await storage.getField(key: "test") ?? ""))") + await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1) } } @@ -24,16 +24,16 @@ public enum Backend2 { public init() { } - public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { + public func container(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData { print("Init test widget 4") let storage = ViewStorage(nil) - storage.fields["test"] = 0 + await storage.setField(key: "test", value: 0) return storage } - public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) { - print("Update test widget 4 (#\(storage.fields["test"] ?? ""))") - storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1 + public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async { + print("Update test widget 4 (#\(await storage.getField(key: "test") ?? ""))") + await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1) } } @@ -46,17 +46,15 @@ public enum Backend2 { self.content = content() } - public func container(data: WidgetData, type: Data.Type) -> Meta.ViewStorage where Data: ViewRenderData { + public func container(data: WidgetData, type: Data.Type) async -> Meta.ViewStorage where Data: ViewRenderData { let storage = ViewStorage(nil) - storage.content = [.mainContent: content.storages(data: data, type: type)] + await storage.setContent(key: .mainContent, value: content.storages(data: data, type: type)) return storage } - public func update(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData { - guard let storages = storage.content[.mainContent] else { - return - } - content.update(storages, data: data, updateProperties: updateProperties, type: type) + public func update(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async where Data: ViewRenderData { + let storages = await storage.getContent(key: .mainContent) + await content.update(storages, data: data, updateProperties: updateProperties, type: type) } } @@ -65,19 +63,19 @@ public enum Backend2 { public protocol BackendSceneElement: SceneElement { } - public class Backend2App: AppStorage { + public actor Backend2App: AppStorage{ public typealias SceneElementType = BackendSceneElement public var storage: StandardAppStorage = .init() - public required init(id: String) { } + public init(id: String) { } - public func run(setup: @escaping () -> Void) { + nonisolated public func run(setup: @escaping () -> Void) { setup() } - public func quit() { + nonisolated public func quit() { fatalError("Quit") }