portico/Sources/Portico/State/Bindable.swift

67 lines
2.6 KiB
Swift

import Observation
/// A property wrapper that vends two-way ``Binding``s to the properties of an
/// `@Observable` reference model.
///
/// `@Environment(Model.self)` hands a view a read-only model reference, and
/// ``Binding/init(_:_:)`` already turns one of that model's properties into a
/// two-way binding. `@Bindable` is the ergonomic front end for the pairing:
/// `$model.title` resolves, through `@dynamicMemberLookup`, to exactly
/// `Binding(model, \.title)`.
///
/// ```swift
/// struct Editor: View {
/// @Bindable var model: Model
///
/// var body: some View {
/// VStack {
/// SwitchRow().active($model.isEnabled)
/// Label(str: "").label($model.title)
/// }
/// }
/// }
/// ```
///
/// The wrapper stores the model reference and nothing else: no state, no
/// subscription, and no ``DynamicProperty`` conformance, so the mount-time
/// `_resolveDynamicProperties` walk skips it. All reactivity belongs to the
/// ``Binding`` a subscript vends, which routes through ``DependencyTracker``
/// and ``ObservationBridge`` exactly like a hand-written observable binding -
/// widget edits reach the model synchronously, model mutations reach widgets on
/// the coalesced idle flush.
@propertyWrapper @dynamicMemberLookup @MainActor
public struct Bindable<Value: AnyObject & Observation.Observable> {
/// The wrapped model.
///
/// Read-only: `@Bindable` never re-points at a different object, it only
/// vends bindings into the one it was given.
public let wrappedValue: Value
/// Wraps `wrappedValue`, backing `@Bindable var model: Model` declarations.
///
/// - Parameter wrappedValue: The observable model. Held strongly.
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
/// Wraps `object` for inline use, as in `Bindable(model).title`.
///
/// - Parameter object: The observable model. Held strongly.
public init(_ object: Value) {
self.wrappedValue = object
}
/// Self, so that `$model.property` reaches ``subscript(dynamicMember:)``.
public var projectedValue: Bindable<Value> { self }
/// A two-way ``Binding`` to the model property named by `keyPath`.
///
/// - Parameter keyPath: A writable key path into the wrapped model.
/// - Returns: A binding whose reads track and whose writes land directly on
/// the model, propagating to every other reader via Observation.
public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<Value, Subject>
) -> Binding<Subject> {
Binding(wrappedValue, keyPath)
}
}