Repository: broadwaylamb/OpenCombine (swift)

Issue:
@Published property wrapper doesn't work
I see in project's release notes that `@Published` doesn't work yet, which is confusing as it's available in the list of public module declarations. In addition, the property is not recognized even when `import OpenCombine` is present in the file. Would be great to know if there are some specific reasons for that, otherwise, just creating an issue to track this. Maybe someone could provide more info or directions on the best way to fix this 🙏

Pull request:
#97 gets too big, so I decided to fix `@Published` in a separate PR.

Fixes #102.

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5f8220544..6fce73127 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -37,7 +37,10 @@ jobs:
                    -scheme OpenCombine-Package \
                    -sdk macosx10.15 \
                    -derivedDataPath DerivedData \
+              | tee xcodebuild_build-for-testing.log \
               | xcpretty
+      - store_artifacts:
+          path: xcodebuild_build-for-testing.log
       - run:
           name: Testing on macOS 10.15.0 with xcodebuild
           command: |
@@ -46,7 +49,10 @@ jobs:
                    -scheme OpenCombine-Package \
                    -sdk macosx10.15 \
                    -derivedDataPath DerivedData \
+              | tee xcodebuild_test-without-building.log \
               | xcpretty --report junit -o build/reports/results.xml
+      - store_artifacts:
+          path: xcodebuild_test-without-building.log
       - store_test_results:
           path: build/reports
       - run:
@@ -72,7 +78,10 @@ jobs:
                    -scheme OpenCombine-Package \
                    -destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
                    -derivedDataPath DerivedData \
+              | tee xcodebuild_build-for-testing.log \
               | xcpretty
+      - store_artifacts:
+          path: xcodebuild_build-for-testing.log
       - run:
           name: Testing against Combine on iOS 13.2.2 with xcodebuild
           command: |
@@ -81,7 +90,10 @@ jobs:
                    -scheme OpenCombine-Package \
                    -destination "platform=iOS Simulator,name=iPhone 11,OS=13.2.2" \
                    -derivedDataPath DerivedData \
+              | tee xcodebuild_test-without-building.log \
               | xcpretty --report junit -o build/reports/results.xml
+      - store_artifacts:
+          path: xcodebuild_test-without-building.log
       - store_test_results:
           path: build/reports
 
@@ -123,7 +135,10 @@ jobs:
                     -scheme OpenCombine-Package \
                     -destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
                     -derivedDataPath DerivedData \
+               | tee xcodebuild_build-for-testing.log \
                | xcpretty
+      - store_artifacts:
+          path: xcodebuild_build-for-testing.log
       - run:
           name: Testing on iOS 9.3 with xcodebuild
           command: |
@@ -132,7 +147,10 @@ jobs:
                    -scheme OpenCombine-Package \
                    -destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
                    -derivedDataPath DerivedData \
+              | tee xcodebuild_test-without-building.log \
               | xcpretty --report junit -o build/reports/results.xml
+      - store_artifacts:
+          path: xcodebuild_test-without-building.log
       - store_test_results:
           path: build/reports
       - run:
diff --git a/RemainingCombineInterface.swift b/RemainingCombineInterface.swift
index 413ca6dbf..e9c4f5a67 100644
--- a/RemainingCombineInterface.swift
+++ b/RemainingCombineInterface.swift
@@ -2094,68 +2094,3 @@ extension Publishers.Zip4 : Equatable where A : Equatable, B : Equatable, C : Eq
     ///   - rhs: Another value to compare.
     public static func == (lhs: Publishers.Zip4<A, B, C, D>, rhs: Publishers.Zip4<A, B, C, D>) -> Bool
 }
