Skip to content

Commit

Permalink
Introduce Deallocatable and BindingExecutionContextProvider. Impl…
Browse files Browse the repository at this point in the history
…ement `bind(to:)`. Improve documentation of various classes.
  • Loading branch information
srdanrasic committed Mar 18, 2017
1 parent 125d52d commit e781e1d
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 44 deletions.
4 changes: 2 additions & 2 deletions ReactiveKit.podspec
Original file line number Diff line number Diff line change
@@ -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" => "[email protected]" }
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'
Expand Down
10 changes: 10 additions & 0 deletions ReactiveKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
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 = "<group>"; };
EC7A6F881D3CCF5B00F9EF4A /* NoError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NoError.swift; path = Sources/NoError.swift; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand Down
2 changes: 1 addition & 1 deletion ReactiveKit/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>3.3.1</string>
<string>3.4.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
93 changes: 83 additions & 10 deletions Sources/Bindable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element, NoError>) -> 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<B: BindableProtocol>(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<B: BindableProtocol>(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)
Expand All @@ -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<B: BindableProtocol & SignalProtocol>(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<B: BindableProtocol & SignalProtocol>(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<Target: Deallocatable>(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<Target: Deallocatable>(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 {
Expand Down
65 changes: 65 additions & 0 deletions Sources/Deallocatable.swift
Original file line number Diff line number Diff line change
@@ -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<Int> = ...
///
/// 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<Void> { 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<Void> {
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
Loading

0 comments on commit e781e1d

Please sign in to comment.