Fix @Published (#112)

This commit is contained in:
Sergej Jaskiewicz
2019-11-26 17:46:01 +03:00
committed by GitHub
parent 668c292245
commit 7c5a76cf2b
7 changed files with 349 additions and 99 deletions
+18
View File
@@ -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:
-65
View File
@@ -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()
}
@@ -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()
}
}
+34 -32
View File
@@ -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)
@@ -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>
@@ -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])
}
}
+111
View File
@@ -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