-
-/// A type of object with a publisher that emits before the object has changed.
-///
-/// By default an `ObservableObject` will synthesize an `objectWillChange`
-/// publisher that emits before any of its `@Published` properties changes:
-///
-///     class Contact: ObservableObject {
-///         @Published var name: String
-///         @Published var age: Int
-///
-///         init(name: String, age: Int) {
-///             self.name = name
-///             self.age = age
-///         }
-///
-///         func haveBirthday() -> Int {
-///             age += 1
-///             return age
-///         }
-///     }
-///
-///     let john = Contact(name: "John Appleseed", age: 24)
-///     john.objectWillChange.sink { _ in print("\(john.age) will change") }
-///     print(john.haveBirthday())
-///     // Prints "24 will change"
-///     // Prints "25"
-///
-public protocol ObservableObject : AnyObject {
-
-    /// The type of publisher that emits before the object has changed.
-    associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
-
-    /// A publisher that emits before the object has changed.
-    var objectWillChange: Self.ObjectWillChangePublisher { get }
-}
-
-extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
-
-    /// A publisher that emits before the object has changed.
-    public var objectWillChange: ObservableObjectPublisher { get }
-}
-
-/// The default publisher of an `ObservableObject`.
-final public class ObservableObjectPublisher : Publisher {
-
-    /// The kind of values published by this publisher.
-    public typealias Output = Void
-
-    /// The kind of errors this publisher might publish.
-    ///
-    /// Use `Never` if this `Publisher` does not publish errors.
-    public typealias Failure = Never
-
-    public init()
-
-    /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
-    ///
-    /// - SeeAlso: `subscribe(_:)`
-    /// - Parameters:
-    ///     - subscriber: The subscriber to attach to this `Publisher`.
-    ///                   once attached it can begin to receive values.
-    final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output
-
-    final public func send()
-}
diff --git a/Sources/OpenCombine/ObservableObject.swift b/Sources/OpenCombine/ObservableObject.swift
new file mode 100644
index 000000000..d8133a464
--- /dev/null
+++ b/Sources/OpenCombine/ObservableObject.swift
@@ -0,0 +1,85 @@
+//
+//  ObservableObject.swift
+//  
+//
+//  Created by Sergej Jaskiewicz on 08/09/2019.
+//
+
+/// A type of object with a publisher that emits before the object has changed.
+///
+/// By default an `ObservableObject` will synthesize an `objectWillChange`
+/// publisher that emits before any of its `@Published` properties changes:
+///
+///     class Contact : ObservableObject {
+///         @Published var name: String
+///         @Published var age: Int
+///
+///         init(name: String, age: Int) {
+///             self.name = name
+///             self.age = age
+///         }
+///
+///         func haveBirthday() -> Int {
+///             age += 1
+///         }
+///     }
+///
+///     let john = Contact(name: "John Appleseed", age: 24)
+///     john.objectWillChange.sink { _ in print("will change") }
+///     print(john.haveBirthday)
+///     // Prints "will change"
+///     // Prints "25"
+///
+public protocol ObservableObject: AnyObject {
+
+    /// The type of publisher that emits before the object has changed.
+    associatedtype ObjectWillChangePublisher: Publisher = ObservableObjectPublisher
+        where ObjectWillChangePublisher.Failure == Never
+
+    /// A publisher that emits before the object has changed.
+    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
+}
+
+/// The default publisher of an `ObservableObject`.
+public final class ObservableObjectPublisher: Publisher {
+
+    public typealias Output = Void
+
+    public typealias Failure = Never
+
+    private let subject: PassthroughSubject<Void, Never>
+
+    public init() {
+        subject = .init()
+    }
+
+    public func receive<Downstream: Subscriber>(subscriber: Downstream)
+        where Downstream.Input == Void, Downstream.Failure == Never
+    {
+        subject.subscribe(subscriber)
+    }
+
+    public func send() {
+        subject.send()
+    }
+}
diff --git a/Sources/OpenCombine/Published.swift b/Sources/OpenCombine/Published.swift
index 038ce52cd..510d7959e 100644
--- a/Sources/OpenCombine/Published.swift
+++ b/Sources/OpenCombine/Published.swift
@@ -14,16 +14,18 @@
 /// of the property first.
 /// Note that the `@Published` property is class-constrained.
 /// Use it with properties of classes, not with non-class types like structures.
