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

Implementation for ObservableObject with Mirror #1

Merged
merged 7 commits into from
Jan 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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