Skip to content

Commit 63fde71

Browse files
committed
Rename Observable to ObservableObject and Observed to Published
Should make migrating from SwiftUI a bit nicer.
1 parent d2dd03a commit 63fde71

File tree

10 files changed

+157
-145
lines changed

10 files changed

+157
-145
lines changed

Examples/Sources/ControlsExample/ControlsApp.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import SwiftCrossUI
55
import SwiftBundlerRuntime
66
#endif
77

8-
class ControlsState: Observable {
9-
}
10-
118
@main
129
@HotReloadable
1310
struct ControlsApp: App {

Sources/SwiftCrossUI/Modifiers/TaskModifier.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,8 @@ extension View {
3434
}
3535
}
3636

37-
class TaskModifierState<Id: Equatable>: Observable {
38-
var task: Task<(), any Error>?
39-
}
40-
4137
struct TaskModifier<Id: Equatable, Content: View>: View {
42-
@State
43-
var task: Task<(), any Error>? = nil
38+
@State var task: Task<(), any Error>? = nil
4439

4540
var id: Id
4641
var content: Content

Sources/SwiftCrossUI/State/Binding.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ public class Binding<Value> {
1818
private let setValue: (Value) -> Void
1919

2020
/// Creates a binding with a custom getter and setter. To create a binding from
21-
/// an observed state variable use its projected value instead: e.g. `state.$value`
22-
/// will give you a binding for reading and writing `state.value` (assuming that
23-
/// `state.value` is marked with `@Observed`).
21+
/// an `@State` property use its projected value instead: e.g. `$myStateProperty`
22+
/// will give you a binding for reading and writing `myStateProperty` (assuming that
23+
/// `myStateProperty` is marked with `@State` at its declaration site).
2424
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) {
2525
self.getValue = get
2626
self.setValue = set
Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
11
/// An object that can be observed for changes.
22
///
3-
/// The default implementation only publishes changes made to properties that have been wrapped with
4-
/// the ``Observed`` property wrapper. Even properties that themselves conform to ``Observable``
5-
/// must be wrapped with the ``Observed`` property wrapper for clarity.
3+
/// The default implementation only publishes changes made to properties that
4+
/// have been wrapped with the ``Published`` property wrapper. Even properties
5+
/// that themselves conform to ``ObservableObject`` must be wrapped with the
6+
/// ``Published`` property wrapper for clarity.
67
///
78
/// ```swift
8-
/// class NestedState: Observable {
9+
/// class NestedState: ObservableObject {
910
/// // Both `startIndex` and `endIndex` will have their changes published to `NestedState`'s
1011
/// // `didChange` publisher.
11-
/// @Observed
12+
/// @Published
1213
/// var startIndex = 0
1314
///
14-
/// @Observed
15+
/// @Published
1516
/// var endIndex = 0
1617
/// }
1718
///
18-
/// class CounterState: Observable {
19-
/// // Only changes to `count` will be published (it is the only property with `@Observed`)
20-
/// @Observed
19+
/// class CounterState: ObservableObject {
20+
/// // Only changes to `count` will be published (it is the only property with `@Published`)
21+
/// @Published
2122
/// var count = 0
2223
///
2324
/// var otherCount = 0
2425
///
25-
/// // Even though `nested` is `Observable`, its changes won't be published because if you
26-
/// // could have observed properties without `@Observed` things would get pretty messy
27-
/// // and you'd always have to check the definition of the type of each property to know
28-
/// // exactly what would and wouldn't cause updates.
26+
/// // Even though `nested` is `ObservableObject`, its changes won't be
27+
/// // published because if you could have observed properties without
28+
/// // `@Published` things would get pretty messy and you'd always have to
29+
/// // check the definition of the type of each property to know exactly
30+
/// // what would and wouldn't cause updates.
2931
/// var nested = NestedState()
3032
/// }
3133
/// ```
32-
public protocol Observable: AnyObject {
34+
public protocol ObservableObject: AnyObject {
3335
/// A publisher which publishes changes made to the object. Only publishes changes made to
34-
/// ``Observed`` properties by default.
36+
/// ``Published`` properties by default.
3537
var didChange: Publisher { get }
3638
}
3739

38-
extension Observable {
40+
extension ObservableObject {
3941
public var didChange: Publisher {
4042
let publisher = Publisher()
4143
.tag(with: String(describing: type(of: self)))
@@ -44,8 +46,8 @@ extension Observable {
4446
while let aClass = mirror {
4547
for (_, property) in aClass.children {
4648
guard
47-
property is ObservedMarkerProtocol,
48-
let property = property as? Observable
49+
property is PublishedMarkerProtocol,
50+
let property = property as? ObservableObject
4951
else {
5052
continue
5153
}
@@ -57,3 +59,6 @@ extension Observable {
5759
return publisher
5860
}
5961
}
62+
63+
@available(*, deprecated, message: "Replace Observable with ObservableObject")
64+
public typealias Observable = ObservableObject

Sources/SwiftCrossUI/State/Observed.swift

Lines changed: 0 additions & 110 deletions
This file was deleted.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import Foundation
2+
3+
/// ``ObservableObject`` values nested within an ``ObservableObject`` object
4+
/// will only have their changes published by the parent ``ObservableObject``
5+
/// if marked with this marker protocol. This avoids uncertainty around which
6+
/// properties will or will not have their changes published by the parent.
7+
/// For clarity reasons, you shouldn't conform your own types to this protocol.
8+
/// Instead, apply the ``Published`` property wrapper when needed.
9+
///
10+
/// ```swift
11+
/// // The following example highlights why the marker protocol exists.
12+
///
13+
/// class MyNestedState: ObservableObject {
14+
/// @Published var count = 0
15+
/// }
16+
///
17+
/// class MyState: ObservableObject {
18+
/// // Without the marker protocol mechanism in place, `nested` would get
19+
/// // published as well as `index`. However, that would not be possible to
20+
/// // know without looking at the definition to check if `MyNestedState`
21+
/// // is `ObservableObject`. Because of the marker protocol, it is required
22+
/// // that both properties are annotated with `@Published` (which conforms
23+
/// // to the marker protocol).
24+
/// var nested = MyNestedState()
25+
/// @Published var index = 0
26+
/// }
27+
/// ```
28+
///
29+
public protocol PublishedMarkerProtocol {}
30+
31+
/// A wrapper which publishes a change whenever the wrapped value is set. If
32+
/// the wrapped value is ``ObservableObject``, its `didChange` publisher will
33+
/// also be forwarded to the wrapper's publisher.
34+
///
35+
/// A compile time warning is emitted if the wrapper is applied to a class
36+
/// which isn't ``ObservableObject`` because this is considered undesired
37+
/// behaviour. Only replacing the value with a new instance of the class would
38+
/// cause a change to be published; changing the class' properties would not.
39+
/// The warning will show up as a deprecation, but it isn't (as you could guess
40+
/// from the accompanying message).
41+
@propertyWrapper
42+
public final class Published<Value>: ObservableObject, PublishedMarkerProtocol {
43+
/// A handle that can be used to cancel the link to the previous upstream publisher.
44+
private var upstreamLinkCancellable: Cancellable?
45+
46+
/// A binding to the inner value.
47+
public var projectedValue: Binding<Value> {
48+
Binding(
49+
get: {
50+
self.wrappedValue
51+
},
52+
set: { newValue in
53+
self.wrappedValue = newValue
54+
}
55+
)
56+
}
57+
58+
/// The underlying wrapped value.
59+
public var wrappedValue: Value {
60+
didSet {
61+
valueDidChange()
62+
}
63+
}
64+
65+
/// A publisher that publishes any observable changes made to
66+
/// ``Published/wrappedValue``.
67+
public let didChange = Publisher().tag(with: "Published")
68+
69+
/// Creates a publishing wrapper around a value type or
70+
/// ``ObservableObject`` class.
71+
public init(wrappedValue: Value) {
72+
self.wrappedValue = wrappedValue
73+
valueDidChange(publish: false)
74+
}
75+
76+
/// Creates a publishing wrapper around a value type or ``ObservableObject``
77+
/// class.
78+
public init(wrappedValue: Value) where Value: AnyObject, Value: ObservableObject {
79+
// This initializer exists to redirect valid classes away from the initializer which
80+
// contains a compile time warning (through deprecation).
81+
self.wrappedValue = wrappedValue
82+
valueDidChange(publish: false)
83+
}
84+
85+
/// Creates a wrapper around a non-ObservableObject class. Setting
86+
/// ``Published/wrappedValue`` to a new instance of the class is the only
87+
/// change that will get published. This is hardly ever intentional, so
88+
/// this initializer variant contains a deprecation warning to warn
89+
/// developers (but does nothing functionally different).
90+
@available(
91+
*, deprecated,
92+
message: "A class must conform to ObservableObject to be Published"
93+
)
94+
public init(wrappedValue: Value) where Value: AnyObject {
95+
self.wrappedValue = wrappedValue
96+
valueDidChange(publish: false)
97+
}
98+
99+
/// Handles changing a value. If `publish` is `false` the change won't be
100+
/// published, but if the wrapped value is ``ObservableObject`` the new
101+
/// upstream publisher will still get relinked.
102+
public func valueDidChange(publish: Bool = true) {
103+
if publish {
104+
didChange.send()
105+
}
106+
107+
if let upstream = wrappedValue as? ObservableObject {
108+
upstreamLinkCancellable?.cancel()
109+
upstreamLinkCancellable = didChange.link(toUpstream: upstream.didChange)
110+
}
111+
}
112+
}
113+
114+
extension Published: Codable where Value: Codable {
115+
public convenience init(from decoder: Decoder) throws {
116+
self.init(wrappedValue: try Value(from: decoder))
117+
}
118+
119+
public func encode(to encoder: Encoder) throws {
120+
try wrappedValue.encode(to: encoder)
121+
}
122+
}
123+
124+
@available(*, deprecated, message: "Replace Observed with Published")
125+
public typealias Observed = Published

Sources/SwiftCrossUI/State/State.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public struct State<Value>: DynamicProperty, StateProperty {
4242
public init(wrappedValue initialValue: Value) {
4343
storage = Storage(initialValue)
4444

45-
if let initialValue = initialValue as? Observable {
45+
if let initialValue = initialValue as? ObservableObject {
4646
_ = didChange.link(toUpstream: initialValue.didChange)
4747
}
4848
}

Sources/SwiftCrossUI/SwiftCrossUI.docc/SwiftCrossUI.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ Objects that are read from and/or written to as part of your app.
5454

5555
- ``State``
5656
- ``Binding``
57-
- ``Observable``
58-
- ``Observed``
57+
- ``ObservableObject``
58+
- ``Published``
5959
- ``Publisher``
6060
- ``Cancellable``
6161

Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend> {
103103
// Update the view and its children when state changes (children are always updated first).
104104
let mirror = Mirror(reflecting: view)
105105
for property in mirror.children {
106-
if property.label == "state" && property.value is Observable {
106+
if property.label == "state" && property.value is ObservableObject {
107107
print(
108108
"""
109109

Sources/SwiftCrossUI/_App.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class _App<AppRoot: App> {
7979

8080
let mirror = Mirror(reflecting: self.app)
8181
for property in mirror.children {
82-
if property.label == "state" && property.value is Observable {
82+
if property.label == "state" && property.value is ObservableObject {
8383
print(
8484
"""
8585

0 commit comments

Comments
 (0)