Skip to content
This repository has been archived by the owner on Nov 30, 2021. It is now read-only.

Commit

Permalink
Merge pull request #1 from TokamakUI/observableobject-mirror
Browse files Browse the repository at this point in the history
Implementation for ObservableObject with Mirror
  • Loading branch information
MaxDesiatov authored Jan 11, 2021
2 parents bccff3e + 63e4acc commit eea5e70
Show file tree
Hide file tree
Showing 8 changed files with 530 additions and 104 deletions.
4 changes: 1 addition & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@ let package = Package(
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
],
dependencies: [.package(url: "https://github.com/MaxDesiatov/Runtime.git", from: "2.1.2")],
targets: [
.target(name: "COpenCombineHelpers"),
.target(
name: "OpenCombine",
dependencies: [
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.product(name: "Runtime", package: "Runtime", condition: .when(platforms: [.wasi])),
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"Publishers/Publishers.Encode.swift.gyb",
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.11.0")
],
targets: [
.target(name: "MyAwesomePackage", dependencies: ["OpenCombine",
"OpenCombineDispatch",
"OpenCombineFoundation"])
.target(
name: "MyAwesomePackage",
dependencies: [
"OpenCombine",
.product(name: "OpenCombineFoundation", package: "OpenCombine"),
.product(name: "OpenCombineDispatch", package: "OpenCombine")
]
),
]
```

Expand Down
113 changes: 32 additions & 81 deletions Sources/OpenCombine/ObservableObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
// Created by Sergej Jaskiewicz on 08/09/2019.
//

#if canImport(Runtime)
import Runtime
#endif

/// A type of object with a publisher that emits before the object has changed.
///
/// By default an `ObservableObject` synthesizes an `objectWillChange` publisher that
Expand Down Expand Up @@ -46,103 +42,58 @@ public protocol ObservableObject: AnyObject {
var objectWillChange: ObjectWillChangePublisher { get }
}

#if swift(>=5.1) && canImport(Runtime)
private protocol _ObservableObjectProperty {
var objectWillChange: ObservableObjectPublisher? { get set }
}

extension _ObservableObjectProperty {

fileprivate static func installPublisher(
_ publisher: ObservableObjectPublisher,
on publishedStorage: UnsafeMutableRawPointer
) {
// It is safe to call assumingMemoryBound here because we know for sure
// that the actual type of the pointee is Self.
publishedStorage
.assumingMemoryBound(to: Self.self)
.pointee
.objectWillChange = publisher
}

fileprivate static func getPublisher(
from publishedStorage: UnsafeMutableRawPointer
) -> ObservableObjectPublisher? {
// It is safe to call assumingMemoryBound here because we know for sure
// that the actual type of the pointee is Self.
return publishedStorage
.assumingMemoryBound(to: Self.self)
.pointee
.objectWillChange
}
var objectWillChange: ObservableObjectPublisher? { get nonmutating set }
}

#if swift(>=5.1)
extension Published: _ObservableObjectProperty {}
#endif

extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
// swiftlint:disable let_var_whitespace
#if swift(>=5.1)

/// A publisher that emits before the object has changed.
#if canImport(Runtime)
public var objectWillChange: ObservableObjectPublisher {
var installedPublisher: ObservableObjectPublisher?
let info = try! typeInfo(of: Self.self)
for property in info.properties {
let storage = Unmanaged
.passUnretained(self)
.toOpaque()
.advanced(by: property.offset)

guard let fieldType = property.type as? _ObservableObjectProperty.Type else {
// Visit other fields until we meet a @Published field
continue
}
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 = fieldType.getPublisher(from: storage) {
installedPublisher = alreadyInstalledPublisher
// Don't visit other fields, as all @Published fields
// already have a publisher installed.
break
}
// 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 {
// 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
}
let publisher = ObservableObjectPublisher()
installedPublisher = publisher
return publisher
}

fieldType.installPublisher(lazilyCreatedPublisher, on: storage)
property.objectWillChange = lazilyCreatedPublisher

// Continue visiting other fields.
// Continue visiting other fields.
}
reflection = aClass.superclassMirror
}
return installedPublisher ?? ObservableObjectPublisher()
}
#else
@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")
}
#endif
#else
public var objectWillChange: ObservableObjectPublisher {
return ObservableObjectPublisher()
}
#endif
// swiftlint:enable let_var_whitespace
}

#endif

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

Expand Down
16 changes: 12 additions & 4 deletions Sources/OpenCombine/Published.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,16 @@ public struct Published<Value> {
case value(Value)
case publisher(Publisher)
}
@propertyWrapper
private 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 {
Expand All @@ -119,7 +127,7 @@ public struct Published<Value> {
return publisher.subject.objectWillChange
}
}
set {
nonmutating set {
projectedValue.subject.objectWillChange = newValue
}
}
Expand All @@ -145,14 +153,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
33 changes: 20 additions & 13 deletions Sources/OpenCombine/Publishers/Publishers.FlatMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,17 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Child.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
{
let inner = Inner(downstream: subscriber,
let outer = Outer(downstream: subscriber,
maxPublishers: maxPublishers,
map: transform)
subscriber.receive(subscription: inner)
upstream.subscribe(inner)
subscriber.receive(subscription: outer)
upstream.subscribe(outer)
}
}
}

extension Publishers.FlatMap {
private final class Inner<Downstream: Subscriber>
private final class Outer<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
Expand Down Expand Up @@ -243,7 +243,7 @@ extension Publishers.FlatMap {
subscription.request(maxPublishers)
}

fileprivate func receive(_ input: Upstream.Output) -> Subscribers.Demand {
fileprivate func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
let cancelledOrCompleted = self.cancelledOrCompleted
lock.unlock()
Expand All @@ -260,9 +260,9 @@ extension Publishers.FlatMap {
return .none
}

fileprivate func receive(completion: Subscribers.Completion<Child.Failure>) {
outerSubscription = nil
fileprivate func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
outerSubscription = nil
outerFinished = true
switch completion {
case .finished:
Expand All @@ -272,6 +272,8 @@ extension Publishers.FlatMap {
let wasAlreadyCancelledOrCompleted = cancelledOrCompleted
cancelledOrCompleted = true
for (_, subscription) in subscriptions {
// Cancelling subscriptions with the lock acquired. Not good,
// but that's what Combine does. This code path is tested.
subscription.cancel()
}
subscriptions = [:]
Expand Down Expand Up @@ -354,16 +356,21 @@ extension Publishers.FlatMap {

fileprivate func cancel() {
lock.lock()
if cancelledOrCompleted {
lock.unlock()
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
lock.unlock()
for (_, subscription) in subscriptions {
subscription.cancel()
}
// Combine doesn't acquire the lock here. Weird.
// Combine doesn't acquire outerLock here. Weird.
outerSubscription?.cancel()
outerSubscription = nil
}

// MARK: - Reflection
Expand Down Expand Up @@ -471,9 +478,9 @@ extension Publishers.FlatMap {
private func releaseLockThenSendCompletionDownstreamIfNeeded(
outerFinished: Bool
) -> Bool {
#if DEBUG
#if DEBUG
lock.assertOwner() // Sanity check
#endif
#endif
if !cancelledOrCompleted && outerFinished && buffer.isEmpty &&
subscriptions.count + pendingSubscriptions == 0 {
cancelledOrCompleted = true
Expand All @@ -495,10 +502,10 @@ extension Publishers.FlatMap {
CustomReflectable,
CustomPlaygroundDisplayConvertible {
private let index: SubscriptionIndex
private let inner: Inner
private let inner: Outer
fileprivate let combineIdentifier = CombineIdentifier()

fileprivate init(index: SubscriptionIndex, inner: Inner) {
fileprivate init(index: SubscriptionIndex, inner: Outer) {
self.index = index
self.inner = inner
}
Expand Down
9 changes: 9 additions & 0 deletions Tests/OpenCombineTests/Helpers/AssertCrashes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,13 @@ extension XCTest {
}
#endif // !Xcode && !os(iOS) && !os(watchOS) && !os(tvOS) && !WASI
}

@available(macOS 10.13, iOS 8.0, *)
func assertCrashesOnDarwin(within body: () -> Void) {
#if canImport(Darwin) && OPENCOMBINE_COMPATIBILITY_TEST
assertCrashes(within: body)
#else
body()
#endif
}
}
Loading

0 comments on commit eea5e70

Please sign in to comment.