diff --git a/Sources/Model/User Interface/App/App.swift b/Sources/Model/User Interface/App/App.swift index 680c858..976a2e4 100644 --- a/Sources/Model/User Interface/App/App.swift +++ b/Sources/Model/User Interface/App/App.swift @@ -47,7 +47,7 @@ extension App { public static func main() { let app = setupApp() app.app.run { - for element in app.scene where element as? Storage.SceneElementType != nil { + for element in app.scene where element is Storage.SceneElementType { element.setupInitialContainers(app: app.app) } } diff --git a/Sources/Model/User Interface/View/AnyView.swift b/Sources/Model/User Interface/View/AnyView.swift index 3cec9f7..12a6d66 100644 --- a/Sources/Model/User Interface/View/AnyView.swift +++ b/Sources/Model/User Interface/View/AnyView.swift @@ -72,11 +72,11 @@ extension AnyView { /// Whether the view can be rendered in a certain environment. func renderable(type: Data.Type, data: WidgetData) -> Bool where Data: ViewRenderData { let result = getModified(data: data, type: type) - return result as? Data.WidgetType != nil - || result as? SimpleView != nil - || result as? View != nil - || result as? ConvenienceWidget != nil - || result as? Body != nil + return result is Data.WidgetType + || result is SimpleView + || result is View + || result is ConvenienceWidget + || result is Body } /// Apply a modification onto a view. diff --git a/Sources/Model/User Interface/View/Widget.swift b/Sources/Model/User Interface/View/Widget.swift index 31199f1..2abbd2c 100644 --- a/Sources/Model/User Interface/View/Widget.swift +++ b/Sources/Model/User Interface/View/Widget.swift @@ -42,4 +42,176 @@ extension Widget { /// A widget's view is empty. public var viewContent: Body { [] } + /// Update the stored content. + /// - Parameters: + /// - storage: The storage to update. + /// - data: Modify views before being updated + /// - updateProperties: Whether to update the view's properties. + /// - type: The view render data type. + /// + /// This is the default implementation which requires the usage of ``Property``. + public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) { + self.updateProperties(storage, updateProperties: updateProperties) + if updateProperties { + storage.previousState = self + } + } + + /// Update the properties wrapped with ``Property``. + /// - Parameters: + /// - storage: The storage to update. + /// - updateProperties: Whether to update the view's properties. + public func updateProperties(_ storage: ViewStorage, updateProperties: Bool) { + let mirror = Mirror(reflecting: self) + updateNotEquatable(mirror: mirror, storage: storage) + guard updateProperties else { + return + } + updateAlwaysWhenStateUpdate(mirror: mirror, storage: storage) + updateEquatable(mirror: mirror, storage: storage) + } + + /// Update the properties which are not equatable and should always be updated (e.g. closures). + /// - Parameters: + /// - mirror: A mirror of the widget. + /// - storage: The view storage. + func updateNotEquatable(mirror: Mirror, storage: ViewStorage) { + 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) + } + } + } + } + + /// Update the properties which should always be updated when a state property changed + /// (e.g. "regular" properties which are not equatable). + /// - Parameters: + /// - mirror: A mirror of the widget. + /// - storage: The view storage. + /// + /// Initialize the ``Property`` property wrapper with the ``UpdateStrategy/alwaysWhenStateUpdate``. + func updateAlwaysWhenStateUpdate(mirror: Mirror, storage: ViewStorage) { + for property in mirror.children { + if let value = property.value as? any PropertyProtocol { + if value.updateStrategy == .alwaysWhenStateUpdate { + setProperty(property: value, storage: storage) + } + } + } + } + + /// Update equatable properties (most properties). + /// - 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 { + Mirror(reflecting: previousState).children + } else { + nil + } + for property in mirror.children { + if let value = property.value as? any PropertyProtocol, + value.updateStrategy == .automatic, + let wrappedValue = value.wrappedValue as? any Equatable { + var update = true + if let previous = previousState?.first(where: { previousProperty in + previousProperty.label == property.label + })?.value as? any PropertyProtocol, + equal(previous, wrappedValue) { + update = false + } + if update { + setProperty(property: value, storage: storage) + } + } + } + } + + /// Check whether a property is equal to a value. + /// - Parameters: + /// - property: The property. + /// - value: The value. + /// - Returns: Whether the property and value are equal. + func equal( + _ property: Property, + _ value: Value + ) -> Bool where Property: PropertyProtocol, Value: Equatable { + if let propertyValue = property.wrappedValue as? Value { + return propertyValue == value + } + return false + } + + /// Apply a property to the framework. + /// - Parameters: + /// - property: The property. + /// - storage: The view storage. + func setProperty(property: Property, storage: ViewStorage) where Property: PropertyProtocol { + property.setProperty(storage, property.wrappedValue) + } + +} + +/// Assign an updating closure to a widget's property. +/// +/// 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 { + + /// The function applying the property to the UI. + public var setProperty: (ViewStorage, Value) -> Void + /// The wrapped value. + public var wrappedValue: Value + /// The update strategy. + public var updateStrategy: UpdateStrategy + + /// Initialize a property. + /// - Parameters: + /// - wrappedValue: The wrapped value. + /// - setProperty: The function applying the property to the UI. + /// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases. + public init( + wrappedValue: Value, + set setProperty: @escaping (ViewStorage, Value) -> Void, + updateStrategy: UpdateStrategy = .automatic + ) { + self.setProperty = setProperty + self.wrappedValue = wrappedValue + self.updateStrategy = updateStrategy + } + +} + +/// The property protocol. +protocol PropertyProtocol { + + /// The type of the wrapped value. + associatedtype Value + + /// The wrapped value. + var wrappedValue: Value { get } + /// Set the property. + var setProperty: (ViewStorage, Value) -> Void { get } + /// The update strategy. + var updateStrategy: UpdateStrategy { get } + +} + +/// The update strategy for properties. +public enum UpdateStrategy { + + /// If equatable, update only when the value changed. + /// If not equatable, this is equivalent to ``UpdateStrategy/always``. + case automatic + /// Update always when an update is triggered. + case always + /// Update always when a state value in a parent view changed, + /// regardless of the property's value. + case alwaysWhenStateUpdate + } diff --git a/Tests/SampleBackends/Backend1.swift b/Tests/SampleBackends/Backend1.swift index 26511a9..4c9d183 100644 --- a/Tests/SampleBackends/Backend1.swift +++ b/Tests/SampleBackends/Backend1.swift @@ -40,14 +40,15 @@ public enum Backend1 { public struct Button: BackendWidget { - var label: String - var action: () -> Void + @Property(set: { _, label in print("Update button (label = \(label))") }) + var label = "" + @Property(set: { storage, closure in storage.fields["action"] = closure }) + var action: () -> Void = { } public init(_ label: String, action: @escaping () -> Void) { self.label = label self.action = action } - public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { print("Init button") let storage = ViewStorage(nil) @@ -56,20 +57,10 @@ public enum Backend1 { (storage.fields["action"] as? () -> Void)?() } storage.fields["action"] = action + storage.previousState = self return storage } - public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) { - storage.fields["action"] = action - if updateProperties { - if (storage.previousState as? Self)?.label != label { - print("Update button (label = \(label))") - } - } else { - print("Do not update button (label = \(label))") - } - } - } public struct Window: BackendSceneElement {