From 583e47c3180cb2590dcd493b3b4f45148ce31479 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:23:23 +0200 Subject: [PATCH 01/11] Feat: Add the SwiftUI DynamicProperty contract with nested hydration - Add the public DynamicProperty protocol with a default update() and conform State, Binding, Environment, and AppStorage - Hydrate user-defined dynamic properties recursively: nested framework wrappers bind depth-first with stable declaration-ordered property indices, and update() runs once per hydration before body evaluation - Cover conformances, nested-state persistence, and the update cycle with tests --- Sources/TUIkit/State/AppStorage.swift | 4 + .../Environment/EnvironmentProperty.swift | 4 + .../TUIkitView/State/DynamicProperty.swift | 46 +++++++ Sources/TUIkitView/State/State.swift | 48 ++++++- Tests/TUIkitTests/DynamicPropertyTests.swift | 117 ++++++++++++++++++ 5 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 Sources/TUIkitView/State/DynamicProperty.swift create mode 100644 Tests/TUIkitTests/DynamicPropertyTests.swift diff --git a/Sources/TUIkit/State/AppStorage.swift b/Sources/TUIkit/State/AppStorage.swift index fc37e4a1..f023247b 100644 --- a/Sources/TUIkit/State/AppStorage.swift +++ b/Sources/TUIkit/State/AppStorage.swift @@ -388,6 +388,10 @@ public struct AppStorage: @unchecked Sendable { } } +// MARK: - Dynamic Property Conformance + +extension AppStorage: DynamicProperty {} + // MARK: - App Storage Box /// Reference storage that binds AppStorage to its first rendering runtime. diff --git a/Sources/TUIkitView/Environment/EnvironmentProperty.swift b/Sources/TUIkitView/Environment/EnvironmentProperty.swift index 23c55c95..e23e0df7 100644 --- a/Sources/TUIkitView/Environment/EnvironmentProperty.swift +++ b/Sources/TUIkitView/Environment/EnvironmentProperty.swift @@ -112,3 +112,7 @@ public struct Environment { } } } + +// MARK: - Dynamic Property Conformance + +extension Environment: DynamicProperty {} diff --git a/Sources/TUIkitView/State/DynamicProperty.swift b/Sources/TUIkitView/State/DynamicProperty.swift new file mode 100644 index 00000000..3025a9d3 --- /dev/null +++ b/Sources/TUIkitView/State/DynamicProperty.swift @@ -0,0 +1,46 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// DynamicProperty.swift +// +// Created by LAYERED.work +// License: MIT + +// MARK: - DynamicProperty + +/// An interface for a stored variable that updates an external property of a +/// view. +/// +/// Matches SwiftUI's contract: the renderer binds every dynamic property of a +/// view (including properties nested inside user-defined `DynamicProperty` +/// types) to the view's committed structural identity, then calls +/// ``update()`` before evaluating `body`. +/// +/// ## Composing framework wrappers +/// +/// User-defined dynamic properties can embed framework wrappers; the nested +/// wrappers hydrate exactly like directly declared ones: +/// +/// ```swift +/// struct Counter: DynamicProperty { +/// @State private var count = 0 +/// +/// var value: Int { count } +/// func increment() { count += 1 } +/// } +/// ``` +/// +/// > Note: `update()` is invoked on a copy of the property value obtained by +/// > reflection. Framework wrappers use reference-backed storage, so their +/// > effects persist; custom implementations should route side effects +/// > through reference-typed storage as well. +public protocol DynamicProperty { + /// Updates the underlying value of the stored property. + /// + /// The renderer calls this function before evaluating the owning view's + /// `body`, once per hydration. + mutating func update() +} + +extension DynamicProperty { + /// Default: dynamic properties without external resources do nothing. + public mutating func update() {} +} diff --git a/Sources/TUIkitView/State/State.swift b/Sources/TUIkitView/State/State.swift index 576be1bb..0cc85267 100644 --- a/Sources/TUIkitView/State/State.swift +++ b/Sources/TUIkitView/State/State.swift @@ -314,24 +314,58 @@ public enum StateRegistration { withHydration(owner: owner, context: context, block) } - /// Binds direct dynamic-property fields for tests and specialized runtime paths. + /// Binds dynamic-property fields for tests and specialized runtime paths. + /// + /// Walks the owner's stored properties depth-first: framework wrappers + /// bind directly, and user-defined ``DynamicProperty`` values are + /// descended into so their nested wrappers hydrate with stable, + /// declaration-ordered property indices. After a dynamic property's + /// subtree is bound, its `update()` runs once. package static func bindDynamicProperties( in owner: Owner, context: HydrationContext ) { var propertyIndex = 0 + bindDynamicProperties(in: owner, context: context, propertyIndex: &propertyIndex) + } + + private static func bindDynamicProperties( + in owner: Any, + context: HydrationContext, + propertyIndex: inout Int + ) { var mirror: Mirror? = Mirror(reflecting: owner) while let currentMirror = mirror { for child in currentMirror.children { - guard let property = child.value as? any RuntimeDynamicProperty else { continue } - property.bind(to: context, propertyIndex: propertyIndex) - propertyIndex += 1 + if let property = child.value as? any RuntimeDynamicProperty { + property.bind(to: context, propertyIndex: propertyIndex) + propertyIndex += 1 + } else if child.value is any DynamicProperty { + bindDynamicProperties( + in: child.value, + context: context, + propertyIndex: &propertyIndex + ) + } + + if let dynamicProperty = child.value as? any DynamicProperty { + runUpdate(on: dynamicProperty) + } } mirror = currentMirror.superclassMirror } } + /// Runs `update()` on a copy of the property. + /// + /// Reflection cannot write back into a value-typed owner; framework + /// wrappers persist their effects through reference-backed storage. + private static func runUpdate(on property: P) { + var copy = property + copy.update() + } + private static func withHydration( owner: Any?, context: RenderContext, @@ -493,6 +527,12 @@ public struct State { } } +// MARK: - Dynamic Property Conformance + +extension State: DynamicProperty {} + +extension Binding: DynamicProperty {} + // MARK: - Runtime Binding extension State: RuntimeDynamicProperty { diff --git a/Tests/TUIkitTests/DynamicPropertyTests.swift b/Tests/TUIkitTests/DynamicPropertyTests.swift new file mode 100644 index 00000000..56b9caae --- /dev/null +++ b/Tests/TUIkitTests/DynamicPropertyTests.swift @@ -0,0 +1,117 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// DynamicPropertyTests.swift +// +// Created by LAYERED.work +// License: MIT + +import Foundation +import Testing + +@testable import TUIkit + +@MainActor +@Suite("DynamicProperty Contract") +struct DynamicPropertyTests { + + /// Creates an isolated render context. + private func testContext( + tuiContext: TUIContext, + identity: String = "Root" + ) -> RenderContext { + RenderContext( + availableWidth: 40, + availableHeight: 10, + tuiContext: tuiContext, + identity: ViewIdentity(path: identity) + ) + } + + // MARK: - Conformances + + @Test("The built-in property wrappers are DynamicProperty types") + func builtInWrappersAreDynamicProperties() { + #expect(State.self is any DynamicProperty.Type) + #expect(Binding.self is any DynamicProperty.Type) + #expect(Environment.self is any DynamicProperty.Type) + #expect(AppStorage.self is any DynamicProperty.Type) + } + + // MARK: - Nested Hydration + + /// A user-defined dynamic property composing framework state. + struct Counter: DynamicProperty { + @State var count = 0 + } + + struct CounterView: View { + let counter = Counter() + + var body: some View { + Text("count:\(counter.count)") + } + } + + @Test("State nested in a custom DynamicProperty persists across re-renders") + func nestedStatePersists() { + let tuiContext = TUIContext() + let context = testContext(tuiContext: tuiContext, identity: "CounterView") + + let first = CounterView() + _ = StateRegistration.withHydration(of: first, context: context) { + first.counter.count += 1 + return first.body + } + + let second = CounterView() + let body = StateRegistration.withHydration(of: second, context: context) { + second.body + } + let buffer = renderToBuffer(body, context: context) + + #expect(buffer.lines.first?.stripped == "count:1") + } + + // MARK: - Update Cycle + + /// Records every update() invocation. + final class UpdateRecorder { + var updates = 0 + } + + /// A dynamic property observing its own update cycle. + struct Updating: DynamicProperty { + let recorder: UpdateRecorder + + func update() { + recorder.updates += 1 + } + } + + struct UpdatingView: View { + let property: Updating + + var body: some View { + Text("updated") + } + } + + @Test("update() runs once per hydration before body evaluation") + func updateRunsOncePerHydration() { + let tuiContext = TUIContext() + let context = testContext(tuiContext: tuiContext, identity: "UpdatingView") + let recorder = UpdateRecorder() + let view = UpdatingView(property: Updating(recorder: recorder)) + + _ = StateRegistration.withHydration(of: view, context: context) { + view.body + } + + #expect(recorder.updates == 1) + + _ = StateRegistration.withHydration(of: view, context: context) { + view.body + } + + #expect(recorder.updates == 2) + } +} From 1d4e34aa2873f9a9772616e64858bcf57590c0d1 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:26:07 +0200 Subject: [PATCH 02/11] Feat: Align Binding with SwiftUI's projection and conformance surface - Add @dynamicMemberLookup with writable key-path child bindings - Add init(projectedValue:), optional promotion and unwrapping initializers, and the AnyHashable projection - Add conditional Identifiable, Sequence, Collection, Bidirectional-, and RandomAccessCollection conformances with writable element bindings - Mark Binding @unchecked Sendable when its value is Sendable - Cover writeback semantics for member, optional, and element bindings --- .../TUIkitView/State/Binding+SwiftUI.swift | 154 ++++++++++++++++++ Sources/TUIkitView/State/State.swift | 1 + Tests/TUIkitTests/BindingAlignmentTests.swift | 117 +++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 Sources/TUIkitView/State/Binding+SwiftUI.swift create mode 100644 Tests/TUIkitTests/BindingAlignmentTests.swift diff --git a/Sources/TUIkitView/State/Binding+SwiftUI.swift b/Sources/TUIkitView/State/Binding+SwiftUI.swift new file mode 100644 index 00000000..d1f026fb --- /dev/null +++ b/Sources/TUIkitView/State/Binding+SwiftUI.swift @@ -0,0 +1,154 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// Binding+SwiftUI.swift +// +// Created by LAYERED.work +// License: MIT + +// MARK: - Dynamic Member Lookup + +extension Binding { + /// Returns a binding to the resulting value of a given key path. + /// + /// Writing through the returned binding writes back into the original + /// bound value, so nested properties stay two-way connected: + /// + /// ```swift + /// @State private var profile = Profile(name: "…") + /// TextField("Name", text: $profile.name) + /// ``` + /// + /// - Parameter keyPath: A key path to a specific value. + /// - Returns: A new binding focused on the member at the key path. + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> Binding { + Binding( + get: { self.wrappedValue[keyPath: keyPath] }, + set: { self.wrappedValue[keyPath: keyPath] = $0 } + ) + } +} + +// MARK: - Initializers + +extension Binding { + /// Creates a binding from the value of another binding. + /// + /// Matches SwiftUI's property-wrapper rewrapping initializer used by the + /// compiler for `@Binding var value` declarations initialized from a + /// projected value. + /// + /// - Parameter projectedValue: A binding to rewrap. + public init(projectedValue: Binding) { + self = projectedValue + } + + /// Creates a binding by projecting the base value to an optional value. + /// + /// - Parameter base: A binding to a non-optional source of truth. + public init(_ base: Binding) where Value == V? { + self.init( + get: { base.wrappedValue }, + set: { newValue in + if let newValue { + base.wrappedValue = newValue + } + } + ) + } + + /// Creates a binding by projecting the base optional value to its + /// unwrapped value, or fails when the base value is `nil`. + /// + /// The returned binding reads the base at access time; if the base + /// becomes `nil` later, reads keep returning the last known value while + /// writes still target the base. + /// + /// - Parameter base: A binding to an optional source of truth. + public init?(_ base: Binding) { + guard let initialValue = base.wrappedValue else { return nil } + self.init( + get: { base.wrappedValue ?? initialValue }, + set: { base.wrappedValue = $0 } + ) + } + + /// Creates a binding by projecting the base value to a hashable value. + /// + /// - Parameter base: A binding to a hashable source of truth. + public init(_ base: Binding) where Value == AnyHashable, V: Hashable { + self.init( + get: { AnyHashable(base.wrappedValue) }, + set: { newValue in + if let typed = newValue.base as? V { + base.wrappedValue = typed + } + } + ) + } +} + +// MARK: - Identifiable Conformance + +extension Binding: Identifiable where Value: Identifiable { + /// The stable identity of the bound value. + public var id: Value.ID { + wrappedValue.id + } +} + +// MARK: - Collection Conformances + +extension Binding: Sequence where Value: MutableCollection { + public typealias Element = Binding + public typealias Iterator = IndexingIterator> +} + +extension Binding: Collection where Value: MutableCollection { + public typealias Index = Value.Index + public typealias Indices = Value.Indices + + public var startIndex: Binding.Index { + wrappedValue.startIndex + } + + public var endIndex: Binding.Index { + wrappedValue.endIndex + } + + public var indices: Value.Indices { + wrappedValue.indices + } + + public func index(after position: Binding.Index) -> Binding.Index { + wrappedValue.index(after: position) + } + + public func formIndex(after position: inout Binding.Index) { + wrappedValue.formIndex(after: &position) + } + + /// Returns a writable binding to the element at the given position. + public subscript(position: Binding.Index) -> Binding.Element { + Binding( + get: { self.wrappedValue[position] }, + set: { self.wrappedValue[position] = $0 } + ) + } +} + +extension Binding: BidirectionalCollection where Value: BidirectionalCollection, Value: MutableCollection { + public func index(before position: Binding.Index) -> Binding.Index { + wrappedValue.index(before: position) + } + + public func formIndex(before position: inout Binding.Index) { + wrappedValue.formIndex(before: &position) + } +} + +extension Binding: RandomAccessCollection where Value: RandomAccessCollection, Value: MutableCollection {} + +// MARK: - Sendable Boundary + +extension Binding: @unchecked Sendable where Value: Sendable {} diff --git a/Sources/TUIkitView/State/State.swift b/Sources/TUIkitView/State/State.swift index 0cc85267..0ac27092 100644 --- a/Sources/TUIkitView/State/State.swift +++ b/Sources/TUIkitView/State/State.swift @@ -409,6 +409,7 @@ public enum StateRegistration { /// } /// ``` @propertyWrapper +@dynamicMemberLookup public struct Binding { /// The getter for the value. private let getValue: () -> Value diff --git a/Tests/TUIkitTests/BindingAlignmentTests.swift b/Tests/TUIkitTests/BindingAlignmentTests.swift new file mode 100644 index 00000000..a945808f --- /dev/null +++ b/Tests/TUIkitTests/BindingAlignmentTests.swift @@ -0,0 +1,117 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// BindingAlignmentTests.swift +// +// Created by LAYERED.work +// License: MIT + +import Testing + +@testable import TUIkit + +@MainActor +@Suite("Binding SwiftUI Alignment") +struct BindingAlignmentTests { + + /// A mutable box providing binding storage for tests. + final class Storage { + var value: Value + + init(_ value: Value) { + self.value = value + } + + var binding: Binding { + Binding(get: { self.value }, set: { self.value = $0 }) + } + } + + struct Profile: Equatable { + var name: String + var age: Int + } + + // MARK: - Dynamic Member Lookup + + @Test("Dynamic member lookup produces writable child bindings") + func dynamicMemberLookupWritesBack() { + let storage = Storage(Profile(name: "initial", age: 30)) + let profile = storage.binding + + let name: Binding = profile.name + + #expect(name.wrappedValue == "initial") + + name.wrappedValue = "changed" + + #expect(storage.value.name == "changed") + #expect(storage.value.age == 30) + } + + // MARK: - Initializers + + @Test("init(projectedValue:) rewraps a binding") + func initProjectedValue() { + let storage = Storage(7) + let binding = Binding(projectedValue: storage.binding) + + binding.wrappedValue = 9 + + #expect(storage.value == 9) + } + + @Test("A non-optional binding promotes to an optional binding") + func optionalPromotion() { + let storage = Storage("value") + let optional: Binding = Binding(storage.binding) + + #expect(optional.wrappedValue == "value") + + optional.wrappedValue = "updated" + + #expect(storage.value == "updated") + } + + @Test("Unwrapping an optional binding fails for nil and writes back otherwise") + func optionalUnwrapping() { + let empty = Storage(nil) + #expect(Binding(empty.binding) == nil) + + let filled = Storage("present") + let unwrapped = Binding(filled.binding) + #expect(unwrapped != nil) + #expect(unwrapped?.wrappedValue == "present") + + unwrapped?.wrappedValue = "rewritten" + #expect(filled.value == "rewritten") + } + + // MARK: - Collection Conformance + + @Test("A collection binding yields writable element bindings") + func collectionBindingElements() { + let storage = Storage([1, 2, 3]) + let bindings = storage.binding + + #expect(bindings.count == 3) + #expect(bindings[1].wrappedValue == 2) + + for element in bindings where element.wrappedValue == 3 { + element.wrappedValue = 30 + } + + #expect(storage.value == [1, 2, 30]) + } + + // MARK: - Identifiable + + struct Item: Identifiable { + let id: String + } + + @Test("A binding to an Identifiable value shares its id") + func identifiableBinding() { + let storage = Storage(Item(id: "item-1")) + + #expect(storage.binding.id == "item-1") + } +} From c2ada4f7afa054222880b12a913515d18b3d9721 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:28:23 +0200 Subject: [PATCH 03/11] Feat: Complete the State initializer surface - Add SwiftUI's init(initialValue:) spelling and the empty initializer for nil-expressible state values --- Sources/TUIkitView/State/State.swift | 18 +++++++++++++ Tests/TUIkitTests/StateAlignmentTests.swift | 28 +++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 Tests/TUIkitTests/StateAlignmentTests.swift diff --git a/Sources/TUIkitView/State/State.swift b/Sources/TUIkitView/State/State.swift index 0ac27092..bb5a9b0f 100644 --- a/Sources/TUIkitView/State/State.swift +++ b/Sources/TUIkitView/State/State.swift @@ -526,6 +526,24 @@ public struct State { self.defaultValue = wrappedValue self.location = StateLocation(defaultValue: wrappedValue) } + + /// Creates a state with an initial value. + /// + /// SwiftUI-compatible spelling of ``init(wrappedValue:)``. + /// + /// - Parameter value: The initial/default value. + public init(initialValue value: Value) { + self.init(wrappedValue: value) + } +} + +extension State where Value: ExpressibleByNilLiteral { + /// Creates state without an initial value, starting at `nil`. + /// + /// Matches SwiftUI's empty initializer for optional state values. + public init() { + self.init(wrappedValue: nil) + } } // MARK: - Dynamic Property Conformance diff --git a/Tests/TUIkitTests/StateAlignmentTests.swift b/Tests/TUIkitTests/StateAlignmentTests.swift new file mode 100644 index 00000000..f8fd297e --- /dev/null +++ b/Tests/TUIkitTests/StateAlignmentTests.swift @@ -0,0 +1,28 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// StateAlignmentTests.swift +// +// Created by LAYERED.work +// License: MIT + +import Testing + +@testable import TUIkit + +@MainActor +@Suite("State SwiftUI Alignment") +struct StateAlignmentTests { + + @Test("init(initialValue:) matches init(wrappedValue:)") + func initialValueInitializer() { + let state = State(initialValue: 42) + + #expect(state.wrappedValue == 42) + } + + @Test("Optional state starts as nil with the empty initializer") + func optionalEmptyInitializer() { + let state = State() + + #expect(state.wrappedValue == nil) + } +} From 82705313f66107827c7bd15558e2bea346808bb2 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:33:18 +0200 Subject: [PATCH 04/11] Feat: Align AppStorage with SwiftUI's typed initializer families - Drop the struct-level Codable constraint; persistence runs through family-specific read/write steps on the storage box - Add SwiftUI's typed families (Bool, Int, Double, String, URL, Data, Date, RawRepresentable with String or Int raw values) plus their optional variants, where nil assignment removes the stored value - Rename the backend parameter to SwiftUI's store label; it accepts a StorageBackend as the documented terminal adaptation of UserDefaults - Keep arbitrary Codable values as a documented additive convenience --- Sources/TUIkit/State/AppStorage.swift | 309 ++++++++++++++++-- Tests/TUIkitTests/AppStorageFamilyTests.swift | 128 ++++++++ 2 files changed, 401 insertions(+), 36 deletions(-) create mode 100644 Tests/TUIkitTests/AppStorageFamilyTests.swift diff --git a/Sources/TUIkit/State/AppStorage.swift b/Sources/TUIkit/State/AppStorage.swift index f023247b..280254aa 100644 --- a/Sources/TUIkit/State/AppStorage.swift +++ b/Sources/TUIkit/State/AppStorage.swift @@ -297,7 +297,7 @@ private extension JSONFileStorage { /// `TUIContext`) or passed explicitly to the property wrapper: /// /// ```swift -/// @AppStorage("token", storage: MyCustomBackend()) var token = "" +/// @AppStorage("token", store: MyCustomBackend()) var token = "" /// ``` /// /// Code that touches an `@AppStorage` property before any runtime hydrates it @@ -335,41 +335,22 @@ enum UnboundAppStorage { /// /// # Supported Types /// -/// Any type that conforms to `Codable`: -/// - String, Int, Double, Bool -/// - Date, Data, URL -/// - Arrays and Dictionaries of Codable types -/// - Custom Codable structs and enums +/// SwiftUI's initializer families cover `Bool`, `Int`, `Double`, `String`, +/// `URL`, `Data`, `Date`, `RawRepresentable` values with `String` or `Int` +/// raw values, and their optional variants. As an additive terminal +/// convenience, any `Codable` value is supported as well. +/// +/// The `store` parameter accepts a ``StorageBackend`` instead of SwiftUI's +/// `UserDefaults`, which has no terminal equivalent; passing `nil` uses the +/// owning runtime's injected backend. @propertyWrapper -public struct AppStorage: @unchecked Sendable { +public struct AppStorage: @unchecked Sendable { /// Reference storage that captures the first runtime owning this property. private let box: AppStorageBox - /// Creates an AppStorage with the default storage backend. - /// - /// - Parameters: - /// - wrappedValue: The default value. - /// - key: The key to use for storage. - public init(wrappedValue: Value, _ key: String) { - self.box = AppStorageBox( - key: key, - defaultValue: wrappedValue, - explicitStorage: nil - ) - } - - /// Creates an AppStorage with a custom storage backend. - /// - /// - Parameters: - /// - wrappedValue: The default value. - /// - key: The key to use for storage. - /// - storage: The storage backend to use. - public init(wrappedValue: Value, _ key: String, storage: StorageBackend) { - self.box = AppStorageBox( - key: key, - defaultValue: wrappedValue, - explicitStorage: storage - ) + /// Creates a wrapper around prepared reference storage. + fileprivate init(box: AppStorageBox) { + self.box = box } /// The current value. @@ -388,6 +369,246 @@ public struct AppStorage: @unchecked Sendable { } } +// MARK: - Codable Convenience + +extension AppStorage { + /// Creates a property that can read and write any Codable value. + /// + /// Additive terminal convenience beyond SwiftUI's typed families; the + /// typed overloads below take precedence for their exact value types. + /// + /// - Parameters: + /// - wrappedValue: The default value. + /// - key: The key to read and write the value to. + /// - store: The backend to use, or `nil` for the runtime's backend. + public init( + wrappedValue: Value, + _ key: String, + store: (any StorageBackend)? = nil + ) where Value: Codable { + self.init(box: AppStorageBox( + key: key, + defaultValue: wrappedValue, + explicitStorage: store, + read: { backend, key in backend.value(forKey: key) }, + write: { backend, key, value in backend.setValue(value, forKey: key) } + )) + } +} + +// MARK: - Standard Value Families + +extension AppStorage { + /// Shared construction for Codable-backed typed families. + fileprivate static func codableFamily( + wrappedValue: Value, + key: String, + store: (any StorageBackend)? + ) -> AppStorage where Value: Codable { + AppStorage(wrappedValue: wrappedValue, key, store: store) + } + + /// Creates a property that can read and write a Boolean value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value == Bool { + self = Self.codableFamily(wrappedValue: wrappedValue, key: key, store: store) + } + + /// Creates a property that can read and write an integer value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value == Int { + self = Self.codableFamily(wrappedValue: wrappedValue, key: key, store: store) + } + + /// Creates a property that can read and write a double value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value == Double { + self = Self.codableFamily(wrappedValue: wrappedValue, key: key, store: store) + } + + /// Creates a property that can read and write a string value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value == String { + self = Self.codableFamily(wrappedValue: wrappedValue, key: key, store: store) + } + + /// Creates a property that can read and write a URL value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value == URL { + self = Self.codableFamily(wrappedValue: wrappedValue, key: key, store: store) + } + + /// Creates a property that can read and write a data value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value == Data { + self = Self.codableFamily(wrappedValue: wrappedValue, key: key, store: store) + } + + /// Creates a property that can read and write a date value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value == Date { + self = Self.codableFamily(wrappedValue: wrappedValue, key: key, store: store) + } +} + +// MARK: - RawRepresentable Families + +extension AppStorage { + /// Shared construction for raw-representable typed families. + fileprivate static func rawFamily( + wrappedValue: Value, + key: String, + store: (any StorageBackend)?, + rawValue: @escaping @Sendable (Value) -> Raw, + fromRaw: @escaping @Sendable (Raw) -> Value? + ) -> AppStorage { + AppStorage(box: AppStorageBox( + key: key, + defaultValue: wrappedValue, + explicitStorage: store, + read: { backend, key in + let raw: Raw? = backend.value(forKey: key) + return raw.flatMap(fromRaw) + }, + write: { backend, key, value in + backend.setValue(rawValue(value), forKey: key) + } + )) + } + + /// Creates a property that can read and write a string-backed + /// RawRepresentable value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value: RawRepresentable, Value.RawValue == String { + self = Self.rawFamily( + wrappedValue: wrappedValue, + key: key, + store: store, + rawValue: { $0.rawValue }, + fromRaw: { Value(rawValue: $0) } + ) + } + + /// Creates a property that can read and write an integer-backed + /// RawRepresentable value. + public init(wrappedValue: Value, _ key: String, store: (any StorageBackend)? = nil) + where Value: RawRepresentable, Value.RawValue == Int { + self = Self.rawFamily( + wrappedValue: wrappedValue, + key: key, + store: store, + rawValue: { $0.rawValue }, + fromRaw: { Value(rawValue: $0) } + ) + } +} + +// MARK: - Optional Families + +extension AppStorage { + /// Shared construction for optional Codable-backed families. + fileprivate static func optionalFamily( + key: String, + store: (any StorageBackend)? + ) -> AppStorage where Value == Wrapped? { + AppStorage(box: AppStorageBox( + key: key, + defaultValue: nil, + explicitStorage: store, + read: { backend, key in + let stored: Wrapped? = backend.value(forKey: key) + return stored.map { $0 } + }, + write: { backend, key, value in + if let value { + backend.setValue(value, forKey: key) + } else { + backend.removeValue(forKey: key) + } + } + )) + } + + /// Creates a property that can read and write an optional Boolean value. + public init(_ key: String, store: (any StorageBackend)? = nil) where Value == Bool? { + self = Self.optionalFamily(key: key, store: store) + } + + /// Creates a property that can read and write an optional integer value. + public init(_ key: String, store: (any StorageBackend)? = nil) where Value == Int? { + self = Self.optionalFamily(key: key, store: store) + } + + /// Creates a property that can read and write an optional double value. + public init(_ key: String, store: (any StorageBackend)? = nil) where Value == Double? { + self = Self.optionalFamily(key: key, store: store) + } + + /// Creates a property that can read and write an optional string value. + public init(_ key: String, store: (any StorageBackend)? = nil) where Value == String? { + self = Self.optionalFamily(key: key, store: store) + } + + /// Creates a property that can read and write an optional URL value. + public init(_ key: String, store: (any StorageBackend)? = nil) where Value == URL? { + self = Self.optionalFamily(key: key, store: store) + } + + /// Creates a property that can read and write an optional data value. + public init(_ key: String, store: (any StorageBackend)? = nil) where Value == Data? { + self = Self.optionalFamily(key: key, store: store) + } + + /// Creates a property that can read and write an optional date value. + public init(_ key: String, store: (any StorageBackend)? = nil) where Value == Date? { + self = Self.optionalFamily(key: key, store: store) + } + + /// Creates a property that can read and write an optional string-backed + /// RawRepresentable value. + public init(_ key: String, store: (any StorageBackend)? = nil) + where Value == R?, R: RawRepresentable, R.RawValue == String { + self.init(box: AppStorageBox( + key: key, + defaultValue: nil, + explicitStorage: store, + read: { backend, key in + let raw: String? = backend.value(forKey: key) + return raw.flatMap(R.init(rawValue:)).map { $0 } + }, + write: { backend, key, value in + if let value { + backend.setValue(value.rawValue, forKey: key) + } else { + backend.removeValue(forKey: key) + } + } + )) + } + + /// Creates a property that can read and write an optional integer-backed + /// RawRepresentable value. + public init(_ key: String, store: (any StorageBackend)? = nil) + where Value == R?, R: RawRepresentable, R.RawValue == Int { + self.init(box: AppStorageBox( + key: key, + defaultValue: nil, + explicitStorage: store, + read: { backend, key in + let raw: Int? = backend.value(forKey: key) + return raw.flatMap(R.init(rawValue:)).map { $0 } + }, + write: { backend, key, value in + if let value { + backend.setValue(value.rawValue, forKey: key) + } else { + backend.removeValue(forKey: key) + } + } + )) + } +} + // MARK: - Dynamic Property Conformance extension AppStorage: DynamicProperty {} @@ -395,7 +616,13 @@ extension AppStorage: DynamicProperty {} // MARK: - App Storage Box /// Reference storage that binds AppStorage to its first rendering runtime. -private final class AppStorageBox: @unchecked Sendable { +private final class AppStorageBox: @unchecked Sendable { + /// Reads the stored value from a backend, if present. + typealias Read = @Sendable (any StorageBackend, String) -> Value? + + /// Writes (or clears) the stored value on a backend. + typealias Write = @Sendable (any StorageBackend, String, Value) -> Void + /// Persistent key. private let key: String @@ -405,6 +632,12 @@ private final class AppStorageBox: @unchecked Sendable { /// Explicit backend supplied by the property-wrapper initializer. private let explicitStorage: StorageBackend? + /// Family-specific decoding step. + private let read: Read + + /// Family-specific encoding step. + private let write: Write + /// Backend captured from the owning runtime. private var runtimeStorage: StorageBackend? @@ -421,22 +654,26 @@ private final class AppStorageBox: @unchecked Sendable { init( key: String, defaultValue: Value, - explicitStorage: StorageBackend? + explicitStorage: StorageBackend?, + read: @escaping Read, + write: @escaping Write ) { self.key = key self.defaultValue = defaultValue self.explicitStorage = explicitStorage + self.read = read + self.write = write } /// Current persisted value. var value: Value { get { let storage = resolvedDependencies().storage - return storage.value(forKey: key) ?? defaultValue + return read(storage, key) ?? defaultValue } set { let dependencies = resolvedDependencies() - dependencies.storage.setValue(newValue, forKey: key) + write(dependencies.storage, key, newValue) if let identity = dependencies.identity { dependencies.invalidationSink?.invalidate(.subtree(identity)) diff --git a/Tests/TUIkitTests/AppStorageFamilyTests.swift b/Tests/TUIkitTests/AppStorageFamilyTests.swift new file mode 100644 index 00000000..9f2c56e0 --- /dev/null +++ b/Tests/TUIkitTests/AppStorageFamilyTests.swift @@ -0,0 +1,128 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// AppStorageFamilyTests.swift +// +// Created by LAYERED.work +// License: MIT + +import Foundation +import Testing + +@testable import TUIkit + +@MainActor +@Suite("AppStorage Initializer Families") +struct AppStorageFamilyTests { + + enum StringChoice: String { + case first + case second + } + + enum IntChoice: Int { + case one = 1 + case two = 2 + } + + struct CustomPayload: Codable, Equatable { + var label: String + } + + // MARK: - Typed Families + + @Test("Standard value families roundtrip through the injected store") + func standardFamiliesRoundtrip() { + let store = VolatileStorageBackend() + + let flag = AppStorage(wrappedValue: false, "flag", store: store) + flag.wrappedValue = true + #expect(flag.wrappedValue == true) + + let count = AppStorage(wrappedValue: 0, "count", store: store) + count.wrappedValue = 7 + #expect(count.wrappedValue == 7) + + let ratio = AppStorage(wrappedValue: 0.0, "ratio", store: store) + ratio.wrappedValue = 0.5 + #expect(ratio.wrappedValue == 0.5) + + let name = AppStorage(wrappedValue: "initial", "name", store: store) + name.wrappedValue = "updated" + #expect(name.wrappedValue == "updated") + + let link = AppStorage(wrappedValue: URL(fileURLWithPath: "/tmp"), "link", store: store) + link.wrappedValue = URL(fileURLWithPath: "/var") + #expect(link.wrappedValue == URL(fileURLWithPath: "/var")) + + let blob = AppStorage(wrappedValue: Data(), "blob", store: store) + blob.wrappedValue = Data([1, 2, 3]) + #expect(blob.wrappedValue == Data([1, 2, 3])) + + let stamp = AppStorage(wrappedValue: Date(timeIntervalSince1970: 0), "stamp", store: store) + stamp.wrappedValue = Date(timeIntervalSince1970: 100) + #expect(stamp.wrappedValue == Date(timeIntervalSince1970: 100)) + } + + @Test("RawRepresentable families roundtrip without Codable conformance") + func rawRepresentableFamiliesRoundtrip() { + let store = VolatileStorageBackend() + + let choice = AppStorage(wrappedValue: StringChoice.first, "string-choice", store: store) + choice.wrappedValue = .second + #expect(choice.wrappedValue == .second) + + let number = AppStorage(wrappedValue: IntChoice.one, "int-choice", store: store) + number.wrappedValue = .two + #expect(number.wrappedValue == .two) + } + + // MARK: - Optional Families + + @Test("Optional families start nil, store values, and clear on nil") + func optionalFamiliesRoundtrip() { + let store = VolatileStorageBackend() + + let name = AppStorage("optional-name", store: store) + #expect(name.wrappedValue == nil) + + name.wrappedValue = "present" + #expect(name.wrappedValue == "present") + + name.wrappedValue = nil + #expect(name.wrappedValue == nil) + + let choice = AppStorage("optional-choice", store: store) + #expect(choice.wrappedValue == nil) + choice.wrappedValue = .first + #expect(choice.wrappedValue == .first) + } + + // MARK: - Codable Convenience + + @Test("Custom Codable payloads keep working as an additive convenience") + func codableConvenienceStillWorks() { + let store = VolatileStorageBackend() + + let payload = AppStorage( + wrappedValue: CustomPayload(label: "default"), + "payload", + store: store + ) + payload.wrappedValue = CustomPayload(label: "written") + + #expect(payload.wrappedValue == CustomPayload(label: "written")) + } + + // MARK: - Store Sharing + + @Test("Two wrappers with the same key share the injected store") + func wrappersShareInjectedStore() { + let store = VolatileStorageBackend() + + let first = AppStorage(wrappedValue: 0, "shared", store: store) + let second = AppStorage(wrappedValue: 0, "shared", store: store) + + first.wrappedValue = 99 + + #expect(second.wrappedValue == 99) + } +} From d53aebc5cbaaa702dd8953921b421bd7ba774498 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:35:16 +0200 Subject: [PATCH 05/11] Feat: Add Bindable for deriving bindings from observable objects - Add SwiftUI's Bindable property wrapper with dynamic member lookup producing writable bindings into @Observable models - Provide the wrappedValue, unlabeled, and projectedValue initializer spellings plus conditional Identifiable and DynamicProperty conformances --- Sources/TUIkitView/State/Bindable.swift | 101 ++++++++++++++++++++++++ Tests/TUIkitTests/BindableTests.swift | 50 ++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 Sources/TUIkitView/State/Bindable.swift create mode 100644 Tests/TUIkitTests/BindableTests.swift diff --git a/Sources/TUIkitView/State/Bindable.swift b/Sources/TUIkitView/State/Bindable.swift new file mode 100644 index 00000000..cd3b47ca --- /dev/null +++ b/Sources/TUIkitView/State/Bindable.swift @@ -0,0 +1,101 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// Bindable.swift +// +// Created by LAYERED.work +// License: MIT + +import Observation + +// MARK: - Bindable + +/// A property wrapper type that supports creating bindings to the mutable +/// properties of observable objects. +/// +/// Matches SwiftUI's `Bindable`: wrap an `@Observable` model and use dynamic +/// member lookup to derive ``Binding`` values for its mutable properties: +/// +/// ```swift +/// @Observable +/// final class Profile { +/// var name = "" +/// } +/// +/// struct ProfileEditor: View { +/// @Bindable var profile: Profile +/// +/// var body: some View { +/// TextField("Name", text: $profile.name) +/// } +/// } +/// ``` +@propertyWrapper +@dynamicMemberLookup +public struct Bindable { + /// The wrapped observable object. + public var wrappedValue: Value + + /// The bindable wrapper itself, enabling `$model.property` bindings. + public var projectedValue: Bindable { + self + } +} + +// MARK: - Dynamic Member Lookup + +extension Bindable where Value: AnyObject { + /// Returns a binding to the value of a given key path. + /// + /// Writing through the returned binding mutates the observable object + /// directly; Observation reports the change to the owning runtime. + /// + /// - Parameter keyPath: A reference-writable key path into the object. + /// - Returns: A binding to the member at the key path. + public subscript( + dynamicMember keyPath: ReferenceWritableKeyPath + ) -> Binding { + Binding( + get: { self.wrappedValue[keyPath: keyPath] }, + set: { self.wrappedValue[keyPath: keyPath] = $0 } + ) + } +} + +// MARK: - Initializers + +extension Bindable where Value: AnyObject, Value: Observable { + /// Creates a bindable object from an observable object. + /// + /// - Parameter wrappedValue: The observable object to wrap. + public init(wrappedValue: Value) { + self.wrappedValue = wrappedValue + } + + /// Creates a bindable object from an observable object. + /// + /// SwiftUI-compatible unlabeled spelling for direct construction. + /// + /// - Parameter wrappedValue: The observable object to wrap. + public init(_ wrappedValue: Value) { + self.wrappedValue = wrappedValue + } + + /// Creates a bindable from the value of another bindable. + /// + /// - Parameter projectedValue: A bindable to rewrap. + public init(projectedValue: Bindable) { + self = projectedValue + } +} + +// MARK: - Identifiable Conformance + +extension Bindable: Identifiable where Value: Identifiable { + /// The stable identity of the wrapped object. + public var id: Value.ID { + wrappedValue.id + } +} + +// MARK: - Dynamic Property Conformance + +extension Bindable: DynamicProperty {} diff --git a/Tests/TUIkitTests/BindableTests.swift b/Tests/TUIkitTests/BindableTests.swift new file mode 100644 index 00000000..ad59c45d --- /dev/null +++ b/Tests/TUIkitTests/BindableTests.swift @@ -0,0 +1,50 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// BindableTests.swift +// +// Created by LAYERED.work +// License: MIT + +import Observation +import Testing + +@testable import TUIkit + +@MainActor +@Suite("Bindable") +struct BindableTests { + + @Observable + final class Model { + var text = "initial" + var count = 0 + } + + @Test("Dynamic member lookup produces bindings that write into the model") + func dynamicMemberWritesIntoModel() { + let model = Model() + let bindable = Bindable(model) + + let text: Binding = bindable.text + + #expect(text.wrappedValue == "initial") + + text.wrappedValue = "changed" + + #expect(model.text == "changed") + } + + @Test("The wrapper spellings share the same model instance") + func wrapperSpellingsShareModel() { + let model = Model() + + let byWrappedValue = Bindable(wrappedValue: model) + let byValue = Bindable(model) + let byProjection = Bindable(projectedValue: byValue) + + byWrappedValue.wrappedValue.count = 1 + + #expect(byValue.wrappedValue.count == 1) + #expect(byProjection.wrappedValue.count == 1) + #expect(byValue.projectedValue.wrappedValue === model) + } +} From 119536a301d1bd0335aa4c22cfa901a565bec246 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:37:36 +0200 Subject: [PATCH 06/11] Fix: Suppress Bindable's synthesized memberwise initializer - The synthesized internal init(wrappedValue:) violated the property-wrapper accessibility requirement; a private designated initializer now backs the public Observable-constrained spellings --- Sources/TUIkitView/State/Bindable.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/TUIkitView/State/Bindable.swift b/Sources/TUIkitView/State/Bindable.swift index cd3b47ca..7505551c 100644 --- a/Sources/TUIkitView/State/Bindable.swift +++ b/Sources/TUIkitView/State/Bindable.swift @@ -38,6 +38,15 @@ public struct Bindable { public var projectedValue: Bindable { self } + + /// Designated storage initializer. + /// + /// Suppresses the synthesized internal memberwise initializer, which + /// would violate the property-wrapper accessibility requirement; the + /// public initializers below are constrained to observable objects. + private init(storing wrappedValue: Value) { + self.wrappedValue = wrappedValue + } } // MARK: - Dynamic Member Lookup @@ -67,7 +76,7 @@ extension Bindable where Value: AnyObject, Value: Observable { /// /// - Parameter wrappedValue: The observable object to wrap. public init(wrappedValue: Value) { - self.wrappedValue = wrappedValue + self.init(storing: wrappedValue) } /// Creates a bindable object from an observable object. @@ -76,7 +85,7 @@ extension Bindable where Value: AnyObject, Value: Observable { /// /// - Parameter wrappedValue: The observable object to wrap. public init(_ wrappedValue: Value) { - self.wrappedValue = wrappedValue + self.init(storing: wrappedValue) } /// Creates a bindable from the value of another bindable. From 9f6ee3b3cb01477de246174ade84ef9272c67b86 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:39:24 +0200 Subject: [PATCH 07/11] Docs: Document Bindable, DynamicProperty, and the AppStorage families - Add Bindable and custom dynamic property sections to the state management article and describe the typed AppStorage families - Extend the data-flow compile fixture with wrapper families, dynamic member bindings, and a custom dynamic property --- .../TUIkit.docc/Articles/StateManagement.md | 34 +++++++++++++- .../CompileContracts/DataFlowPositive.swift | 45 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md b/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md index 1dc57fc6..18eae540 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md @@ -83,10 +83,40 @@ ContentView() .environment(\.myCustomValue, "custom") ``` +## @Bindable + +``Bindable`` derives bindings from the mutable properties of `@Observable` +model objects: + +```swift +@Observable +final class Profile { + var name = "" +} + +struct ProfileEditor: View { + @Bindable var profile: Profile + + var body: some View { + TextField("Name", text: $profile.name) + } +} +``` + +## Custom Dynamic Properties + +Conform to ``DynamicProperty`` to compose framework wrappers inside your own +property types. Nested wrappers hydrate with the owning view's identity, and +`update()` runs before every `body` evaluation. + ## @AppStorage ``AppStorage`` persists values across app launches using the application's -runtime-owned storage backend. TUIkit apps use ``JSONFileStorage`` by default: +runtime-owned storage backend. TUIkit apps use ``JSONFileStorage`` by +default. SwiftUI's typed initializer families (`Bool`, `Int`, `Double`, +`String`, `URL`, `Data`, `Date`, raw-representable enums, and their optional +variants) are available; any other `Codable` value works as an additive +convenience: ```swift struct SettingsView: View { @@ -102,7 +132,7 @@ Pass an explicit backend when a property wrapper is created outside the app runtime or needs dedicated storage: ```swift -@AppStorage("username", storage: MyStorageBackend()) var username = "Guest" +@AppStorage("username", store: MyStorageBackend()) var username = "Guest" ``` An `@AppStorage` property accessed before any runtime hydrates it falls back diff --git a/Tools/APICompatibility/Configuration/CompileContracts/DataFlowPositive.swift b/Tools/APICompatibility/Configuration/CompileContracts/DataFlowPositive.swift index f5d7014e..a33767b9 100644 --- a/Tools/APICompatibility/Configuration/CompileContracts/DataFlowPositive.swift +++ b/Tools/APICompatibility/Configuration/CompileContracts/DataFlowPositive.swift @@ -1,5 +1,11 @@ +import Foundation +import Observation import TUIkit +// Representative SwiftUI-style data-flow declarations that must compile +// unchanged under Swift 6.0: property wrappers, dynamic member bindings, +// typed AppStorage families, and custom dynamic properties. + @MainActor struct DataFlowView: View { @State private var count = 0 @@ -8,3 +14,42 @@ struct DataFlowView: View { Text("\(count)") } } + +@Observable +private final class DataFlowModel { + var name = "" +} + +private struct DataFlowProfile { + var title: String +} + +@MainActor +private struct WrapperFamilies: View { + @State private var profile = DataFlowProfile(title: "initial") + @State private var optionalSelection: Int? + @AppStorage("flag") private var flag = false + @AppStorage("level") private var level = 0 + @AppStorage("nickname") private var nickname: String? + @Bindable var model: DataFlowModel + + var body: some View { + Text(profile.title) + } + + @MainActor + func derivedBindings() -> (Binding, Binding, Binding) { + ($profile.title, $model.name, $optionalSelection) + } +} + +private struct CustomDynamicProperty: DynamicProperty { + @State private var backing = 0 + + mutating func update() {} +} + +@MainActor +private func constantCollectionBindings(_ values: Binding<[Int]>) -> [Binding] { + values.map { $0 } +} From 941f44ad0fb4214aac445c305eb602339f00a5f5 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:43:13 +0200 Subject: [PATCH 08/11] Test: Recharacterize the Binding sendability contracts - Binding is now Sendable for Sendable values per the SwiftUI reference, so the positive fixture asserts it and the negative fixture rejects bindings to non-Sendable values instead --- .../CompileContracts/SendabilityNegative.swift | 8 ++++++-- .../CompileContracts/SendabilityPositive.swift | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Tools/APICompatibility/Configuration/CompileContracts/SendabilityNegative.swift b/Tools/APICompatibility/Configuration/CompileContracts/SendabilityNegative.swift index aaebe6f1..970d06e8 100644 --- a/Tools/APICompatibility/Configuration/CompileContracts/SendabilityNegative.swift +++ b/Tools/APICompatibility/Configuration/CompileContracts/SendabilityNegative.swift @@ -2,6 +2,10 @@ import TUIkit private func requireSendable(_: Value.Type) {} -func rejectNonSendableBinding() { - requireSendable(Binding.self) +private final class NonSendableModel { + var value = 0 +} + +func rejectNonSendableValueBinding() { + requireSendable(Binding.self) } diff --git a/Tools/APICompatibility/Configuration/CompileContracts/SendabilityPositive.swift b/Tools/APICompatibility/Configuration/CompileContracts/SendabilityPositive.swift index ff348479..54a34964 100644 --- a/Tools/APICompatibility/Configuration/CompileContracts/SendabilityPositive.swift +++ b/Tools/APICompatibility/Configuration/CompileContracts/SendabilityPositive.swift @@ -5,3 +5,7 @@ private func requireSendable(_: Value.Type) {} func acceptSendableButtonRole() { requireSendable(ButtonRole.self) } + +func acceptSendableValueBinding() { + requireSendable(Binding.self) +} From f472ad7fa323b9c7d2350dacccda8716d9206bad Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:45:45 +0200 Subject: [PATCH 09/11] Test: Prove DynamicProperty conformances without tautological casts - The 'is' checks tripped the warning-fatal gate ("'is' test is always true"); generic requirement calls prove the conformances at compile time instead --- Tests/TUIkitTests/DynamicPropertyTests.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/TUIkitTests/DynamicPropertyTests.swift b/Tests/TUIkitTests/DynamicPropertyTests.swift index 56b9caae..c545dcca 100644 --- a/Tests/TUIkitTests/DynamicPropertyTests.swift +++ b/Tests/TUIkitTests/DynamicPropertyTests.swift @@ -28,12 +28,19 @@ struct DynamicPropertyTests { // MARK: - Conformances + /// Compile-time proof that a type conforms to DynamicProperty. + private func requireDynamicProperty(_: P.Type) {} + @Test("The built-in property wrappers are DynamicProperty types") func builtInWrappersAreDynamicProperties() { - #expect(State.self is any DynamicProperty.Type) - #expect(Binding.self is any DynamicProperty.Type) - #expect(Environment.self is any DynamicProperty.Type) - #expect(AppStorage.self is any DynamicProperty.Type) + // The generic requirements below only compile when each wrapper + // conforms; an `is` check would trip the warning-fatal gate with + // "'is' test is always true". + requireDynamicProperty(State.self) + requireDynamicProperty(Binding.self) + requireDynamicProperty(Environment.self) + requireDynamicProperty(AppStorage.self) + requireDynamicProperty(Bindable.self) } // MARK: - Nested Hydration From 258eb0b5e9a905b533328685319bc07f0bd50e0f Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 13:47:12 +0200 Subject: [PATCH 10/11] Chore: Satisfy the Self-reference lint rule in the conformance test --- Tests/TUIkitTests/DynamicPropertyTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TUIkitTests/DynamicPropertyTests.swift b/Tests/TUIkitTests/DynamicPropertyTests.swift index c545dcca..e0bec734 100644 --- a/Tests/TUIkitTests/DynamicPropertyTests.swift +++ b/Tests/TUIkitTests/DynamicPropertyTests.swift @@ -40,7 +40,7 @@ struct DynamicPropertyTests { requireDynamicProperty(Binding.self) requireDynamicProperty(Environment.self) requireDynamicProperty(AppStorage.self) - requireDynamicProperty(Bindable.self) + requireDynamicProperty(Bindable.self) } // MARK: - Nested Hydration From 6fcd8c851af04be4d23e5b40dff02cff0aa214c0 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 14:09:59 +0200 Subject: [PATCH 11/11] Chore: Regenerate the API compatibility manifest for the data-flow surface - Drop the two stale overrides from the reshaped AppStorage wrapper - Add overrides for the new surface: DynamicProperty, Bindable, the Binding projection initializers and conditional conformances, the State initializers, and the typed AppStorage families - Regenerate the manifest with TUIkitAPICheck against fresh 6.0.3 macOS and Linux snapshots --- .../Configuration/compatibility-manifest.json | 274 ++++++++++++++++- .../Configuration/review-policy.json | 290 +++++++++++++++++- 2 files changed, 552 insertions(+), 12 deletions(-) diff --git a/Tools/APICompatibility/Configuration/compatibility-manifest.json b/Tools/APICompatibility/Configuration/compatibility-manifest.json index 6286949e..cd307b06 100644 --- a/Tools/APICompatibility/Configuration/compatibility-manifest.json +++ b/Tools/APICompatibility/Configuration/compatibility-manifest.json @@ -338891,6 +338891,21 @@ "ownerIssue" : "#35", "symbolID" : "s:10TUIkitView14renderToBuffer_7context0A4Core05FrameE0Vx_AA13RenderContextVtAA0B0RzlF" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15DynamicPropertyP" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15DynamicPropertyP6updateyyF" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15DynamicPropertyPAAE6updateyyF" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -339076,6 +339091,11 @@ "ownerIssue" : "#35", "symbolID" : "s:10TUIkitView5StateV" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView5StateV12initialValueACyxGx_tcfc" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -339091,6 +339111,11 @@ "ownerIssue" : "#35", "symbolID" : "s:10TUIkitView5StateV14projectedValueAA7BindingVyxGvp" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView5StateVAAs23ExpressibleByNilLiteralRzlEACyxGycfc" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -339101,6 +339126,16 @@ "ownerIssue" : "#35", "symbolID" : "s:10TUIkitView7BindingV12wrappedValuexvp" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingV13dynamicMemberACyqd__Gs15WritableKeyPathCyxqd__G_tcluip" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingV14projectedValueACyxGAE_tcfc" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -339116,6 +339151,86 @@ "ownerIssue" : "#35", "symbolID" : "s:10TUIkitView7BindingV8constantyACyxGxFZ" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASKRzSMRzlE5index6before5IndexSlQzAG_tF" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASKRzSMRzlE9formIndex6beforey0E0SlQzz_tF" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE10startIndex0E0Qzvp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE5Indexa" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE5index5after5IndexQzAG_tF" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE7Elementa" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE7Indicesa" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE7indices7IndicesQzvp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE8Iteratora" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE8endIndex0E0Qzvp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlE9formIndex5aftery0E0Qzz_tF" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAASMRzlEyACy7ElementQzG5IndexQzcip" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVAAs12IdentifiableRzlE2id2IDQzvp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVyACyqd__SgGACyqd__GcADRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVyACys11AnyHashableVGACyqd__GcAERszSHRd__lufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView7BindingVyACyxGSgACyxSgGcfc" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -339176,6 +339291,46 @@ "ownerIssue" : "#35", "symbolID" : "s:10TUIkitView8AppStateCACycfc" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableV" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableV12wrappedValuexvp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableV14projectedValueACyxGvp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableVAARlzC11Observation10ObservableRzlE12wrappedValueACyxGx_tcfc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableVAARlzC11Observation10ObservableRzlE14projectedValueACyxGAG_tcfc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableVAARlzC11Observation10ObservableRzlEyACyxGxcfc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableVAARlzClE13dynamicMemberAA7BindingVyqd__Gs24ReferenceWritableKeyPathCyxqd__G_tcluip" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView8BindableVAAs12IdentifiableRzlE2id2IDQzvp" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -340929,12 +341084,67 @@ { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_7storageACyxGx_SSAA0C7Backend_ptcfc" + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACy10Foundation3URLVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACy10Foundation4DataVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACy10Foundation4DateVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACy20FoundationEssentials3URLVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACy20FoundationEssentials4DataVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACy20FoundationEssentials4DateVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACySSGSS_SSAA0C7Backend_pSgtcSSRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACySbGSb_SSAA0C7Backend_pSgtcSbRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACySdGSd_SSAA0C7Backend_pSgtcSdRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACySiGSi_SSAA0C7Backend_pSgtcSiRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACyxGx_SSAA0C7Backend_pSgtcSYRzSS03RawE0Rtzlufc" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_ACyxGx_SStcfc" + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACyxGx_SSAA0C7Backend_pSgtcSYRzSi03RawE0Rtzlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV12wrappedValue_5storeACyxGx_SSAA0C7Backend_pSgtcSeRzSERzlufc" }, { "classification" : "implementationLeak", @@ -340946,6 +341156,66 @@ "ownerIssue" : "#35", "symbolID" : "s:6TUIkit10AppStorageV14projectedValue0A4View7BindingVyxGvp" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACy10Foundation3URLVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACy10Foundation4DataVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACy10Foundation4DateVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACy20FoundationEssentials3URLVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACy20FoundationEssentials4DataVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACy20FoundationEssentials4DateVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACySSSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACySbSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACySdSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACySiSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACyqd__SgGSS_AA0C7Backend_pSgtcAERszSYRd__SS8RawValueRtd__lufc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:6TUIkit10AppStorageV_5storeACyqd__SgGSS_AA0C7Backend_pSgtcAERszSYRd__Si8RawValueRtd__lufc" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", diff --git a/Tools/APICompatibility/Configuration/review-policy.json b/Tools/APICompatibility/Configuration/review-policy.json index c8beedbe..78fc25b7 100644 --- a/Tools/APICompatibility/Configuration/review-policy.json +++ b/Tools/APICompatibility/Configuration/review-policy.json @@ -21318,16 +21318,6 @@ "ownerIssue": "#35", "symbolID": "s:6TUIkit10AppStorageV" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_7storageACyxGx_SSAA0C7Backend_ptcfc" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_ACyxGx_SStcfc" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -25877,6 +25867,286 @@ "action": "implementationLeak", "ownerIssue": "#35", "symbolID": "s:10TUIkitView15ModifiedContentVA2A0B0RzAA0B8ModifierR_rlE4bodys5NeverOvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15DynamicPropertyP" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15DynamicPropertyP6updateyyF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15DynamicPropertyPAAE6updateyyF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView5StateV12initialValueACyxGx_tcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView5StateVAAs23ExpressibleByNilLiteralRzlEACyxGycfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingV13dynamicMemberACyqd__Gs15WritableKeyPathCyxqd__G_tcluip" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingV14projectedValueACyxGAE_tcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASKRzSMRzlE5index6before5IndexSlQzAG_tF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASKRzSMRzlE9formIndex6beforey0E0SlQzz_tF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE10startIndex0E0Qzvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE5Indexa" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE5index5after5IndexQzAG_tF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE7Elementa" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE7Indicesa" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE7indices7IndicesQzvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE8Iteratora" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE8endIndex0E0Qzvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlE9formIndex5aftery0E0Qzz_tF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAASMRzlEyACy7ElementQzG5IndexQzcip" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVAAs12IdentifiableRzlE2id2IDQzvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVyACyqd__SgGACyqd__GcADRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVyACys11AnyHashableVGACyqd__GcAERszSHRd__lufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView7BindingVyACyxGSgACyxSgGcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableV" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableV12wrappedValuexvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableV14projectedValueACyxGvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableVAARlzC11Observation10ObservableRzlE12wrappedValueACyxGx_tcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableVAARlzC11Observation10ObservableRzlE14projectedValueACyxGAG_tcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableVAARlzC11Observation10ObservableRzlEyACyxGxcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableVAARlzClE13dynamicMemberAA7BindingVyqd__Gs24ReferenceWritableKeyPathCyxqd__G_tcluip" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView8BindableVAAs12IdentifiableRzlE2id2IDQzvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACy10Foundation3URLVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACy10Foundation4DataVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACy10Foundation4DateVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACy20FoundationEssentials3URLVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACy20FoundationEssentials4DataVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACy20FoundationEssentials4DateVGAH_SSAA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACySSGSS_SSAA0C7Backend_pSgtcSSRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACySbGSb_SSAA0C7Backend_pSgtcSbRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACySdGSd_SSAA0C7Backend_pSgtcSdRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACySiGSi_SSAA0C7Backend_pSgtcSiRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACyxGx_SSAA0C7Backend_pSgtcSYRzSS03RawE0Rtzlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACyxGx_SSAA0C7Backend_pSgtcSYRzSi03RawE0Rtzlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV12wrappedValue_5storeACyxGx_SSAA0C7Backend_pSgtcSeRzSERzlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACy10Foundation3URLVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACy10Foundation4DataVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACy10Foundation4DateVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACy20FoundationEssentials3URLVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACy20FoundationEssentials4DataVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACy20FoundationEssentials4DateVSgGSS_AA0C7Backend_pSgtcAHRszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACySSSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACySbSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACySdSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACySiSgGSS_AA0C7Backend_pSgtcAERszlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACyqd__SgGSS_AA0C7Backend_pSgtcAERszSYRd__SS8RawValueRtd__lufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:6TUIkit10AppStorageV_5storeACyqd__SgGSS_AA0C7Backend_pSgtcAERszSYRd__Si8RawValueRtd__lufc" } ] }