+@available(swift, introduced: 5.1)
 @propertyWrapper
 public struct Published<Value> {
 
-    /// Initialize the storage of the Published
-    /// property as well as the corresponding `Publisher`.
+    /// Initialize the storage of the `Published` property as well as the corresponding
+    /// `Publisher`.
     public init(initialValue: Value) {
-        value = initialValue
+        self.init(wrappedValue: initialValue)
     }
 
-    @available(*, unavailable)
+    /// Initialize the storage of the `Published` property as well as the corresponding
+    /// `Publisher`.
     public init(wrappedValue: Value) {
         value = wrappedValue
     }
@@ -31,21 +33,10 @@ public struct Published<Value> {
     /// A publisher for properties marked with the `@Published` attribute.
     public struct Publisher: OpenCombine.Publisher {
 
-        /// The kind of values published by this publisher.
         public typealias Output = Value
 
-        /// The kind of errors this publisher might publish.
-        ///
-        /// Use `Never` if this `Publisher` does not publish errors.
         public typealias Failure = Never
 
-        /// This function is called to attach the specified
-        /// `Subscriber` to this `Publisher` by `subscribe(_:)`
-        ///
-        /// - SeeAlso: `subscribe(_:)`
-        /// - Parameters:
-        ///     - subscriber: The subscriber to attach to this `Publisher`.
-        ///                   once attached it can begin to receive values.
         public func receive<Downstream: Subscriber>(subscriber: Downstream)
             where Downstream.Input == Value, Downstream.Failure == Never
         {
@@ -61,8 +52,12 @@ public struct Published<Value> {
 
     private var value: Value
 
-    /// The property that can be accessed with the
-    /// `$` syntax and allows access to the `Publisher`
+    private var publisher: Publisher?
+
+    internal var objectWillChange: ObservableObjectPublisher?
+
+    /// The property that can be accessed with the `$` syntax and allows access to
+    /// the `Publisher`
     public var projectedValue: Publisher {
         mutating get {
             if let publisher = publisher {
@@ -74,28 +69,35 @@ public struct Published<Value> {
         }
     }
 
-    @available(*, unavailable, message:
-        "@Published is only available on properties of classes")
-
+    // swiftlint:disable let_var_whitespace
+    @available(*, unavailable, message: """
+               @Published is only available on properties of classes
+               """)
     public var wrappedValue: Value {
-        get { value }
-        set {
-            value = newValue
-            publisher?.subject.value = newValue
-        }
+        get { fatalError() }
+        set { fatalError() } // swiftlint:disable:this unused_setter_value
     }
+    // swiftlint:enable let_var_whitespace
 
-    private var publisher: Publisher?
-
-    @available(*, unavailable, message:
-        "This subscript is unavailable in OpenCombine yet")
     public static subscript<EnclosingSelf: AnyObject>(
         _enclosingInstance object: EnclosingSelf,
         wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
         storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Published<Value>>
     ) -> Value {
-        get { fatalError() }
-        set { fatalError() }
+        get {
+            return object[keyPath: storageKeyPath].value
+        }
+        set {
+            object[keyPath: storageKeyPath].objectWillChange?.send()
+            object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
+            object[keyPath: storageKeyPath].value = newValue
+        }
+        // TODO: Benchmark and explore a possibility to use _modify
     }
 }
-#endif
+#else
+
+@available(swift, introduced: 5.1)
+public typealias Published = Never
+
+#endif // swift(>=5.1)


Test patch:
diff --git a/Tests/OpenCombineTests/Helpers/TrackingSubscriber.swift b/Tests/OpenCombineTests/Helpers/TrackingSubscriber.swift
index 1b50361a1..bc67db553 100644
--- a/Tests/OpenCombineTests/Helpers/TrackingSubscriber.swift
+++ b/Tests/OpenCombineTests/Helpers/TrackingSubscriber.swift
@@ -195,6 +195,18 @@ extension TrackingSubscriberBase where Value: Equatable {
     }
 }
 
+@available(macOS 10.15, iOS 13.0, *)
+extension TrackingSubscriberBase where Value == Void {
+    func assertHistoryEqual(_ expected: [Event],
+                            file: StaticString = #file,
+                            line: UInt = #line) {
+        assertHistoryEqual(expected,
+                           valueComparator: { _, _ in true },
+                           file: file,
+                           line: line)
+    }
+}
+
 @available(macOS 10.15, iOS 13.0, *)
 extension TrackingSubscriberBase.Event {
     func isEqual(to other: TrackingSubscriberBase<Value, Failure>.Event,
@@ -222,12 +234,23 @@ extension TrackingSubscriberBase.Event {
 @available(macOS 10.15, iOS 13.0, *)
 extension TrackingSubscriberBase.Event: Equatable where Value: Equatable {
 
-    static func == (lhs: TrackingSubscriberBase<Value, Failure>.Event,
-                    rhs: TrackingSubscriberBase<Value, Failure>.Event) -> Bool {
+    static func == (lhs: TrackingSubscriberBase.Event,
+                    rhs: TrackingSubscriberBase.Event) -> Bool {
         return lhs.isEqual(to: rhs, valueComparator: ==)
     }
 }
 
+@available(macOS 10.15, iOS 13.0, *)
+extension TrackingSubscriberBase.Event where Value == Void {
+
+    static var signal: TrackingSubscriberBase.Event { return .value(()) }
+
+    static func == (lhs: TrackingSubscriberBase.Event,
+                    rhs: TrackingSubscriberBase.Event) -> Bool {
+        return lhs.isEqual(to: rhs, valueComparator: { _, _ in true })
+    }
+}
+
 @available(macOS 10.15, iOS 13.0, *)
 typealias TrackingSubject<Output: Equatable> = TrackingSubjectBase<Output, TestingError>
 
diff --git a/Tests/OpenCombineTests/ObservableObjectPublisherTests.swift b/Tests/OpenCombineTests/ObservableObjectPublisherTests.swift
new file mode 100644
index 000000000..34ae2d3f3
--- /dev/null
+++ b/Tests/OpenCombineTests/ObservableObjectPublisherTests.swift
@@ -0,0 +1,76 @@
+//
+//  ObservableObjectPublisherTests.swift
+//  
+//
+//  Created by Sergej Jaskiewicz on 26.11.2019.
+//
+
+import XCTest
+
+#if OPENCOMBINE_COMPATIBILITY_TEST
+import Combine
+#else
+import OpenCombine
+#endif
+
+@available(macOS 10.15, iOS 13.0, *)
+final class ObservableObjectPublisherTests: XCTestCase {
+
+    func testBasicBehavior() {
+        let publisher = ObservableObjectPublisher()
+        var downstreamSubscription1: Subscription?
+        let tracking1 = TrackingSubscriberBase<Void, Never>(
+            receiveSubscription: { downstreamSubscription1 = $0 }
+        )
+        publisher.subscribe(tracking1)
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
+        downstreamSubscription1?.request(.max(1))
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
+        publisher.send()
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
+                                      .signal])
+        publisher.send()
+        publisher.send()
+        downstreamSubscription1?.request(.max(3))
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
+                                      .signal])
+        publisher.send()
+        publisher.send()
+        publisher.send()
+        publisher.send()
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
+                                      .signal,
+                                      .signal,
+                                      .signal,
+                                      .signal])
+        downstreamSubscription1?.request(.unlimited)
+
+        let tracking2 = TrackingSubscriberBase<Void, Never>(
+            receiveSubscription: { $0.request(.unlimited) }
+        )
+        publisher.subscribe(tracking2)
+        tracking2.assertHistoryEqual([.subscription("PassthroughSubject")])
+
+        publisher.send()
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
+                                      .signal,
+                                      .signal,
+                                      .signal,
+                                      .signal,
+                                      .signal])
+        tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
+                                      .signal])
+
+        downstreamSubscription1?.cancel()
+        publisher.send()
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject"),
+                                      .signal,
+                                      .signal,
+                                      .signal,
+                                      .signal,
+                                      .signal])
+        tracking2.assertHistoryEqual([.subscription("PassthroughSubject"),
+                                      .signal,
+                                      .signal])
+    }
+}
diff --git a/Tests/OpenCombineTests/PublishedTests.swift b/Tests/OpenCombineTests/PublishedTests.swift
new file mode 100644
index 000000000..e5124282f
--- /dev/null
+++ b/Tests/OpenCombineTests/PublishedTests.swift
@@ -0,0 +1,111 @@
+//
+//  PublishedTests.swift
+//  
+//
+//  Created by Sergej Jaskiewicz on 08/09/2019.
+//
+
+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 PublishedTests: XCTestCase {
+
+    func testBasicBehavior() {
+        let testObject = TestObject()
+        var downstreamSubscription1: Subscription?
+        let tracking1 = TrackingSubscriberBase<Int, Never>(
+            receiveSubscription: { downstreamSubscription1 = $0 }
+        )
+        testObject.$state.subscribe(tracking1)
+        XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject")])
+        downstreamSubscription1?.request(.max(2))
+        XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
+                                          .value(0)])
+        testObject.state += 1
+        testObject.state += 2
+        testObject.state += 3
+        XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
+                                          .value(0),
+                                          .value(1)])
+        downstreamSubscription1?.request(.max(10))
+        XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
+                                          .value(0),
+                                          .value(1),
+                                          .value(6)])
+
+        let tracking2 = TrackingSubscriberBase<Int, Never>(
+            receiveSubscription: { $0.request(.unlimited) }
+        )
+        testObject.$state.subscribe(tracking2)
+        XCTAssertEqual(tracking2.history, [.subscription("CurrentValueSubject"),
+                                           .value(6)])
+
+        testObject.state = 42
+        XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
+                                           .value(0),
+                                           .value(1),
+                                           .value(6),
+                                           .value(42)])
+        XCTAssertEqual(tracking2.history, [.subscription("CurrentValueSubject"),
+                                           .value(6),
+                                           .value(42)])
+
+        downstreamSubscription1?.cancel()
+        testObject.state = -1
+        XCTAssertEqual(tracking1.history, [.subscription("CurrentValueSubject"),
+                                           .value(0),
+                                           .value(1),
+                                           .value(6),
+                                           .value(42)])
+        XCTAssertEqual(tracking2.history, [.subscription("CurrentValueSubject"),
+                                           .value(6),
+                                           .value(42),
+                                           .value(-1)])
+    }
+
+    func testObservableObjectWithCustomObjectWillChange() {
+        let testObject = TestObject()
+        var downstreamSubscription: Subscription?
+        let tracking1 = TrackingSubscriberBase<Void, Never>(
+            receiveSubscription: { downstreamSubscription = $0 }
+        )
+        testObject.objectWillChange.subscribe(tracking1)
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
+        downstreamSubscription?.request(.max(2))
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
+        testObject.state = 100
+        tracking1.assertHistoryEqual([.subscription("PassthroughSubject")])
+    }
+}
+
+@available(macOS 10.15, iOS 13.0, *)
+private final class TestObject: ObservableObject {
+
+    let objectWillChange = ObservableObjectPublisher()
+
+    @Published var state: Int
+
+    init() {
+        _state = Published(initialValue: 0)
+    }
+}
+
+#endif