From 587e34100285b4b8f6e12a47f869a65fc4ad81f8 Mon Sep 17 00:00:00 2001 From: Raghav Ahuja Date: Sun, 5 Apr 2020 03:31:47 +0530 Subject: [PATCH] Added ObservableObject, ObservableObjectPublisher, Published --- .../Observable Object Publisher.swift | 130 ++++++++++++++++++ .../Observable Object/Observable Object.swift | 49 +++++++ .../Observable Object/Published.swift | 75 ++++++++++ 3 files changed, 254 insertions(+) create mode 100644 Sources/PublisherKit/Observable Object/Observable Object Publisher.swift create mode 100644 Sources/PublisherKit/Observable Object/Observable Object.swift create mode 100644 Sources/PublisherKit/Observable Object/Published.swift diff --git a/Sources/PublisherKit/Observable Object/Observable Object Publisher.swift b/Sources/PublisherKit/Observable Object/Observable Object Publisher.swift new file mode 100644 index 0000000..1a60546 --- /dev/null +++ b/Sources/PublisherKit/Observable Object/Observable Object Publisher.swift @@ -0,0 +1,130 @@ +// +// Observable Object Publisher.swift +// PublisherKit +// +// Created by Raghav Ahuja on 29/03/20. +// + +/// The default publisher of an `ObservableObject`. +final public class ObservableObjectPublisher: Publisher { + + public typealias Output = Void + + public typealias Failure = Never + + private let lock = Lock() + + private var connections = Set() + + public init() { } + + final public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { + let inner = Inner(downstream: subscriber, parent: self) + + lock.lock() + connections.insert(inner) + lock.unlock() + + subscriber.receive(subscription: inner) + } + + final public func send() { + lock.lock() + let connections = self.connections + lock.unlock() + + connections.forEach { (connection) in + connection.send() + } + } + + private func remove(_ conduit: Conduit) { + lock.lock() + connections.remove(conduit) + lock.unlock() + } +} + +extension ObservableObjectPublisher { + + private class Conduit: Hashable { + + func send() { + /* abstract method to be overrided by Inner subclass. */ + } + + static func == (lhs: Conduit, rhs: Conduit) -> Bool { + return lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + } + + private final class Inner: Conduit, Subscription, CustomStringConvertible, CustomPlaygroundDisplayConvertible, CustomReflectable where Output == Downstream.Input, Failure == Downstream.Failure { + + private let downstream: Downstream + private var parent: ObservableObjectPublisher? + + private let lock = Lock() + private let downstreamLock = RecursiveLock() + + private enum State { + case awaiting + case subscribed + case terminated + } + + private var state: State = .awaiting + + init(downstream: Downstream, parent: ObservableObjectPublisher) { + self.downstream = downstream + self.parent = parent + } + + override func send() { + lock.lock() + let state = self.state + lock.unlock() + + guard state == .subscribed else { return } + + downstreamLock.lock() + _ = downstream.receive() + downstreamLock.unlock() + } + + func request(_ demand: Subscribers.Demand) { + lock.lock() + guard state == .awaiting else { lock.unlock(); return } + state = .subscribed + lock.unlock() + } + + func cancel() { + lock.lock() + state = .terminated + lock.unlock() + + parent?.remove(self) + parent = nil + } + + var description: String { + "ObservableObjectPublisher" + } + + var playgroundDescription: Any { + description + } + + var customMirror: Mirror { + let children: [Mirror.Child] = [ + ("downstream", downstream) + ] + + return Mirror(self, children: children) + } + } +} diff --git a/Sources/PublisherKit/Observable Object/Observable Object.swift b/Sources/PublisherKit/Observable Object/Observable Object.swift new file mode 100644 index 0000000..edcb45e --- /dev/null +++ b/Sources/PublisherKit/Observable Object/Observable Object.swift @@ -0,0 +1,49 @@ +// +// Observable Object.swift +// PublisherKit +// +// Created by Raghav Ahuja on 29/03/20. +// + +/// 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 ObjectWillChangePublisher.Failure == Never + + /// A publisher that emits before the object has changed. + var objectWillChange: ObjectWillChangePublisher { get } +} + +extension ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher { + + /// A publisher that emits before the object has changed. + public var objectWillChange: ObservableObjectPublisher { + ObservableObjectPublisher() + } +} diff --git a/Sources/PublisherKit/Observable Object/Published.swift b/Sources/PublisherKit/Observable Object/Published.swift new file mode 100644 index 0000000..dd9cce8 --- /dev/null +++ b/Sources/PublisherKit/Observable Object/Published.swift @@ -0,0 +1,75 @@ +// +// Published.swift +// PublisherKit +// +// Created by Raghav Ahuja on 29/03/20. +// + +/// Adds a `Publisher` to a property. +/// +/// Properties annotated with `@Published` contain both the stored value and a publisher which sends any new values after the property value has been sent. New subscribers will receive the current value 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. +@propertyWrapper public struct Published { + + private var value: Value + + @available(*, unavailable, message: "@Published is only available on properties of classes") + public var wrappedValue: Value { + get { value } + set { value = newValue } + } + + private var publisher: Publisher? + var objectWillChange: ObservableObjectPublisher? + + /// Initialize the storage of the Published property as well as the corresponding `Publisher`. + public init(initialValue: Value) { + value = initialValue + } + + public init(wrappedValue: Value) { + value = wrappedValue + } + + /// A publisher for properties marked with the `@Published` attribute. + public struct Publisher: PublisherKit.Publisher { + + public typealias Output = Value + + public typealias Failure = Never + + fileprivate let subject: CurrentValueSubject + + fileprivate init(_ output: Output) { + subject = CurrentValueSubject(output) + } + + public func receive(subscriber: S) where Output == S.Input, Failure == S.Failure { + subject.subscribe(subscriber) + } + } + + /// 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 { + return publisher + } + + let publisher = Publisher(value) + self.publisher = publisher + + return publisher + } + } + + public static subscript(_enclosingInstance object: EnclosingSelf, wrapped wrappedKeyPath: ReferenceWritableKeyPath, storage storageKeyPath: ReferenceWritableKeyPath>) -> Value { + get { + object[keyPath: storageKeyPath].value + } set { + object[keyPath: storageKeyPath].objectWillChange?.send() + object[keyPath: storageKeyPath].publisher?.subject.send(newValue) + object[keyPath: storageKeyPath].value = newValue + } + } +}