From 9f3b473f1e375c37f4bd9677195a6f25cd2f197d Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 3 Nov 2025 16:06:33 +0100 Subject: [PATCH 1/3] Renamed StateProperty to ObservableProperty and made the protocol public --- Sources/SwiftCrossUI/State/State.swift | 13 ++++++++----- Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift | 2 +- .../ViewGraph/ViewGraphSnapshotter.swift | 4 ++-- Sources/SwiftCrossUI/_App.swift | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftCrossUI/State/State.swift b/Sources/SwiftCrossUI/State/State.swift index 9b3219c68f..b090a3eb9d 100644 --- a/Sources/SwiftCrossUI/State/State.swift +++ b/Sources/SwiftCrossUI/State/State.swift @@ -5,7 +5,7 @@ import Foundation // - It supports ObservableObject // - It supports Optional @propertyWrapper -public struct State: DynamicProperty, StateProperty { +public struct State: DynamicProperty, ObservableProperty { class Storage { // This inner box is what stays constant between view updates. The // outer box (Storage) is used so that we can assign this box to @@ -55,7 +55,7 @@ public struct State: DynamicProperty, StateProperty { var storage: Storage - var didChange: Publisher { + public var didChange: Publisher { storage.box.didChange } @@ -109,7 +109,7 @@ public struct State: DynamicProperty, StateProperty { } } - func tryRestoreFromSnapshot(_ snapshot: Data) { + public func tryRestoreFromSnapshot(_ snapshot: Data) { guard let decodable = Value.self as? Codable.Type, let state = try? JSONDecoder().decode(decodable, from: snapshot) @@ -120,7 +120,7 @@ public struct State: DynamicProperty, StateProperty { storage.box.value = state as! Value } - func snapshot() throws -> Data? { + public func snapshot() throws -> Data? { if let value = storage.box as? Codable { return try JSONEncoder().encode(value) } else { @@ -129,7 +129,10 @@ public struct State: DynamicProperty, StateProperty { } } -protocol StateProperty { +/// Declaring conformance to ObservableProperty is required for +/// SwiftCrossUI to automatically register an observer on the +/// conforming object's Publisher. +public protocol ObservableProperty { var didChange: Publisher { get } func tryRestoreFromSnapshot(_ snapshot: Data) func snapshot() throws -> Data? diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index b80b848699..0d4ad09f33 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -116,7 +116,7 @@ public class ViewGraphNode: Sendable { ) } - guard let value = property.value as? StateProperty else { + guard let value = property.value as? ObservableProperty else { continue } diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift index ea7cd436e7..7c94ab7d9e 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift @@ -55,7 +55,7 @@ public struct ViewGraphSnapshotter: ErasedViewGraphNodeTransformer { let mirror = Mirror(reflecting: view) for property in mirror.children { guard - let stateProperty = property as? StateProperty, + let stateProperty = property as? ObservableProperty, let propertyName = property.label, let encodedState = state[propertyName] else { @@ -80,7 +80,7 @@ public struct ViewGraphSnapshotter: ErasedViewGraphNodeTransformer { for property in mirror.children { guard let propertyName = property.label, - let property = property as? StateProperty, + let property = property as? ObservableProperty, let encodedState = try? property.snapshot() else { continue diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift index 4d0a0a3624..1f9c8df2a8 100644 --- a/Sources/SwiftCrossUI/_App.swift +++ b/Sources/SwiftCrossUI/_App.swift @@ -66,7 +66,7 @@ class _App { ) } - guard let value = property.value as? StateProperty else { + guard let value = property.value as? ObservableProperty else { continue } From 976e096599f53d88e7bae963f2d1c0f9b51b5e70 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 15 Dec 2025 21:16:59 +0100 Subject: [PATCH 2/3] Implemented requested changes moved ObservableProperty to separate file, updated documentation comment --- Sources/SwiftCrossUI/State/ObservableProperty.swift | 11 +++++++++++ Sources/SwiftCrossUI/State/State.swift | 11 +---------- 2 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 Sources/SwiftCrossUI/State/ObservableProperty.swift diff --git a/Sources/SwiftCrossUI/State/ObservableProperty.swift b/Sources/SwiftCrossUI/State/ObservableProperty.swift new file mode 100644 index 0000000000..bb30020e2e --- /dev/null +++ b/Sources/SwiftCrossUI/State/ObservableProperty.swift @@ -0,0 +1,11 @@ +import Foundation + +/// View properties that conform to ObservableProperty are automatically observed by SwiftCrossUI. +/// +/// This protocol is intended to be implemented by property wrappers. You shouldn't +/// have to implement it for your own model types. +public protocol ObservableProperty: DynamicProperty { + var didChange: Publisher { get } + func tryRestoreFromSnapshot(_ snapshot: Data) + func snapshot() throws -> Data? +} diff --git a/Sources/SwiftCrossUI/State/State.swift b/Sources/SwiftCrossUI/State/State.swift index b090a3eb9d..e49fc1e696 100644 --- a/Sources/SwiftCrossUI/State/State.swift +++ b/Sources/SwiftCrossUI/State/State.swift @@ -5,7 +5,7 @@ import Foundation // - It supports ObservableObject // - It supports Optional @propertyWrapper -public struct State: DynamicProperty, ObservableProperty { +public struct State: ObservableProperty { class Storage { // This inner box is what stays constant between view updates. The // outer box (Storage) is used so that we can assign this box to @@ -128,12 +128,3 @@ public struct State: DynamicProperty, ObservableProperty { } } } - -/// Declaring conformance to ObservableProperty is required for -/// SwiftCrossUI to automatically register an observer on the -/// conforming object's Publisher. -public protocol ObservableProperty { - var didChange: Publisher { get } - func tryRestoreFromSnapshot(_ snapshot: Data) - func snapshot() throws -> Data? -} From 5f3343264041711453f4f48b550274968e2b2068 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Mon, 15 Dec 2025 21:52:12 +0100 Subject: [PATCH 3/3] Attempt to fix CI run failing due to as? ObservableProperty instead of as? any ObservableProperty --- Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift | 2 +- Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift | 4 ++-- Sources/SwiftCrossUI/_App.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index 0d4ad09f33..b74ab602cb 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -116,7 +116,7 @@ public class ViewGraphNode: Sendable { ) } - guard let value = property.value as? ObservableProperty else { + guard let value = property.value as? any ObservableProperty else { continue } diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift index 7c94ab7d9e..189bfb567c 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphSnapshotter.swift @@ -55,7 +55,7 @@ public struct ViewGraphSnapshotter: ErasedViewGraphNodeTransformer { let mirror = Mirror(reflecting: view) for property in mirror.children { guard - let stateProperty = property as? ObservableProperty, + let stateProperty = property as? any ObservableProperty, let propertyName = property.label, let encodedState = state[propertyName] else { @@ -80,7 +80,7 @@ public struct ViewGraphSnapshotter: ErasedViewGraphNodeTransformer { for property in mirror.children { guard let propertyName = property.label, - let property = property as? ObservableProperty, + let property = property as? any ObservableProperty, let encodedState = try? property.snapshot() else { continue diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift index 1f9c8df2a8..fe5a2a012a 100644 --- a/Sources/SwiftCrossUI/_App.swift +++ b/Sources/SwiftCrossUI/_App.swift @@ -66,7 +66,7 @@ class _App { ) } - guard let value = property.value as? ObservableProperty else { + guard let value = property.value as? any ObservableProperty else { continue }