// // 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 where Value: Sendable { /// The function applying the property to the UI. public var setProperty: @Sendable (Pointer, Value, ViewStorage) async -> 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: @Sendable @escaping (Pointer, Value, ViewStorage) async -> 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: @Sendable @escaping (Pointer, Value) async -> Void, pointer: Pointer.Type, updateStrategy: UpdateStrategy = .automatic ) { self.init( wrappedValue: wrappedValue, set: { pointer, value, _ in await 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: @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 { await 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: @Sendable @escaping (Pointer, Value.Wrapped) async -> Void, pointer: Pointer.Type, updateStrategy: UpdateStrategy = .automatic ) { self.init( set: { pointer, value, _ in await setProperty(pointer, value) }, pointer: pointer, updateStrategy: updateStrategy ) } } /// The property protocol. protocol PropertyProtocol: Sendable { /// 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: @Sendable (Pointer, Value, ViewStorage) async -> Void { get } /// The update strategy. var updateStrategy: UpdateStrategy { get } } /// The update strategy for properties. public enum UpdateStrategy: Sendable { /// 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 { /// The view storage. /// - Parameters: /// - data: Modify views before being updated. /// - type: The view render data type. /// - Returns: The view storage. public func container( data: WidgetData, type: Data.Type ) async -> ViewStorage where Data: ViewRenderData { let storage = ViewStorage(initializeWidget()) await initProperties(storage, data: data, type: type) await update(storage, data: data, updateProperties: true, type: type) return storage } /// 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 ) async where Data: ViewRenderData { await self.updateProperties(storage, data: data, updateProperties: updateProperties, type: type) if updateProperties { await storage.setPreviousState(self) } } /// Initialize the properties wrapped with ``Property``. /// - Parameters: /// - storage: The view storage. /// - data: Modify views before being updated. /// - type: The view render data type. public func initProperties( _ storage: ViewStorage, data: WidgetData, type: Data.Type ) async where Data: ViewRenderData { let mirror = Mirror(reflecting: self) for property in mirror.children { if let value = property.value as? any ViewPropertyProtocol { 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 { await initBindingProperty(value, parent: storage) } } } /// Initialize the properties wrapped with ``ViewProperty``. /// - Parameters: /// - value: The property. /// - view: The subview's view storage. /// - parent: The parent's view storage. func initViewProperty( _ value: Property, view: ViewStorage, parent: ViewStorage ) 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) } } /// Initialize a binding property. /// - Parameters: /// - value: The property. /// - parent: The view storage. 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 } set: { newValue in if let compareValue = newValue as? any Equatable, !equal(value.wrappedValue.wrappedValue, compareValue) { value.wrappedValue.wrappedValue = newValue } }, parent ) } } /// Update the properties wrapped with ``Property``. /// - Parameters: /// - storage: The storage to update. /// - data: The widget data. /// - updateProperties: Whether to update the view's properties. /// - type: The view render data type. public func updateProperties( _ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type ) async where Data: ViewRenderData { let mirror = Mirror(reflecting: self) await updateNotEquatable( mirror: mirror, storage: storage, data: data, updateProperties: updateProperties, type: type ) guard updateProperties else { return } 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). /// - Parameters: /// - mirror: A mirror of the widget. /// - storage: The view storage. /// - data: The widget data. /// - updateProperties: Whether to update the properties. /// - type: The view render data type. func updateNotEquatable( mirror: Mirror, storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type ) 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 { await setProperty(property: value, storage: storage) } } if let value = property.value as? any ViewPropertyProtocol, 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 { await setBindingProperty(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) async { for property in mirror.children { if let value = property.value as? any PropertyProtocol { if value.updateStrategy == .alwaysWhenStateUpdate { await 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) async { let previousState: Mirror.Children? = if let previousState = await 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 { await 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 { equal(property.wrappedValue, value) } /// Check whether a value is equal to another value. /// - Parameters: /// - value1: The first value. /// - value2: The second value. /// - Returns: Whether the values are equal. func equal( _ value1: Value1, _ value2: Value2 ) -> Bool where Value2: Equatable { if let value1 = value1 as? Value2 { return value1 == value2 } return false } /// Apply a property to the framework. /// - Parameters: /// - property: The property. /// - storage: The view storage. 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 = await storage.pointer as? Property.Pointer { await property.setProperty(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 } }