From e781e1d0ce398259ca38cc0d5d0ed6b56d8eab39 Mon Sep 17 00:00:00 2001 From: Srdan Rasic Date: Sat, 18 Mar 2017 15:20:22 +0100 Subject: [PATCH] Introduce `Deallocatable` and `BindingExecutionContextProvider`. Implement `bind(to:)`. Improve documentation of various classes. --- ReactiveKit.podspec | 4 +- ReactiveKit.xcodeproj/project.pbxproj | 10 +++ ReactiveKit/Info.plist | 2 +- Sources/Bindable.swift | 93 ++++++++++++++++++++++++--- Sources/Deallocatable.swift | 65 +++++++++++++++++++ Sources/Disposable.swift | 69 ++++++++++++++------ Sources/ExecutionContext.swift | 14 +++- Sources/Reactive.swift | 28 ++++++-- Sources/SignalProtocol.swift | 4 +- 9 files changed, 245 insertions(+), 44 deletions(-) create mode 100644 Sources/Deallocatable.swift diff --git a/ReactiveKit.podspec b/ReactiveKit.podspec index 4fe5a7e..a922910 100644 --- a/ReactiveKit.podspec +++ b/ReactiveKit.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |s| s.name = "ReactiveKit" - s.version = "3.3.1" + s.version = "3.4.0" s.summary = "A Swift Reactive Programming Framework" s.description = "ReactiveKit is a Swift framework for reactive and functional reactive programming." s.homepage = "https://github.com/ReactiveKit/ReactiveKit" s.license = 'MIT' s.author = { "Srdan Rasic" => "srdan.rasic@gmail.com" } - s.source = { :git => "https://github.com/ReactiveKit/ReactiveKit.git", :tag => "v3.3.1" } + s.source = { :git => "https://github.com/ReactiveKit/ReactiveKit.git", :tag => "v3.4.0" } s.ios.deployment_target = '8.0' s.osx.deployment_target = '10.9' diff --git a/ReactiveKit.xcodeproj/project.pbxproj b/ReactiveKit.xcodeproj/project.pbxproj index d847425..8457ae0 100644 --- a/ReactiveKit.xcodeproj/project.pbxproj +++ b/ReactiveKit.xcodeproj/project.pbxproj @@ -63,6 +63,10 @@ 16F2F56E1D6D924B00B85896 /* SignalProtocol+Arities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16F2F56B1D6D924B00B85896 /* SignalProtocol+Arities.swift */; }; 16F2F56F1D6D924B00B85896 /* SignalProtocol+Arities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16F2F56B1D6D924B00B85896 /* SignalProtocol+Arities.swift */; }; EC06A8711DBBF8C7006AEA81 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC06A86F1DBBF8A6006AEA81 /* ResultTests.swift */; }; + EC31EE0D1E7BF97C00857946 /* Deallocatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC31EE0C1E7BF97C00857946 /* Deallocatable.swift */; }; + EC31EE0E1E7BF97C00857946 /* Deallocatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC31EE0C1E7BF97C00857946 /* Deallocatable.swift */; }; + EC31EE0F1E7BF97C00857946 /* Deallocatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC31EE0C1E7BF97C00857946 /* Deallocatable.swift */; }; + EC31EE101E7BF97C00857946 /* Deallocatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC31EE0C1E7BF97C00857946 /* Deallocatable.swift */; }; EC4C28121E167EED0055D256 /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4C28111E167EED0055D256 /* Typealiases.swift */; }; EC4C28131E167EED0055D256 /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4C28111E167EED0055D256 /* Typealiases.swift */; }; EC4C28141E167EED0055D256 /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4C28111E167EED0055D256 /* Typealiases.swift */; }; @@ -112,6 +116,7 @@ 16F2F5661D6D8A4500B85896 /* Lock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Lock.swift; path = Sources/Lock.swift; sourceTree = SOURCE_ROOT; }; 16F2F56B1D6D924B00B85896 /* SignalProtocol+Arities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SignalProtocol+Arities.swift"; path = "Sources/SignalProtocol+Arities.swift"; sourceTree = SOURCE_ROOT; }; EC06A86F1DBBF8A6006AEA81 /* ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = ""; }; + EC31EE0C1E7BF97C00857946 /* Deallocatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Deallocatable.swift; path = Sources/Deallocatable.swift; sourceTree = SOURCE_ROOT; }; EC4C28111E167EED0055D256 /* Typealiases.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Typealiases.swift; path = Sources/Typealiases.swift; sourceTree = SOURCE_ROOT; }; EC6C0FC51DB4A76A00C3880B /* PropertyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PropertyTests.swift; sourceTree = ""; }; EC7A6F881D3CCF5B00F9EF4A /* NoError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NoError.swift; path = Sources/NoError.swift; sourceTree = SOURCE_ROOT; }; @@ -208,6 +213,7 @@ EC8A99E61CABD9B50042A6AD /* ExecutionContext.swift */, 16886A191D3168E000D83E39 /* Connectable.swift */, EC8A99DC1CABD9B50042A6AD /* Disposable.swift */, + EC31EE0C1E7BF97C00857946 /* Deallocatable.swift */, EC8A99DF1CABD9B50042A6AD /* Observer.swift */, EC8A99E41CABD9B50042A6AD /* Subjects.swift */, 16886A221D31705500D83E39 /* Bindable.swift */, @@ -468,6 +474,7 @@ buildActionMask = 2147483647; files = ( 16A205081D3236EC0054484B /* Property.swift in Sources */, + EC31EE0E1E7BF97C00857946 /* Deallocatable.swift in Sources */, 16F2F5681D6D8A4500B85896 /* Lock.swift in Sources */, 16886A1F1D31699F00D83E39 /* Subjects.swift in Sources */, 16F2F56D1D6D924B00B85896 /* SignalProtocol+Arities.swift in Sources */, @@ -492,6 +499,7 @@ buildActionMask = 2147483647; files = ( 16A205071D3236EC0054484B /* Property.swift in Sources */, + EC31EE0F1E7BF97C00857946 /* Deallocatable.swift in Sources */, 16F2F5691D6D8A4500B85896 /* Lock.swift in Sources */, 16886A201D31699F00D83E39 /* Subjects.swift in Sources */, 16F2F56E1D6D924B00B85896 /* SignalProtocol+Arities.swift in Sources */, @@ -516,6 +524,7 @@ buildActionMask = 2147483647; files = ( 16A205061D3236EC0054484B /* Property.swift in Sources */, + EC31EE101E7BF97C00857946 /* Deallocatable.swift in Sources */, 16F2F56A1D6D8A4500B85896 /* Lock.swift in Sources */, 16886A1E1D31699E00D83E39 /* Subjects.swift in Sources */, 16F2F56F1D6D924B00B85896 /* SignalProtocol+Arities.swift in Sources */, @@ -540,6 +549,7 @@ buildActionMask = 2147483647; files = ( EC8706671D3CDDAA0068B589 /* Property.swift in Sources */, + EC31EE0D1E7BF97C00857946 /* Deallocatable.swift in Sources */, 16F2F5671D6D8A4500B85896 /* Lock.swift in Sources */, EC8706661D3CDCE10068B589 /* Bindable.swift in Sources */, 16F2F56C1D6D924B00B85896 /* SignalProtocol+Arities.swift in Sources */, diff --git a/ReactiveKit/Info.plist b/ReactiveKit/Info.plist index dd6cc70..a8f98d8 100644 --- a/ReactiveKit/Info.plist +++ b/ReactiveKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.3.1 + 3.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/Bindable.swift b/Sources/Bindable.swift index 32da39c..b3824b7 100644 --- a/Sources/Bindable.swift +++ b/Sources/Bindable.swift @@ -25,23 +25,31 @@ /// Bindable is like an observer, but knows to manage the subscription by itself. public protocol BindableProtocol { + /// Type of the received elements. associatedtype Element - /// Accepts a signal that should be observed by the receiver. + /// Establish a one-way binding between the signal and the receiver. + /// - Warning: Do not use this method to bind signals. Use `bind(to:)` instead. func bind(signal: Signal) -> Disposable } extension SignalProtocol where Error == NoError { - /// Establish a one-way binding between the source and the bindable - /// and return a disposable that can cancel binding. + /// Establish a one-way binding between the source and the bindable. + /// - Parameter bindable: A binding target that will receive signal events. + /// - Parameter context: An execution context used to delived events. + /// Defaults to a context that breaks recursive calls. + /// - Returns: A disposable that can cancel the binding. @discardableResult public func bind(to bindable: B, context: @escaping ExecutionContext = createNonRecursiveContext()) -> Disposable where B.Element == Element { return bindable.bind(signal: observeIn(context)) } - /// Establish a one-way binding between the source and the bindable - /// and return a disposable that can cancel binding. + /// Establish a one-way binding between the source and the bindable. + /// - Parameter bindable: A binding target that will receive signal events. + /// - Parameter context: An execution context used to delived events. + /// Defaults to a context that breaks recursive calls. + /// - Returns: A disposable that can cancel the binding. @discardableResult public func bind(to bindable: B, context: @escaping ExecutionContext = createNonRecursiveContext()) -> Disposable where B.Element: OptionalProtocol, B.Element.Wrapped == Element { return map { B.Element($0) }.observeIn(context).bind(to: bindable) @@ -50,16 +58,81 @@ extension SignalProtocol where Error == NoError { extension BindableProtocol where Self: SignalProtocol, Self.Error == NoError { - /// Establish a two-way binding between the source and the bindable - /// and return a disposable that can cancel binding. + /// Establish a two-way binding between the source and the bindable. + /// - Parameter target: A binding target that will receive events from + /// the receiver and a source that will send events to the receiver. + /// - Parameter context: An execution context used to delived events. + /// Defaults to a context that breaks recursive calls. + /// - Returns: A disposable that can cancel the binding. @discardableResult - public func bidirectionalBind(to bindable: B, context: @escaping ExecutionContext = createNonRecursiveContext()) -> Disposable where B.Element == Element, B.Error == Error { - let d1 = bind(to: bindable, context: context) - let d2 = bindable.bind(to: self, context: context) + public func bidirectionalBind(to target: B, context: @escaping ExecutionContext = createNonRecursiveContext()) -> Disposable where B.Element == Element, B.Error == Error { + let d1 = bind(to: target, context: context) + let d2 = target.bind(to: self, context: context) return CompositeDisposable([d1, d2]) } } +extension SignalProtocol where Error == NoError { + + /// Bind the receiver to the target using the given setter closure. Closure is + /// called whenever the signal emits `next` event. + /// + /// Binding lives until either the signal completes or the target is deallocated. + /// That means that the returned disposable can be safely ignored. + /// + /// - Parameters: + /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding + /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. + /// Also conforms to `BindingExecutionContextProvider` that provides that context on which to execute the setter. + /// - setter: A closure that gets called on each next signal event both with the target and the sent element. + /// - Returns: A disposable that can cancel the binding. + @discardableResult + public func bind(to target: Target, setter: @escaping (Target, Element) -> Void) -> Disposable + where Target: BindingExecutionContextProvider + { + return bind(to: target, context: target.bindingExecutionContext, setter: setter) + } + + /// Bind the receiver to the target using the given setter closure. Closure is + /// called whenever the signal emits `next` event. + /// + /// Binding lives until either the signal completes or the target is deallocated. + /// That means that the returned disposable can be safely ignored. + /// + /// - Parameters: + /// - target: A binding target. Conforms to `Deallocatable` so it can inform the binding + /// when it gets deallocated. Upon target deallocation, the binding gets automatically disposed. + /// - context: An execution context on which to execute the setter. + /// - setter: A closure that gets called on each next signal event both with the target and the sent element. + /// - Returns: A disposable that can cancel the binding. + @discardableResult + public func bind(to target: Target, context: @escaping ExecutionContext, setter: @escaping (Target, Element) -> Void) -> Disposable { + return take(until: target.deallocated).observeNext { [weak target] element in + context { + if let target = target { + setter(target, element) + } + } + } + } +} + +/// Provides an execution context used to deliver binding events. +/// +/// `NSObject` conforms to this protocol be providing `ImmediateOnMainExecutionContext` +/// as binding execution context. Specific subclasses can override the context if needed. +public protocol BindingExecutionContextProvider { + + /// An execution context used to deliver binding events. + var bindingExecutionContext: ExecutionContext { get } +} + +extension NSObject: BindingExecutionContextProvider { + + public var bindingExecutionContext: ExecutionContext { + return ImmediateOnMainExecutionContext + } +} /// A context that breaks recursive calls (binding cycles). private func createNonRecursiveContext() -> ExecutionContext { diff --git a/Sources/Deallocatable.swift b/Sources/Deallocatable.swift new file mode 100644 index 0000000..de237e9 --- /dev/null +++ b/Sources/Deallocatable.swift @@ -0,0 +1,65 @@ +// +// Deallocatable.swift +// ReactiveKit +// +// Created by Srdan Rasic on 17/03/2017. +// Copyright © 2017 Srdan Rasic. All rights reserved. +// + +/// A type that notifies about its own deallocation. +/// +/// `Deallocatable` can be used as a binding target. For example, +/// instead of observing a signal, one can bind it to a `Deallocatable`. +/// +/// class View: Deallocatable { ... } +/// +/// let view: View = ... +/// let signal: SafeSignal = ... +/// +/// signal.bind(to: view) { view, number in +/// view.display(number) +/// } +public protocol Deallocatable: class { + + /// A signal that fires `completed` event when the receiver is deallocated. + var deallocated: SafeSignal { get } +} + +/// A type that provides a dispose bag. +/// `DisposeBagProvider` conforms to `Deallocatable` out of the box. +public protocol DisposeBagProvider: Deallocatable { + + /// A `DisposeBag` that can be used to dispose observations and bindings. + var bag: DisposeBag { get } +} + +extension DisposeBagProvider { + + /// A signal that fires `completed` event when the receiver is deallocated. + public var deallocated: SafeSignal { + return bag.deallocated + } +} + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + + import ObjectiveC.runtime + + extension NSObject: DisposeBagProvider { + + private struct AssociatedKeys { + static var DisposeBagKey = "DisposeBagKey" + } + + public var bag: DisposeBag { + if let disposeBag = objc_getAssociatedObject(self, &NSObject.AssociatedKeys.DisposeBagKey) { + return disposeBag as! DisposeBag + } else { + let disposeBag = DisposeBag() + objc_setAssociatedObject(self, &NSObject.AssociatedKeys.DisposeBagKey, disposeBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return disposeBag + } + } + } + +#endif diff --git a/Sources/Disposable.swift b/Sources/Disposable.swift index 3d11f95..c0a4c19 100644 --- a/Sources/Disposable.swift +++ b/Sources/Disposable.swift @@ -24,10 +24,19 @@ import Foundation -/// Objects conforming to this protocol dispose (cancel) signals and operations. +/// A disposable is an object that can be used to cancel a signal observation. +/// +/// Disposables are returned by `observe*` and `bind*` methods. +/// +/// let disposable = signal.observe { ... } +/// +/// Disposing the disposable cancels the observation. A signal is guaranteed not to +/// fire any event after is has been disposed. +/// +/// disposable.dispose() public protocol Disposable { - /// Dispose the signal or operation. + /// Dispose the signal observation or binding. func dispose() /// Returns `true` is already disposed. @@ -81,7 +90,7 @@ public final class BlockDisposable: Disposable { } /// A disposable that disposes itself upon deallocation. -public class DeinitDisposable: Disposable { +public final class DeinitDisposable: Disposable { public var otherDisposable: Disposable? = nil @@ -102,7 +111,7 @@ public class DeinitDisposable: Disposable { } } -/// A disposable that disposes a collection of disposables upon disposing. +/// A disposable that disposes a collection of disposables upon its own disposing. public final class CompositeDisposable: Disposable { public private(set) var isDisposed: Bool = false @@ -127,6 +136,10 @@ public final class CompositeDisposable: Disposable { } } + public static func += (left: CompositeDisposable, right: Disposable) { + left.add(disposable: right) + } + public func dispose() { lock.lock(); defer { lock.unlock() } isDisposed = true @@ -135,11 +148,7 @@ public final class CompositeDisposable: Disposable { } } -public func += (left: CompositeDisposable, right: Disposable) { - left.add(disposable: right) -} - -/// A disposable that disposes other disposable. +/// A disposable that disposes other disposable upon its own disposing. public final class SerialDisposable: Disposable { public private(set) var isDisposed: Bool = false @@ -169,17 +178,36 @@ public final class SerialDisposable: Disposable { } /// A container of disposables that will dispose the disposables upon deinit. +/// A bag is a prefered way to handle disposables: +/// +/// let bag = DisposeBag() +/// +/// signal +/// .observe { ... } +/// .dispose(in: bag) +/// +/// When bag gets deallocated, it will dispose all disposables it contains. public protocol DisposeBagProtocol: Disposable { func add(disposable: Disposable) } /// A container of disposables that will dispose the disposables upon deinit. +/// A bag is a prefered way to handle disposables: +/// +/// let bag = DisposeBag() +/// +/// signal +/// .observe { ... } +/// .dispose(in: bag) +/// +/// When bag gets deallocated, it will dispose all disposables it contains. public final class DisposeBag: DisposeBagProtocol { + private var disposables: [Disposable] = [] private var subject: ReplayOneSubject? private lazy var lock = NSRecursiveLock(name: "com.reactivekit.disposebag") - /// This will return true whenever the bag is empty. + /// `true` if bag is empty, `false` otherwise. public var isDisposed: Bool { return disposables.count == 0 } @@ -187,18 +215,24 @@ public final class DisposeBag: DisposeBagProtocol { public init() { } - /// Adds the given disposable to the bag. - /// Disposable will be disposed when the bag is deinitialized. + /// Add the given disposable to the bag. + /// Disposable will be disposed when the bag is deallocated. public func add(disposable: Disposable) { disposables.append(disposable) } + /// Add a disposable to a dispose bag. + public static func += (left: DisposeBag, right: Disposable) { + left.add(disposable: right) + } + /// Disposes all disposables that are currenty in the bag. public func dispose() { disposables.forEach { $0.dispose() } disposables.removeAll() } + /// A signal that fires `completed` event when the bag gets deallocated. public var deallocated: SafeSignal { lock.lock() if subject == nil { @@ -214,18 +248,11 @@ public final class DisposeBag: DisposeBagProtocol { } } -public func += (left: DisposeBag, right: Disposable) { - left.add(disposable: right) -} - public extension Disposable { + /// Put the disposable in the given bag. Disposable will be disposed when + /// the bag is either deallocated or disposed. public func dispose(in disposeBag: DisposeBagProtocol) { disposeBag.add(disposable: self) } - - @available(*, deprecated, renamed: "dispose(in:)") - public func disposeIn(_ disposeBag: DisposeBag) { - disposeBag.add(disposable: self) - } } diff --git a/Sources/ExecutionContext.swift b/Sources/ExecutionContext.swift index d9b0cbe..78c7044 100644 --- a/Sources/ExecutionContext.swift +++ b/Sources/ExecutionContext.swift @@ -25,15 +25,23 @@ import Foundation import Dispatch -/// Represents a context that can execute given block. +/// Execution context is an abstraction over a thread or a dispatch queue. +/// It is just a function that executes other function. +/// +/// let context = DispatchQueue.background.context +/// +/// context { +/// print("Printing on background queue.") +/// } +/// public typealias ExecutionContext = (@escaping () -> Void) -> Void -/// Execute block on current thread or queue. +/// Executes on current thread or queue. public let ImmediateExecutionContext: ExecutionContext = { block in block() } -/// If current thread is main thread, just execute block. Otherwise, do +/// If current thread is main thread, just executes the block. Otherwise, do /// async dispatch of the block to the main queue (thread). public let ImmediateOnMainExecutionContext: ExecutionContext = { block in if Thread.isMainThread { diff --git a/Sources/Reactive.swift b/Sources/Reactive.swift index b6370e8..6b1fa2b 100644 --- a/Sources/Reactive.swift +++ b/Sources/Reactive.swift @@ -22,15 +22,17 @@ // THE SOFTWARE. // +import Foundation + /// A proxy protocol for reactive extensions. /// -/// To provide reactive extensions on type X, do +/// To provide reactive extensions on type `X`, do /// -/// extension ReactiveExtensions where Base == X { -/// var y: Signal { ... } -/// } +/// extension ReactiveExtensions where Base == X { +/// var y: SafeSignal { ... } +/// } /// -/// where X conforms to ReactiveExtensionsProvider. +/// where `X` conforms to `ReactiveExtensionsProvider`. public protocol ReactiveExtensions { associatedtype Base var base: Base { get } @@ -48,11 +50,27 @@ public protocol ReactiveExtensionsProvider: class {} public extension ReactiveExtensionsProvider { + /// Reactive extensions of `self`. public var reactive: Reactive { return Reactive(self) } + /// Reactive extensions of `Self`. public static var reactive: Reactive.Type { return Reactive.self } } + +extension NSObject: ReactiveExtensionsProvider {} + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + + extension ReactiveExtensions where Base: NSObject { + + /// A signal that fires completion event when the object is deallocated. + public var deallocated: SafeSignal { + return base.bag.deallocated + } + } + +#endif diff --git a/Sources/SignalProtocol.swift b/Sources/SignalProtocol.swift index 3b48c61..a54a709 100644 --- a/Sources/SignalProtocol.swift +++ b/Sources/SignalProtocol.swift @@ -110,7 +110,7 @@ public extension SignalProtocol { } } - /// Create a signal that just terminates with the give error. + /// Create a signal that just terminates with the given error. public static func failed(_ error: Error) -> Signal { return Signal { observer in observer.failed(error) @@ -118,7 +118,7 @@ public extension SignalProtocol { } } - /// Create an signal that never completes. + /// Create a signal that never completes. public static func never() -> Signal { return Signal { observer in return NonDisposable.instance