Fix @Published (#112)
This commit is contained in:
committed by
GitHub
parent
668c292245
commit
7c5a76cf2b
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user