Implementation for ObservableObject with Mirror (#201)
A temporary implementation until we implement the proper type metadata introspection.
This commit is contained in:
@@ -42,26 +42,58 @@ public protocol ObservableObject: AnyObject {
|
||||
var objectWillChange: ObjectWillChangePublisher { get }
|
||||
}
|
||||
|
||||
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||
// swiftlint:disable let_var_whitespace
|
||||
#if swift(>=5.1)
|
||||
/// A publisher that emits before the object has changed.
|
||||
@available(*, unavailable, message: """
|
||||
The default implementation of objectWillChange is not available yet. \
|
||||
It's being worked on in \
|
||||
https://github.com/broadwaylamb/OpenCombine/pull/97
|
||||
""")
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
fatalError("unimplemented")
|
||||
}
|
||||
#else
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
return ObservableObjectPublisher()
|
||||
}
|
||||
#endif
|
||||
// swiftlint:enable let_var_whitespace
|
||||
private protocol _ObservableObjectProperty {
|
||||
var objectWillChange: ObservableObjectPublisher? { get nonmutating set }
|
||||
}
|
||||
|
||||
#if swift(>=5.1)
|
||||
extension Published: _ObservableObjectProperty {}
|
||||
|
||||
extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||
|
||||
/// A publisher that emits before the object has changed.
|
||||
public var objectWillChange: ObservableObjectPublisher {
|
||||
var installedPublisher: ObservableObjectPublisher?
|
||||
var reflection: Mirror? = Mirror(reflecting: self)
|
||||
while let aClass = reflection {
|
||||
for (_, property) in aClass.children {
|
||||
guard let property = property as? _ObservableObjectProperty else {
|
||||
// Visit other fields until we meet a @Published field
|
||||
continue
|
||||
}
|
||||
|
||||
// Now we know that the field is @Published.
|
||||
if let alreadyInstalledPublisher = property.objectWillChange {
|
||||
installedPublisher = alreadyInstalledPublisher
|
||||
// Don't visit other fields, as all @Published fields
|
||||
// already have a publisher installed.
|
||||
break
|
||||
}
|
||||
|
||||
// Okay, this field doesn't have a publisher installed.
|
||||
// This means that other fields don't have it either
|
||||
// (because we install it only once and fields can't be added at runtime).
|
||||
var lazilyCreatedPublisher: ObjectWillChangePublisher {
|
||||
if let publisher = installedPublisher {
|
||||
return publisher
|
||||
}
|
||||
let publisher = ObservableObjectPublisher()
|
||||
installedPublisher = publisher
|
||||
return publisher
|
||||
}
|
||||
|
||||
property.objectWillChange = lazilyCreatedPublisher
|
||||
|
||||
// Continue visiting other fields.
|
||||
}
|
||||
reflection = aClass.superclassMirror
|
||||
}
|
||||
return installedPublisher ?? ObservableObjectPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// A publisher that publishes changes from observable objects.
|
||||
public final class ObservableObjectPublisher: Publisher {
|
||||
|
||||
|
||||
@@ -107,8 +107,16 @@ public struct Published<Value> {
|
||||
case value(Value)
|
||||
case publisher(Publisher)
|
||||
}
|
||||
@propertyWrapper
|
||||
private final class Box {
|
||||
var wrappedValue: Storage
|
||||
|
||||
private var storage: Storage
|
||||
init(wrappedValue: Storage) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
@Box private var storage: Storage
|
||||
|
||||
internal var objectWillChange: ObservableObjectPublisher? {
|
||||
get {
|
||||
@@ -119,8 +127,8 @@ public struct Published<Value> {
|
||||
return publisher.subject.objectWillChange
|
||||
}
|
||||
}
|
||||
set {
|
||||
projectedValue.subject.objectWillChange = newValue
|
||||
nonmutating set {
|
||||
getPublisher().subject.objectWillChange = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +153,7 @@ public struct Published<Value> {
|
||||
///
|
||||
/// - Parameter initialValue: The publisher's initial value.
|
||||
public init(wrappedValue: Value) {
|
||||
storage = .value(wrappedValue)
|
||||
_storage = Box(wrappedValue: .value(wrappedValue))
|
||||
}
|
||||
|
||||
/// The property for which this instance exposes a publisher.
|
||||
@@ -153,14 +161,7 @@ public struct Published<Value> {
|
||||
/// The `projectedValue` is the property accessed with the `$` operator.
|
||||
public var projectedValue: Publisher {
|
||||
mutating get {
|
||||
switch storage {
|
||||
case .value(let value):
|
||||
let publisher = Publisher(value)
|
||||
storage = .publisher(publisher)
|
||||
return publisher
|
||||
case .publisher(let publisher):
|
||||
return publisher
|
||||
}
|
||||
return getPublisher()
|
||||
}
|
||||
set { // swiftlint:disable:this unused_setter_value
|
||||
switch storage {
|
||||
@@ -172,6 +173,17 @@ public struct Published<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: This method can mutate `storage`
|
||||
internal func getPublisher() -> Publisher {
|
||||
switch storage {
|
||||
case .value(let value):
|
||||
let publisher = Publisher(value)
|
||||
storage = .publisher(publisher)
|
||||
return publisher
|
||||
case .publisher(let publisher):
|
||||
return publisher
|
||||
}
|
||||
}
|
||||
// swiftlint:disable let_var_whitespace
|
||||
@available(*, unavailable, message: """
|
||||
@Published is only available on properties of classes
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
//
|
||||
// ObservableObjectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by kateinoigakukun on 2020/12/22.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if swift(>=5.1)
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias Published = Combine.Published
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private typealias ObservableObject = Combine.ObservableObject
|
||||
#else
|
||||
import OpenCombine
|
||||
|
||||
private typealias Published = OpenCombine.Published
|
||||
|
||||
private typealias ObservableObject = OpenCombine.ObservableObject
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ObservableObjectTests: XCTestCase {
|
||||
var disposeBag = [AnyCancellable]()
|
||||
|
||||
override func tearDown() {
|
||||
disposeBag = []
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testBasicBehavior() {
|
||||
let testObject = TestObject()
|
||||
var downstreamSubscription1: Subscription?
|
||||
let tracking1 = TrackingSubscriberBase<Void, Never>(
|
||||
receiveSubscription: { downstreamSubscription1 = $0 }
|
||||
)
|
||||
|
||||
testObject.objectWillChange.subscribe(tracking1)
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
downstreamSubscription1?.request(.max(2))
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
testObject.state1 += 1
|
||||
testObject.state1 += 2
|
||||
testObject.state1 += 3
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
testObject.state2 += 1
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
downstreamSubscription1?.request(.max(10))
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
|
||||
let tracking2 = TrackingSubscriberBase<Void, Never>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
testObject.objectWillChange.subscribe(tracking2)
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher")])
|
||||
|
||||
testObject.state1 = 42
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal])
|
||||
|
||||
downstreamSubscription1?.cancel()
|
||||
testObject.state1 = -1
|
||||
|
||||
tracking1.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal,
|
||||
.signal])
|
||||
tracking2.assertHistoryEqual([.subscription("ObservableObjectPublisher"),
|
||||
.value(()),
|
||||
.value(())])
|
||||
}
|
||||
|
||||
// TODO: `objectWillChange` should return the same `ObservableObjectPublisher`
|
||||
// every time for Combine compatibility
|
||||
//
|
||||
// func testNoFields() {
|
||||
// let observableObject = NoFields()
|
||||
// let publisher1 = observableObject.objectWillChange
|
||||
// let publisher2 = observableObject.objectWillChange
|
||||
// XCTAssert(publisher1 === publisher2)
|
||||
// }
|
||||
|
||||
// func testNoPublishedFields() {
|
||||
// let observableObject = NoPublishedFields()
|
||||
// let publisher1 = observableObject.objectWillChange
|
||||
// let publisher2 = observableObject.objectWillChange
|
||||
// XCTAssert(publisher1 === publisher2)
|
||||
// }
|
||||
|
||||
func testPublishedFieldIsConstant() {
|
||||
let observableObject = PublishedFieldIsConstant()
|
||||
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2,
|
||||
"""
|
||||
Even if the Published field is a constant, a publisher \
|
||||
should be installed there.
|
||||
""")
|
||||
}
|
||||
|
||||
func testDerivedClassWithPublishedField() {
|
||||
let observableObject = ObservedDerivedWithObservedBase()
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink {
|
||||
counter += 1
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(observableObject.publishedValue0, 0)
|
||||
XCTAssertEqual(observableObject.simpleValue, "what")
|
||||
XCTAssertEqual(observableObject.subclassPublished0, 0)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 1)
|
||||
XCTAssertEqual(observableObject.subclassPublished2, 2)
|
||||
|
||||
observableObject.publishedValue0 += 5
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.publishedValue0, 5)
|
||||
|
||||
Published<String>[_enclosingInstance: observableObject,
|
||||
wrapped: \.simpleValue,
|
||||
storage: \.publishedValue1] += "???"
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.simpleValue, "what")
|
||||
|
||||
observableObject.subclassPublished0 += 3
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.subclassPublished0, 3)
|
||||
|
||||
observableObject.subclassPublished1 += 3
|
||||
|
||||
XCTAssertEqual(counter, 4)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 4)
|
||||
|
||||
observableObject.subclassPublished2 += 3
|
||||
|
||||
XCTAssertEqual(counter, 5)
|
||||
XCTAssertEqual(observableObject.subclassPublished1, 4)
|
||||
}
|
||||
|
||||
func testObjCClassSubclass() {
|
||||
let observableObject = ObjCClassSubclass()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testResilientClassSubclass() {
|
||||
let observableObject = ResilientClassSubclass()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testResilientClassSubclass2() {
|
||||
let observableObject = ResilientClassSubclass2()
|
||||
let publisher1 = observableObject.objectWillChange
|
||||
let publisher2 = observableObject.objectWillChange
|
||||
|
||||
XCTAssert(publisher1 === publisher2)
|
||||
}
|
||||
|
||||
func testGenericClass() {
|
||||
let observableObject = GenericClass(123, true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, 123)
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += 1
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, 124)
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
}
|
||||
|
||||
func testGenericSubclassOfResilientClass() {
|
||||
let observableObject = ResilientClassGenericSubclass("hello", true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, "hello")
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += "!"
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, "hello!")
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
}
|
||||
|
||||
func testGenericSubclassOfResilientClass2() {
|
||||
let observableObject = ResilientClassGenericSubclass2("hello", true)
|
||||
|
||||
var counter = 0
|
||||
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value1, "hello")
|
||||
XCTAssertEqual(observableObject.value2, true)
|
||||
|
||||
observableObject.value1 += "!"
|
||||
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value1, "hello!")
|
||||
|
||||
observableObject.value2.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value2, false)
|
||||
|
||||
observableObject.value3.toggle()
|
||||
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.value3, true)
|
||||
}
|
||||
|
||||
func testObservableDerivedWithNonObservableBase() {
|
||||
let observableObject = ObservedDerivedWithNonObservedBase()
|
||||
var counter = 0
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue0, 10)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue1, .pi)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue2,
|
||||
"Asuka is obviously the best girl.")
|
||||
XCTAssertEqual(observableObject.observedDerivedValue3, 255)
|
||||
|
||||
observableObject.nonObservedBaseValue0 -= 1
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue0, 9)
|
||||
|
||||
observableObject.nonObservedBaseValue1 *= 2
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.nonObservedBaseValue1, 2 * .pi)
|
||||
|
||||
observableObject.observedDerivedValue2 = "Nevermind."
|
||||
XCTAssertEqual(counter, 3)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue2, "Nevermind.")
|
||||
|
||||
observableObject.observedDerivedValue3 &+= 1
|
||||
XCTAssertEqual(counter, 4)
|
||||
XCTAssertEqual(observableObject.observedDerivedValue3, 0)
|
||||
}
|
||||
|
||||
func testNSObjectSubclass() {
|
||||
let observableObject = NSObjectSubclass()
|
||||
var counter = 0
|
||||
observableObject.objectWillChange.sink { counter += 1 }.store(in: &disposeBag)
|
||||
|
||||
XCTAssertEqual(counter, 0)
|
||||
XCTAssertEqual(observableObject.value0, 0)
|
||||
XCTAssertEqual(observableObject.value1, 42)
|
||||
|
||||
observableObject.value0 += 1
|
||||
XCTAssertEqual(counter, 1)
|
||||
XCTAssertEqual(observableObject.value0, 1)
|
||||
|
||||
observableObject.value1 += 1
|
||||
XCTAssertEqual(counter, 2)
|
||||
XCTAssertEqual(observableObject.value1, 43)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class NoFields: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class NoPublishedFields: ObservableObject {
|
||||
var field = NoFields()
|
||||
var int = 0
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class PublishedFieldIsConstant: ObservableObject {
|
||||
let publishedValue = Published(initialValue: 42)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ObservedBase: ObservableObject {
|
||||
@Published var publishedValue0 = 0
|
||||
var publishedValue1 = Published(initialValue: "Hello!")
|
||||
let publishedValue2 = Published(initialValue: 42)
|
||||
var simpleValue = "what"
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ObservedDerivedWithObservedBase: ObservedBase {
|
||||
@Published var subclassPublished0 = 0
|
||||
@Published var subclassPublished1 = 1
|
||||
@Published var subclassPublished2 = 2
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension NSNumber: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ObjCClassSubclass: NSObject, ObservableObject {
|
||||
@Published var published = 10
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ResilientClassSubclass: JSONDecoder, ObservableObject {
|
||||
@Published var published0 = 10
|
||||
@Published var published1 = "hello!"
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ResilientClassSubclass2: ResilientClassSubclass {
|
||||
@Published var published3 = true
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension JSONEncoder: ObservableObject {}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class GenericClass<Value1, Value2>: ObservableObject {
|
||||
@Published var value1: Value1
|
||||
@Published var value2: Value2
|
||||
|
||||
init(_ value1: Value1, _ value2: Value2) {
|
||||
self.value1 = value1
|
||||
self.value2 = value2
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class NonObservedBase {
|
||||
@Published var nonObservedBaseValue0 = 10
|
||||
@Published var nonObservedBaseValue1 = Double.pi
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ObservedDerivedWithNonObservedBase: NonObservedBase, ObservableObject {
|
||||
@Published var observedDerivedValue2 = "Asuka is obviously the best girl."
|
||||
@Published var observedDerivedValue3: UInt8 = 255
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class NSObjectSubclass: NSObject, ObservableObject {
|
||||
@Published var value0 = 0
|
||||
@Published var value1: UInt8 = 42
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private class ResilientClassGenericSubclass<Value1, Value2>
|
||||
: JSONDecoder,
|
||||
ObservableObject
|
||||
{
|
||||
@Published var value1: Value1
|
||||
@Published var value2: Value2
|
||||
|
||||
init(_ value1: Value1, _ value2: Value2) {
|
||||
self.value1 = value1
|
||||
self.value2 = value2
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class ResilientClassGenericSubclass2<Value1, Value2>
|
||||
: ResilientClassGenericSubclass<Value1, Value2>
|
||||
{
|
||||
@Published var value3 = false
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
private final class TestObject: ObservableObject {
|
||||
@Published var state1: Int
|
||||
@Published var state2: Int
|
||||
var nonPublished: Int
|
||||
|
||||
init(_ initialValue: Int = 0) {
|
||||
_state1 = Published(initialValue: initialValue)
|
||||
_state2 = Published(initialValue: initialValue)
|
||||
nonPublished = initialValue
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user