Skip to content

Commit

Permalink
Implementation for ObservableObject with Mirror
Browse files Browse the repository at this point in the history
  • Loading branch information
kateinoigakukun committed Dec 22, 2020
1 parent 1fbf688 commit 65805ae
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 18 deletions.
57 changes: 43 additions & 14 deletions Sources/OpenCombine/ObservableObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,55 @@ public protocol ObservableObject: AnyObject {
var objectWillChange: ObjectWillChangePublisher { get }
}

extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
// swiftlint:disable 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.
@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()
var installedPublisher: ObservableObjectPublisher?
let reflection = Mirror(reflecting: self)
for (_, property) in reflection.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.
}
return installedPublisher!
}
#endif
// swiftlint:enable let_var_whitespace
}

#endif

/// A publisher that publishes changes from observable objects.
public final class ObservableObjectPublisher: Publisher {

Expand Down
15 changes: 11 additions & 4 deletions Sources/OpenCombine/Published.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,15 @@ public struct Published<Value> {
case value(Value)
case publisher(Publisher)
}
@propertyWrapper
private class Box {
var wrappedValue: Storage
init(wrappedValue: Storage) {
self.wrappedValue = wrappedValue
}
}

private var storage: Storage
@Box private var storage: Storage

internal var objectWillChange: ObservableObjectPublisher? {
get {
Expand All @@ -119,7 +126,7 @@ public struct Published<Value> {
return publisher.subject.objectWillChange
}
}
set {
nonmutating set {
projectedValue.subject.objectWillChange = newValue
}
}
Expand All @@ -145,14 +152,14 @@ 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.
///
/// The `projectedValue` is the property accessed with the `$` operator.
public var projectedValue: Publisher {
mutating get {
get {
switch storage {
case .value(let value):
let publisher = Publisher(value)
Expand Down
106 changes: 106 additions & 0 deletions Tests/OpenCombineTests/ObservableObjectTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// 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 {

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(())])
}
}

@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

0 comments on commit 65805ae

Please sign in to comment.