Skip to content

Commit

Permalink
Rename Observable to ObservableObject and Observed to Published
Browse files Browse the repository at this point in the history
Should make migrating from SwiftUI a bit nicer.
  • Loading branch information
stackotter committed Jan 9, 2025
1 parent d2dd03a commit 63fde71
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 145 deletions.
3 changes: 0 additions & 3 deletions Examples/Sources/ControlsExample/ControlsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import SwiftCrossUI
import SwiftBundlerRuntime
#endif

class ControlsState: Observable {
}

@main
@HotReloadable
struct ControlsApp: App {
Expand Down
7 changes: 1 addition & 6 deletions Sources/SwiftCrossUI/Modifiers/TaskModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,8 @@ extension View {
}
}

class TaskModifierState<Id: Equatable>: Observable {
var task: Task<(), any Error>?
}

struct TaskModifier<Id: Equatable, Content: View>: View {
@State
var task: Task<(), any Error>? = nil
@State var task: Task<(), any Error>? = nil

var id: Id
var content: Content
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftCrossUI/State/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ public class Binding<Value> {
private let setValue: (Value) -> Void

/// Creates a binding with a custom getter and setter. To create a binding from
/// an observed state variable use its projected value instead: e.g. `state.$value`
/// will give you a binding for reading and writing `state.value` (assuming that
/// `state.value` is marked with `@Observed`).
/// an `@State` property use its projected value instead: e.g. `$myStateProperty`
/// will give you a binding for reading and writing `myStateProperty` (assuming that
/// `myStateProperty` is marked with `@State` at its declaration site).
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) {
self.getValue = get
self.setValue = set
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
/// An object that can be observed for changes.
///
/// The default implementation only publishes changes made to properties that have been wrapped with
/// the ``Observed`` property wrapper. Even properties that themselves conform to ``Observable``
/// must be wrapped with the ``Observed`` property wrapper for clarity.
/// The default implementation only publishes changes made to properties that
/// have been wrapped with the ``Published`` property wrapper. Even properties
/// that themselves conform to ``ObservableObject`` must be wrapped with the
/// ``Published`` property wrapper for clarity.
///
/// ```swift
/// class NestedState: Observable {
/// class NestedState: ObservableObject {
/// // Both `startIndex` and `endIndex` will have their changes published to `NestedState`'s
/// // `didChange` publisher.
/// @Observed
/// @Published
/// var startIndex = 0
///
/// @Observed
/// @Published
/// var endIndex = 0
/// }
///
/// class CounterState: Observable {
/// // Only changes to `count` will be published (it is the only property with `@Observed`)
/// @Observed
/// class CounterState: ObservableObject {
/// // Only changes to `count` will be published (it is the only property with `@Published`)
/// @Published
/// var count = 0
///
/// var otherCount = 0
///
/// // Even though `nested` is `Observable`, its changes won't be published because if you
/// // could have observed properties without `@Observed` things would get pretty messy
/// // and you'd always have to check the definition of the type of each property to know
/// // exactly what would and wouldn't cause updates.
/// // Even though `nested` is `ObservableObject`, its changes won't be
/// // published because if you could have observed properties without
/// // `@Published` things would get pretty messy and you'd always have to
/// // check the definition of the type of each property to know exactly
/// // what would and wouldn't cause updates.
/// var nested = NestedState()
/// }
/// ```
public protocol Observable: AnyObject {
public protocol ObservableObject: AnyObject {
/// A publisher which publishes changes made to the object. Only publishes changes made to
/// ``Observed`` properties by default.
/// ``Published`` properties by default.
var didChange: Publisher { get }
}

extension Observable {
extension ObservableObject {
public var didChange: Publisher {
let publisher = Publisher()
.tag(with: String(describing: type(of: self)))
Expand All @@ -44,8 +46,8 @@ extension Observable {
while let aClass = mirror {
for (_, property) in aClass.children {
guard
property is ObservedMarkerProtocol,
let property = property as? Observable
property is PublishedMarkerProtocol,
let property = property as? ObservableObject
else {
continue
}
Expand All @@ -57,3 +59,6 @@ extension Observable {
return publisher
}
}

@available(*, deprecated, message: "Replace Observable with ObservableObject")
public typealias Observable = ObservableObject
110 changes: 0 additions & 110 deletions Sources/SwiftCrossUI/State/Observed.swift

This file was deleted.

125 changes: 125 additions & 0 deletions Sources/SwiftCrossUI/State/Published.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Foundation

/// ``ObservableObject`` values nested within an ``ObservableObject`` object
/// will only have their changes published by the parent ``ObservableObject``
/// if marked with this marker protocol. This avoids uncertainty around which
/// properties will or will not have their changes published by the parent.
/// For clarity reasons, you shouldn't conform your own types to this protocol.
/// Instead, apply the ``Published`` property wrapper when needed.
///
/// ```swift
/// // The following example highlights why the marker protocol exists.
///
/// class MyNestedState: ObservableObject {
/// @Published var count = 0
/// }
///
/// class MyState: ObservableObject {
/// // Without the marker protocol mechanism in place, `nested` would get
/// // published as well as `index`. However, that would not be possible to
/// // know without looking at the definition to check if `MyNestedState`
/// // is `ObservableObject`. Because of the marker protocol, it is required
/// // that both properties are annotated with `@Published` (which conforms
/// // to the marker protocol).
/// var nested = MyNestedState()
/// @Published var index = 0
/// }
/// ```
///
public protocol PublishedMarkerProtocol {}

/// A wrapper which publishes a change whenever the wrapped value is set. If
/// the wrapped value is ``ObservableObject``, its `didChange` publisher will
/// also be forwarded to the wrapper's publisher.
///
/// A compile time warning is emitted if the wrapper is applied to a class
/// which isn't ``ObservableObject`` because this is considered undesired
/// behaviour. Only replacing the value with a new instance of the class would
/// cause a change to be published; changing the class' properties would not.
/// The warning will show up as a deprecation, but it isn't (as you could guess
/// from the accompanying message).
@propertyWrapper
public final class Published<Value>: ObservableObject, PublishedMarkerProtocol {
/// A handle that can be used to cancel the link to the previous upstream publisher.
private var upstreamLinkCancellable: Cancellable?

/// A binding to the inner value.
public var projectedValue: Binding<Value> {
Binding(
get: {
self.wrappedValue
},
set: { newValue in
self.wrappedValue = newValue
}
)
}

/// The underlying wrapped value.
public var wrappedValue: Value {
didSet {
valueDidChange()
}
}

/// A publisher that publishes any observable changes made to
/// ``Published/wrappedValue``.
public let didChange = Publisher().tag(with: "Published")

/// Creates a publishing wrapper around a value type or
/// ``ObservableObject`` class.
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
valueDidChange(publish: false)
}

/// Creates a publishing wrapper around a value type or ``ObservableObject``
/// class.
public init(wrappedValue: Value) where Value: AnyObject, Value: ObservableObject {
// This initializer exists to redirect valid classes away from the initializer which
// contains a compile time warning (through deprecation).
self.wrappedValue = wrappedValue
valueDidChange(publish: false)
}

/// Creates a wrapper around a non-ObservableObject class. Setting
/// ``Published/wrappedValue`` to a new instance of the class is the only
/// change that will get published. This is hardly ever intentional, so
/// this initializer variant contains a deprecation warning to warn
/// developers (but does nothing functionally different).
@available(
*, deprecated,
message: "A class must conform to ObservableObject to be Published"
)
public init(wrappedValue: Value) where Value: AnyObject {
self.wrappedValue = wrappedValue
valueDidChange(publish: false)
}

/// Handles changing a value. If `publish` is `false` the change won't be
/// published, but if the wrapped value is ``ObservableObject`` the new
/// upstream publisher will still get relinked.
public func valueDidChange(publish: Bool = true) {
if publish {
didChange.send()
}

if let upstream = wrappedValue as? ObservableObject {
upstreamLinkCancellable?.cancel()
upstreamLinkCancellable = didChange.link(toUpstream: upstream.didChange)
}
}
}

extension Published: Codable where Value: Codable {
public convenience init(from decoder: Decoder) throws {
self.init(wrappedValue: try Value(from: decoder))
}

public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(to: encoder)
}
}

@available(*, deprecated, message: "Replace Observed with Published")
public typealias Observed = Published
2 changes: 1 addition & 1 deletion Sources/SwiftCrossUI/State/State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public struct State<Value>: DynamicProperty, StateProperty {
public init(wrappedValue initialValue: Value) {
storage = Storage(initialValue)

if let initialValue = initialValue as? Observable {
if let initialValue = initialValue as? ObservableObject {
_ = didChange.link(toUpstream: initialValue.didChange)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftCrossUI/SwiftCrossUI.docc/SwiftCrossUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ Objects that are read from and/or written to as part of your app.

- ``State``
- ``Binding``
- ``Observable``
- ``Observed``
- ``ObservableObject``
- ``Published``
- ``Publisher``
- ``Cancellable``

Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend> {
// Update the view and its children when state changes (children are always updated first).
let mirror = Mirror(reflecting: view)
for property in mirror.children {
if property.label == "state" && property.value is Observable {
if property.label == "state" && property.value is ObservableObject {
print(
"""
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftCrossUI/_App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class _App<AppRoot: App> {

let mirror = Mirror(reflecting: self.app)
for property in mirror.children {
if property.label == "state" && property.value is Observable {
if property.label == "state" && property.value is ObservableObject {
print(
"""
Expand Down

0 comments on commit 63fde71

Please sign in to comment.