Bond is a Swift binding framework that takes binding concept to a whole new level - boils it down to just one operator. It's simple, powerful, type-safe and multi-paradigm - just like Swift.
Bond was created with two goals in mind: simple to use and simple to understand. One might argue whether the former implies the latter, but Bond will save you some thinking because both are true in this case. Its foundation are two simple classes - everything else are extensions and syntactic sugars.
Say you'd like a label to reflect state of a text field. Instead of going through all the 'action-target' pain, with Bond you'll do it like this:
textField ->> label
That one line establishes a bond between text field's text property and label's text property. In effect, whenever user makes a change to the text field, that change will be automatically propagated to the label.
More often than not, direct binding is not enough. Usually you need to transform input is some way, like prepending a greeting to a name. Of course, Bond has full confidence in functional paradigm.
textField.dynText.map { "Hi " + $0 } ->> label
Whenever a change occurs in the text field, new value will be transformed by the closure and propagated to the label.
Notice how we've used dynText
property of the UITextField. It's an observable representation of text
property provided by Bond framework. There are many other properties like that one for various UIKit components.
In addition to map
, another important functional construct is filter
. It's useful when we are interested only in some values of domain. For example, when observing events of a button, we might be interested only in TouchUpInside
event so we can perform certain action when user taps the button:
lazy var loginButtonTapListener = Bond<UIControlEvents>() { event in
// perform login
}
...
loginButton.dynEvent.filter(==, .TouchUpInside) ->> loginButtonTapListener
As you see, our binding target doesn't have to be an UI component, rather it can be an arbitrary action wrapped in a closure created by instantiating a Bond
object.
Closure is our Listener
, while Bond
is an object that manages bindings. We save actions in properties as we need to retain them.
Bond can also reduce
multiple inputs into a single output. Following snippet depicts how values of two text fields can be reduced to a boolean value and applied to button's enabled
property.
reduce(emailField.dynText, passField.dynText) { email, pass in
return countElements(email) > 0 && countElements(pass) > 0
} ->> loginButton.dynEnabled
Whenever user types something into any of the text fields, expression will be evaluated and button state updated.
Bond's power is not, however, in coupling various UI components, but in a bonding of Model (or ViewModel) to View and vice-versa. It's great for MVVM paradigm. Here is how one could bond user's number of followers property of a model to a label.
viewModel.numberOfFollowers.map { "\($0)" } ->> label
Point here is not in the simplicity of value assignment to text property of a label, but in the creation of a bond which automatically updates label text property whenever number of followers change.
Bond also supports two way bindings. Here is an example of how you could keep username text field and username property of your view model in sync (whenever any of them change, other one will be updated to):
viewModel.username <->> usernameTextField.dynText
Notice the asymmetry of bi-directional bind operator. It's important which side of the expression something is on. In Bond, right side always retains left side! In this example, binding will exist as long as usernameTextField
lives.
Not impressed? Let me give you one last example. Say you have an array of repositories you would like to display in a table view. For each repository you have a name and its owner's profile photo. Of course, photo is not immediately available as it has to be downloaded, but once you get it, you want it to appear in table view's cell. Additionally, when user does 'pull down to refresh', and your array gets new repositiories, you want those in table view too.
So how do you proceed? Well, instead of implementing a data source object, observing photo downloads with KVO and manually updating table view with new items, with Bond you can do all that in just few lines:
var tableViewDataSourceBond: UITableViewDataSourceBond<UITableViewCell>!
override func viewDidLoad() {
super.viewDidLoad()
// create a data source bond for table view
tableViewDataSourceBond = UITableViewDataSourceBond(tableView: self.tableView)
// map repositories to cells and bind
repositories.map { [unowned self] (repository: Repository) -> RepositoryTableViewCell in
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as RepositoryTableViewCell
repository.name ->> cell.nameLabel
repository.photo ->> cell.avatarImageView
return cell
} ->> tableViewDataSourceBond
}
Yes, that's right!
(As always, when working with closures by extremely careful not to cause retain cycles! Note how we used unowned self
in above example. If you are not familiar with concept of retain cycles, check out this: Resolving Strong Reference Cycles for Closures)
(You can find demo app with additional examples here.)
Bond is in essence a variation of the Observer pattern. There is a subject of type Dynamic<T>
and a listener (observer) of type T -> Void
(closure). Dynamic holds a value of some type T
and calls bound listeners whenever new value is set.
In addition to basic Observer pattern, Bond leverages additional object to make bindings lifecycle simple. It's type is Bond<T>
. It wraps listener closure and can bind itself to Dynamics of same generic type T
. The simplicity lies in that the bindings exist as long as corresponding Bond object exists. There is no need to manually unregister listeners.
Let's dive deeper.
Consider numberOfFollowers
property from earlier example. What's its type? An Int
? Well, kind of. Let's see how it is defined.
let numberOfFollowers: Dynamic<Int>
So it is an Int
, but an Int
wrapped in a generic object of type Dynamic<T>
. That's the type you'll use to define a property or a variable whose changes you want to observe. Dynamic is the first of two classes that make up Bond framework.
How do you set its value? Just set it to the value
property, like this:
numberOfFollowers.value = 42
Dynamic is defined like this:
public class Dynamic<T> {
public init(_ v: T)
public func bindTo(bond: Bond<T>)
public var value: T
public let valueBond: Bond<T>
}
We've already seen how to propagate change of value, but let's define it formally. Propagation of the change is done through a Bond
established between a Dynamic
and a Listener
(a closure). Bond is the second of two classes that make up Bond framework. You create a bond by instantiating Bond object with a Listener and by binding a Dynamic to it, like this:
let myBond = Bond<Int>() { value in
println("Number of followers changed to \(value).")
}
numberOfFollowers.bindTo(myBond)
Those few lines will establish a bond between the numberOfFollowers
and the closure that prints each change. Bond lives as long as it is retained by someone else. Usually you'll create bonds as properties in your classes. Bond
object retains bonded Dynamic
object(s)!
Calling bindTo
method is old-fashioned so it can be simplified by an already mentioned ->>
operator. Previous line can be rewritten like:
numberOfFollowers ->> myBond
Very simple indeed. Dynamic on the left side, Bond on the right side.
Bond is defined like this:
public class Bond<T> {
public typealias Listener = T -> Void
public var listener: Listener?
public init()
public init(_ listener: Listener)
public func bind(dynamic: Dynamic<T>)
public func unbindAll()
}
Dynamic can also act as a Bond because it has a Bond attached to itself that updates its value. You can access that bond through valueBond
property. What that means is that you can bind one Dynamic to another.
// given that both 'usernames' are of type Dynamic<String>
viewmodel.username ->> self.username
Be very careful not to make binding cycles like this:
d1 ->> d2
d2 ->> d1
Or even more subtile:
d1 ->> d2
d2 ->> d3
d3 ->> d1
While they'll work, they'll cause retain cycles and will stay in memory until your app is killed.
You can fix first example by using two way binding operator
d1 <->> d2
You should probably never be in situation that you need something as in second example, but should you be, you can fix it by making feedback binding weak. You cannot use operator for that, but it can be achieved like this:
d1 ->> d2
d2 ->> d3
d3.bindTo(d1.valueBond, fire: false, strongly: false)
UIKit views and controls are not, of course, Dynamics and Bonds, so how can they act as agents in a Bond world?
Controls and views for which it makes sense to are extended to provide Dynamics for commonly used properties, like UITextField's text
property, UISlider's value
property or UISwitch's on
property.
To get a Dynamic representation of a property of UIKit object, use the variant that has dyn*
prefix. For example, to get dynamic representation of UITextField's text
property, use dynText
property. Returned Dynamic object is coupled to the control or the view whose value it observes and updates through mechanism like Action-Target or Key-Value-Observing.
Following table lists all available Dynamics of UIKit objects:
Class | Dynamic(s) | Designated Dynamic |
---|---|---|
UIView | dynAlpha dynHidden dynBackgroundColor |
-- |
UISlider | dynValue | dynValue |
UILabel | dynText dynAttributedText dynTextColor |
dynText |
UIProgressView | dynProgress | dynProgress |
UIImageView | dynImage | dynImage |
UIButton | dynEnabled dynTitle dynImageForNormalState |
dynEnabled |
UIBarItem | dynEnabled dynTitle dynImage |
dynEnabled |
UISwitch | dynOn | dynOn |
UITextField | dynText dynEnabled |
dynText |
UITextView | dynText dynAttributedText |
dynText |
UIDatePicker | dynDate | dynDate |
UIActivityIndicatorView | dynIsAnimating | dynIsAnimating |
DynamicArray | dynCount | dynCount |
You might be wondering what Designated Dynamic is. It's way to access most commonly used Dynamic through property designatedDynamic
. Having common name enables us to define protocol like
public protocol Dynamical {
typealias DynamicType
var designatedDynamic: Dynamic<DynamicType> { get }
}
and use that protocol to make binding easier. Instead of doing binding like
titleTextField.dynText ->> titleLabel
it allows us to do just
titleTextField ->> titleLabel
because operator ->>
is overloaded to work with Dynamical
.
Functions map, filter, reduce, zip, rewrite and skip operate on Dynamic object by creating a new Dynamic that is bonded with source one. Newly created Dynamic retain their source Dynamic!
func map<T, U>(dynamic: Dynamic<T>, f: T -> U) -> Dynamic<U>
Map function maps a Dynamic to a new Dynamic of different type. Value transformation from source type to destination type is performed by a given closure. Newly created Dynamic internally holds a Bond to source Dynamic that's updating its value whenever value of source Dynamic changes. It applies given transformation closure on each update.
Map is also available as a method of Dynamic class with first parameter omitted (which is assumed to be self
).
func filter<T>(dynamic: Dynamic<T>, f: T -> Bool) -> Dynamic<T>
Filter function creates a new Dynamic that's bonded to its source Dynamic in similar way as it is done with map function. Difference is that there are no type transformations, but the filtering of source values. Newly created Dynamic changes its value only when new value of source Dynamic satisfies expression in a given closure.
Filter is also available as a method of Dynamic class with first parameter omitted (which is assumed to be self
).
reduce<A, B, T>(dA: Dynamic<A>, dB: Dynamic<B>, f: (A, B) -> T) -> Dynamic<T>
reduce<A, B, C, T>(dA: Dynamic<A>, dB: Dynamic<B>, dC: Dynamic<C>, f: (A, B, C) -> T) -> Dynamic<T>
Reduce is a simple function that takes two or more Dynamics and returns a new Dynamic of arbitrary type. New Dynamic holds a Bond to each of source Dynamics and updates its value whenever any of source Dynamics change. It updates its value by applying a given closure to values of source Dynamics.
zip<T, U>(dynamic: Dynamic<T>, value: U) -> Dynamic<(T, U)>
zip<T, U>(d1: Dynamic<T>, d2: Dynamic<U>) -> Dynamic<(T, U)>
First variant of zip takes a Dynamic and a value and produces new Dynamic with those two in a tuple. Produced Dynamic fires whenever source Dynamic fires. Note that if you pass an object as a value, it'll be retained by the produced Dynamic!
Second variant of zip takes two Dynamics and produces new Dynamic with those two in a tuple. Produced Dynamic fires whenever any of source Dynamics fire.
rewrite<T, U>(dynamic: Dynamic<T>, value: U) -> Dynamic<U>
When you don't care about a value of a Dynamic but are still interested in change events, you can create a Dynamic that rewrites value of source Dynamic with some constant. Note that if you pass an object as a value, it'll be retained by the produced Dynamic!
skip<T>(dynamic: Dynamic<T>, count: Int) -> Dynamic<T>
You can use skip to create a Dynamic that'll not dispatch change events for count
times.
throttle<T>(dynamic: Dynamic<T>, seconds: Double, queue: dispatch_queue_t = dispatch_get_main_queue()) -> Dynamic<T>
Throttle function creates a new Dynamic that propagates changes at most once during the interval defined by seconds
argument. If additional changes occurred while throttling, only the last one will be dispatched. Note that the last argument defines queue
to dispatch change events on and defaults to the main queue!
deliver<T>(dynamic: Dynamic<T>, on queue: dispatch_queue_t) -> Dynamic<T>
DeliverOn function creates a new Dynamic whose changes are observer on specified queue
.
any<T>(dynamics: [Dynamic<T>]) -> Dynamic<T>
Any expects an array of one or more Dynamics of same type and produces a Dynamic that'll fire whenever any of source Dynamics fire.
As each of these functions return another Dynamic, it is possible to compose (chain) more than one of them in order to get desired behaviour. For example, if we need to bind an Int property to a label (which provides a Bond of String type), but only if number is greater than 10, we could do it like this.
number.filter { $0 > 10 }.map { "\($0)" } ->> label
d ->> b
d1 ->> d2
Establishes binding between a Dynamic and a Bond or another Dynamics's valueBond
. Calls Listener closure right after binding. Equivalent to:
d.bindTo(b, fire: true, strongly: true)
d1.bindTo(d2.valueBond, fire: true, strongly: true)
d ->| b
d1 ->| d2
Establishes binding between a Dynamic and a Bond or another Dynamics's valueBond
. Does not call Listener closure after binding. Equivalent to:
d.bindTo(b, fire: false, strongly: true)
d1.bindTo(d2.valueBond, fire: false, strongly: true)
d1 <->> d2
Establishes two way binding between two Dynamics. Equivalent to:
d1.bindTo(d2.valueBond, fire: true, strongly: true)
d2.bindTo(d1.valueBond, fire: false, strongly: false)
Remember that famous table view example from earlier? Let's see how that repositories array is defined.
let repositories: DynamicArray<Repository>
As you can see, arrays are special kind of Dynamics. Reason behind that is that we are usually not interested in change of the array object as whole, rather we are interested in changes that occurred within the array like insertions, deletions or updates.
DynamicArray is a subclass of the Dynamic class with additional methods for array manipulation. It is designed to resemble standard Swift array. It implements same manipulation methods, subscript syntax and a support for for-in
iteration. It's defined like this:
public class DynamicArray<T>: Dynamic<Array<T>>, SequenceType {
public override init(_ v: Array<T>)
public var count: Int
public var capacity: Int
public var isEmpty: Bool
public var first: T?
public var last: T?
public func append(newElement: T)
public func append(array: Array<T>)
public func removeLast() -> T
public func insert(newElement: T, atIndex i: Int)
public func splice(array: Array<T>, atIndex i: Int)
public func removeAtIndex(index: Int) -> T
public func removeAll(keepCapacity: Bool)
public func setArray(newValue: [T])
public subscript(index: Int) -> T
}
As the DynamicArray is the subclass of the Dynamic, it can be bonded with any Bond of same generic type, but that's not what we usually want. Basic Bond object allows us to observe only value changes, and value is in this case an array as whole. In order to observe fine-grain changes, we need a special kind of Bond. It's called ArrayBond
and it's defined in following way:
public class ArrayBond<T>: Bond<Array<T>> {
public var willInsertListener: ((DynamicArray<T>, [Int]) -> Void)?
public var didInsertListener: ((DynamicArray<T>, [Int]) -> Void)?
public var willRemoveListener: ((DynamicArray<T>, [Int]) -> Void)?
public var didRemoveListener: ((DynamicArray<T>, [Int]) -> Void)?
public var willUpdateListener: ((DynamicArray<T>, [Int]) -> Void)?
public var didUpdateListener: ((DynamicArray<T>, [Int]) -> Void)?
public var willResetListener: (DynamicArray<T> -> Void)?
public var didResetListener: (DynamicArray<T> -> Void)?
override public init()
override public func bind(dynamic: Dynamic<Array<T>>)
}
Yeah, it's straightforward - it allows us to register different listeners for different events. Each listener is a closure that accepts DynamicArray itself and an array of indices of objects that will be or have been changed.
Listeners will/didReset
will be called when (and only when) method setArray
is being called. Additionally, no other listener will be called for that operation.
Let's go through one example. We'll create a new bond to our repositories
array, this time of ArrayBond type.
let myBond = ArrayBond<Repository>()
myBond.didInsertListener = { array, indices in
println("Inserted objects at indices \(indices)")
}
myBond.didUpdateListener = { array, indices in
println("Updated objects at indices \(indices)")
}
repositories ->> myBond
repositories.insert(Repository(...), atIndex: 0)
// prints: Inserted objects at indices [0]
repositories[4] = Repository(...)
// prints: Updated objects at indices [4]
Nice!
DynamicArray supports per-element map and filter function. It does not support other functions at the moment.
Map and filter functions that operate on DynamicArray differ from functions that operate on basic Dynamic in a way that they evaluate values lazily. It means that at the moment of mapping or filtering, no element from source array is transformed to destination array. Elements are transformed on an as-needed basis. Thus the map function has O(1) complexity and no unnecessary table view cell will ever get created. Filter function has O(n) complexity. (Beware that accessing value
property of mapped or filtered DynamicArray returns empty array.)
You can use dynamic array to feed a UITableView. To do this, first create a bond of type UITableViewDataSourceBond
. You'll need to pass a table view during initialization.
var tableViewDataSourceBond: UITableViewDataSourceBond<UITableViewCell>!
override func viewDidLoad() {
super.viewDidLoad()
// create a data source bond for table view
tableViewDataSourceBond = UITableViewDataSourceBond(tableView: self.tableView)
...
}
UITableViewDataSourceBond
will register itself as a dataSource
for table view so you should not change table view's data source or it'll break binding! Next, you need to bind an array of type DynamicArray to that bond. You can do that by doing map on your data source array, like this:
// map repositories to cells and bind
repositories.map { [unowned self] (repository: Repository) -> UITableViewCell in
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as RepositoryTableViewCell
repository.name ->> cell.nameLabel
repository.photo ->> cell.avatarImageView
return cell
} ->> tableViewDataSourceBond
UITableViewDataSourceBond
implements following methods of UITableViewDataSource
protocol:
numberOfSectionsInTableView:
tableView:numberOfRowsInSection:
tableView:cellForRowAtIndexPath:
If you need to provide other information to the table view, you can have your class adhere to protocol UITableViewDataSource
and implement methods you need. After that, set nextDataSource
property of UITableViewDataSourceBond to your object.
If your table view needs to display more than one section, you can feed it with a DynamicArray of DynamicArrays of UITableViewCells. Don't run away, it's actually as simple as:
let sectionOfApples = apples.map { [unowned self] (apple: Apple) -> UITableViewCell in
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as AppleTableViewCell
cell.nameLabel = apple.name
return cell
}
let sectionOfPears = pears.map { [unowned self] (pear: Pear) -> UITableViewCell in
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as PearTableViewCell
cell.nameLabel = pear.name
return cell
}
DynamicArray([sectionOfApples, sectionOfPears]) ->> tableViewDataSourceBond
Just as you can bind dynamic arrays to table views, you can bind them to collection views. Steps are same identical, just with different types: map your dynamic array to a dynamic array of UICollectionViewCell
objects and bind it to a bond of type UICollectionViewDataSourceBond
. Here is an example:
var collectionViewDataSourceBond: UICollectionViewDataSourceBond<UICollectionViewCell>!
override func viewDidLoad() {
super.viewDidLoad()
// create a data source bond for collection view
collectionViewDataSourceBond = UICollectionViewDataSourceBond(collectionView: self.collectionView)
// map repositories to cells and bind
repositories.map { [unowned self] (repository: Repository, index: Int) -> UICollectionViewCell in
let indexPath = NSIndexPath(forItem: index, inSection: 0)
let cell = self.collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as RepositoryCollectionViewCell
repository.name ->> cell.nameLabel
repository.photo ->> cell.avatarImageView
return cell
} ->> collectionViewDataSourceBond
}
Notice how we've used variant of map
that provides both an object to map and its index in the array. We need that index in order to build index path. Collection view differs from table view in that the index path is required when dequeueing cell.
It's also possible to bind multiple sections by placing individual section dynamic arrays into another dynamic array, just like it's done for table views.
DynamicArray([sectionOfApples, sectionOfPears]) ->> collectionViewDataSourceBond
You can create a Dynamic that observers value changes of some KVO-observable property. For example, you can bind a property of your existing Objective-C model object to a label with this simple one-liner:
dynamicObservableFor(self.user, keyPath: "name", defaultValue: "") ->> nameLabel
Default value is used when observed property is set to nil
. Dynamic returned by this method will only observe changes the property. Setting its value
will have no effect on bound property. If you need two way binding, keep reading.
To create bi-directional Dynamic representation a KVO property, use the following variant of the method:
dynamicObservableFor<T>(object: NSObject, #keyPath: String, #from: AnyObject? -> T, #to: T -> AnyObject?) -> Dynamic<T>
Difference is that instead of the default value you need to provide transformations from and to observed type. KVO is not type-safe so you can't see actually type, rather you see AnyObject?
.
For example, if KVO property is of NSString type and you want its Dynamic<String>
representation, you can do following:
let name: Dynamic<String> = dynamicObservableFor(self.user, keyPath: "name", from: { ($0 as? String) ?? "" }, to: { $0 })
from
closure optionally downcasts passed value to String. That will succeed if passed value is of NSString type. It will fail if it is of some other type or if it is nil
. to
closure converts value to NSString. Swift can do that implicitly, so you can just pass the object.
After you get a Dynamic, you can easily bind it to, for example, UITextField.
name <->> nameTextField
For some other types, you might need to do something like this:
let height: Dynamic<Float> = dynamicObservableFor(self.user, keyPath: "height", from: { ($0 as NSNumber).floatValue }, to: { NSNumber(float: $0) })
You can create a Dynamic that observers notifications posted by NSNotificationCenter. During initialization you need to provide notification name and a closure that'll parse notification into Dynamic's type. If you are interested only in notifications from specific object, pass that object too.
let orientation: Dynamic<UIDeviceOrientation> = dynamicObservableFor(UIDeviceOrientationDidChangeNotification, object: nil) {
notification -> UIDeviceOrientation in
return UIDevice.currentDevice().orientation
}
- Add the following to your Cartfile:
github "SwiftBond/Bond" ~> 3.7
- Run
carthage update
- Add the framework as described in Carthage Readme
- Add the following to your Podfile:
pod 'Bond', '~> 3.7'
- Run
pod install
with CocoaPods 0.36 or newer.
- Clone Bond as a submodule into the directory of your choice
git submodule add [email protected]:SwiftBond/Bond.git
git submodule update --init
- Drag Bond.xcodeproj into your project tree as a subproject
- Under your project's Build Phases, expand Target Dependencies
- Click the + and add Bond
- Expand the Link Binary With Libraries phase
- Click the + and add Bond
- Click the + at the top left corner to add a Copy Files build phase
- Set the directory to Frameworks
- Click the + and add Bond
Just get .swift files from Bond/ Directory and add them to your project.
Bond has yet to be shipped in an app. It was tested with many examples, but if there is a bug, please don't yell. Open an Issue, fix it yourself and make a pull request or contact me on Twitter (@srdanrasic) or by email ([email protected]). Should you have any suggestion or a critique, do the same.
https://github.com/SwiftBond/Bond/releases
The MIT License (MIT)
Copyright (c) 2015 Srdan Rasic (@srdanrasic)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.