Add support for optional properties

This commit is contained in:
david-swift 2024-09-15 08:44:24 +02:00
parent 4aee6ea5cf
commit 5fd45f572a
4 changed files with 272 additions and 175 deletions

View File

@ -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:

View File

@ -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<Value, Pointer>: 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<Data>(_ 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, Value>(
_ 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: 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
}
}

View File

@ -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<Data>(_ 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, Value>(
_ 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: 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<Value>: 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
}

View File

@ -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) {