diff --git a/.swiftlint.yml b/.swiftlint.yml index 45a37a6..9987c66 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -141,7 +141,6 @@ file_header: missing_docs: warning: [internal, private] error: [open, public] - excludes_extensions: false excludes_inherited_types: false type_contents_order: order: diff --git a/Sources/Model/User Interface/View/Property.swift b/Sources/Model/User Interface/View/Property.swift new file mode 100644 index 0000000..c1ff1b9 --- /dev/null +++ b/Sources/Model/User Interface/View/Property.swift @@ -0,0 +1,270 @@ +// +// Property.swift +// Meta +// +// Created by david-swift on 12.09.24. +// + +/// 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: (Pointer?, Value, ViewStorage) -> 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. + /// - pointer: The type of the pointer. + /// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases. + public init( + wrappedValue: Value, + set setProperty: @escaping (Pointer?, Value, ViewStorage) -> Void, + pointer: Pointer.Type, + updateStrategy: UpdateStrategy = .automatic + ) { + self.setProperty = setProperty + self.wrappedValue = wrappedValue + self.updateStrategy = updateStrategy + } + + /// Initialize a property. + /// - Parameters: + /// - wrappedValue: The wrapped value. + /// - setProperty: The function applying the property to the UI. + /// - pointer: The type of the pointer. + /// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases. + public init( + wrappedValue: Value, + set setProperty: @escaping (Pointer?, Value) -> Void, + pointer: Pointer.Type, + updateStrategy: UpdateStrategy = .automatic + ) { + self.init( + wrappedValue: wrappedValue, + set: { pointer, value, _ in setProperty(pointer, value) }, + pointer: pointer, + updateStrategy: updateStrategy + ) + } + +} + +extension Property where Value: OptionalProtocol { + + /// Initialize a property. + /// - Parameters: + /// - setProperty: The function applying the property to the UI. + /// - 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, + pointer: Pointer.Type, + updateStrategy: UpdateStrategy = .automatic + ) { + self.setProperty = { pointer, value, storage in + if let value = value.optionalValue { + setProperty(pointer, value, storage) + } + } + wrappedValue = nil + self.updateStrategy = updateStrategy + } + + /// Initialize a property. + /// - Parameters: + /// - wrappedValue: The wrapped value. + /// - setProperty: The function applying the property to the UI. + /// - 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, + pointer: Pointer.Type, + updateStrategy: UpdateStrategy = .automatic + ) { + self.init( + set: { pointer, value, _ in setProperty(pointer, value) }, + pointer: pointer, + updateStrategy: updateStrategy + ) + } + +} + +/// The property protocol. +protocol PropertyProtocol { + + /// The type of the wrapped value. + associatedtype Value + /// The type of the view's pointer. + associatedtype Pointer + + /// The wrapped value. + var wrappedValue: Value { get } + /// Set the property. + var setProperty: (Pointer?, Value, ViewStorage) -> 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 + +} + +extension Widget { + + /// 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 { + if let optional = property.wrappedValue as? any OptionalProtocol, optional.optionalValue == nil { + return + } + property.setProperty(storage.pointer as? Property.Pointer, property.wrappedValue, storage) + } + +} + +/// A protocol for values which can be optional. +public protocol OptionalProtocol: ExpressibleByNilLiteral { + + /// The type of the wrapped value. + associatedtype Wrapped + + /// The value. + var optionalValue: Wrapped? { get } + +} + +extension Optional: OptionalProtocol { + + /// The optional value. + public var optionalValue: Wrapped? { + self + } + +} diff --git a/Sources/Model/User Interface/View/Widget.swift b/Sources/Model/User Interface/View/Widget.swift index 2abbd2c..31199f1 100644 --- a/Sources/Model/User Interface/View/Widget.swift +++ b/Sources/Model/User Interface/View/Widget.swift @@ -42,176 +42,4 @@ 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 4c9d183..a95d397 100644 --- a/Tests/SampleBackends/Backend1.swift +++ b/Tests/SampleBackends/Backend1.swift @@ -40,9 +40,9 @@ public enum Backend1 { public struct Button: BackendWidget { - @Property(set: { _, label in print("Update button (label = \(label))") }) + @Property(set: { print("Update button (label = \($1))") }, pointer: Any.self) var label = "" - @Property(set: { storage, closure in storage.fields["action"] = closure }) + @Property(set: { $2.fields["action"] = $1 }, pointer: Any.self) var action: () -> Void = { } public init(_ label: String, action: @escaping () -> Void) {