Swiftpack.co - Package - DeclarativeHub/Bond

Bond, Swift Bond

Platform CI Status Twitter


Update: Bond 7 has been released! Check out the migration guide to learn more about the update.

Bond is a Swift binding framework that takes binding concepts to a whole new level. It's simple, powerful, type-safe and multi-paradigm - just like Swift.

Bond is built on top of ReactiveKit and bridges the gap between the reactive and imperative paradigms. You can use it as a standalone framework to simplify your state changes with bindings and reactive data sources, but you can also use it with ReactiveKit to complement your reactive data flows with bindings, reactive delegates and reactive data sources.

Bond is a backbone of the Binder Architecture - a preferred architecture to be used with the framework.

Why use Bond?

Say that you would like to do something when text of a text field changes. Well, you could setup the target-action mechanism between your objects and go through all that target-action selector registration pain, or you could simply use Bond and do this:

textField.reactive.text.observeNext { text in
    print(text)
}

Now, instead of printing what the user has typed, you can bind it to a label:

textField.reactive.text.bind(to: label.reactive.text)

Because binding to a label text property is so common, you can even do:

textField.reactive.text.bind(to: label)

That one line establishes a binding between the text field's text property and label's text property. In effect, whenever user makes a change to the text field, that change will automatically be 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. As Bond is backed by ReactiveKit it has full confidence in functional paradigm.

textField.reactive.text
  .map { "Hi " + $0 }
  .bind(to: 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 have used reactive.text property of the text field. It is an observable representation of the text property provided by Bond framework. There are many other extensions like that one for various UIKit components. They are all placed within the .reactive proxy.

For example, to observe button events do:

button.reactive.controlEvents(.touchUpInside)
  .observeNext { e in
    print("Button tapped.")
  }

Handling touchUpInside event is used so frequently that Bond comes with the extension just for that event:

button.reactive.tap
  .observeNext {
    print("Button tapped.")
  }  

You can use any ReactiveKit operators to transform or combine signals. Following snippet depicts how values of two text fields can be reduced to a boolean value and applied to button's enabled property.

combineLatest(emailField.reactive.text, passField.reactive.text) { email, pass in
    return email.length > 0 && pass.length > 0
  }
  .bind(to: button.reactive.isEnabled)

Whenever user types something into any of these text fields, expression will be evaluated and button state updated.

Bond's power is not, however, in coupling various UI components, but in the binding of the business logic layer (i.e. Service or View Model) to the View layer and vice-versa. Here is how one could bind user's number of followers property of the model to the label.

viewModel.numberOfFollowers
  .map { "\($0)" }
  .bind(to: label)

Point here is not in the simplicity of a value assignment to the text property of a label, but in the creation of a binding which automatically updates label text property whenever the 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 too):

viewModel.username
  .bidirectionalBind(to: usernameTextField.reactive.text)

Bond is also great for observing various different events and asynchronous tasks. For example, you could observe a notification like this:

NotificationCenter.default.reactive.notification("MyNotification")
  .observeNext { notification in
    print("Got \(notification)")
  }
  .dispose(in: bag)

Let me give you one last example. Say you have an array of repositories you would like to display in a collection 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 collection view's cell. Additionally, when user does 'pull down to refresh' and your array gets new repositories, you want those in collection view too.

So how do you proceed? Well, instead of implementing a data source object, observing photo downloads with KVO and manually updating the collection view with new items, with Bond you can do all that in just few lines:

repositories.bind(to: collectionView) { array, indexPath, collectionView in
  let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! RepositoryCell
  let repository = array[indexPath.item]

  repository.name
    .bind(to: cell.nameLabel)
    .dispose(in: cell.onReuseBag)

  repository.photo
    .bind(to: cell.avatarImageView)
    .dispose(in: cell.onReuseBag)

  return cell
}

Yes, that's right!

Reactive Extensions

Bond is all about bindings and other reactive extensions. To learn more about how bindings work and how to create your own bindings check out the documentation on bindings.

If you are interested in what bindings and extensions are supported, just start typing .reactive. on any UIKit or AppKit object and you will get the list of available extensions. You can also skim over the source files to get an overview.

Observable Collections

When working with arrays usually we need to know how exactly did an array change. New elements could have been inserted into the array and old ones deleted or updated. Bond provides mechanisms for observing such fine-grained changes.

For example, Bond provides you with a (Mutable)ObservableArray type that can be used to generate and observe fine-grained changes.

let names = MutableObservableArray(["Steve", "Tim"])

...

names.observeNext { e in
  print("array: \(e.collection), diff: \(e.diff), patch: \(e.patch)")
}

You work with the observable array like you would work with the array it encapsulates.

names.append("John") // prints: array: ["Steve", "Tim", "John"], diff: Inserts: [2], patch: [I(John, at: 2)]
names.removeLast()   // prints: array: ["Steve", "Tim"], diff: Deletes: [2], patch: [D(at: 2)]
names[1] = "Mark"    // prints: array: ["Steve", "Mark"], diff: Updates: [1], patch: [U(at: 1, newElement: Mark)]

Peek into observable collections documentation to learn more about observable collections.

Data Source Signals

Observable collections and other data source signals enable us to build powerful UI bindings. For example, an observable array can be bound to a collection view just like this:

names.bind(to: collectionView, cellType: UserCell.self) { (cell, name) in
    cell.titleLabel.text = name
}

No need to implement data source objects and do everything manually. Check out documentation on the data source signals to learn more about them and about table or collection view bindings.

Protocol Proxies

Bond provides NSObject extensions that make it easy to convert delegate method calls into signal. The extensions are built on top of ObjC runtime and enable you to intercept delegate method invocations and convert them into signal events.

Bond uses protocol proxies to implement table and collection view bindings and to provide signals like tableView.reactive.selectedRowIndexPath. Check out the protocol proxies documentation to learn more.

Community Extensions

Make sure to check out Extensions directory. It contains extensions that make Bond easy to use with other frameworks and libraries, like Realm.

If you have an extensions that makes your favourite framework work with Bond and you'd like to share it with everyone, we'd be more than happy to accept your PR.

Requirements

  • iOS 8.0+ / macOS 10.11+ / tvOS 9.0+
  • Swift 4.2

Communication

  • If you'd like to ask a question, open an issue.
  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, submit a pull request (include unit tests).

Installation

Carthage

  1. Add the following to your Cartfile:

    github "DeclarativeHub/Bond"
    
  2. Run carthage update

  3. Add the framework as described in Carthage Readme

Accio

  1. Add the following to your Package.swift:

    .package(url: "https://github.com/DeclarativeHub/Bond.git", .upToNextMajor(from: "7.4.1")),
    
  2. Next, add Bond to your App targets dependencies like so:

    .target(
        name: "App",
        dependencies: [
            "Bond",
        ]
    ),
    
  3. Then run accio update.

CocoaPods

  1. Add the following to your Podfile:

    pod 'Bond'
    
  2. Run pod install.

License

The MIT License (MIT)

Copyright (c) 2015-2019 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.

Github

link
Stars: 4046

Used By

Total: 0

Releases

7.7.1 - 2020-04-21 07:04:39

  • Fix compilation issue on macOS.

7.7.0 - 2020-04-20 18:22:07

Thanks to @ibrahimkteish's awesome contribution, Bond 7.7 introduces the ability to observe view controller lifecycle events! 🎉 🎈

Example usage:

let viewController = UIViewController()

viewController.reactive.lifecycleEvents.observeNext { (event) in
    print(event)
}

viewController.reactive.lifecycleEvent(.viewDidLoad).observeNext {
    print("view did load")
}

viewController.reactive.lifecycleEvent(.viewWillAppear).observeNext {
    print("view will appear")
}

viewController.reactive.lifecycleEvent(.viewDidAppear).observeNext {
    print("view did appear")
}

viewController.reactive.lifecycleEvent(.viewWillDisappear).observeNext {
    print("view will disappear")
}

viewController.reactive.lifecycleEvent(.viewDidDisappear).observeNext {
    print("view did disappear")
}

7.6.6 - 2019-12-17 17:19:57

  • Make collectionView(_: numberOfItemsInSection:) open on CollectionViewBinderDataSource. Thanks @AnthonyMDev!
  • Fix KVO on iOS 10. Thanks @Sephiroth87.

7.6.5 - 2019-12-15 14:20:55

  • Fix Retain Cycle in Swift KeyPath KVO. Thanks @AnthonyMDev!

7.6.4 - 2019-12-13 07:58:55

  • Fix macOS and tvOS compilation. Thanks @cyrusingraham!

7.6.3 - 2019-12-12 13:56:28

  • Introduce MainThreadDisposable. Thanks @diegoRodriguezAguila and @ibrahimkteish!

7.6.2 - 2019-12-12 10:52:32

  • Handle threading changes in ReactiveKit.

7.6.1 - 2019-11-13 19:15:31

  • Update dependencies.

6.10.5 - 2019-07-08 14:14:22

  • ReactiveKit 3.13 compatibility

7.6.0 - 2019-06-30 11:01:33

  • Update to latest ReactiveKit and Differ. Thanks @tonyarnold!

7.5.0 - 2019-04-27 09:49:38

  • Add support for NSCollectionView data source bindings.

7.4.3 - 2019-04-18 20:15:08

  • Fix issues with removeAll on observable trees.

7.4.2 - 2019-04-12 21:34:42

  • Setup Bond dependencies without using submodules.

7.4.1 - 2019-04-02 06:48:02

  • Fix UICollectionView selectedItemIndexPath inaccessible issue. Thanks @madiguzel!

7.4.0 - 2019-03-31 12:11:49

🎊 Official Swift 5 support 🎉

  • Update code syntax to Swift 5
  • Update ReactiveKit dependency to 3.10

Do not update if you are still using Swift 4.2 or lower.

6.10.4 - 2019-03-31 10:57:04

  • ReactiveKit 3.10 (Swift 5) support.

7.3.3 - 2019-03-26 21:18:06

  • Add method replaceItems(ofSectionAt:with:performDiff:) to MutableObservableArray2D that can replace items of a section at the given index with new items and calculate change diff. For example:
data.replaceItems(ofSectionAt: 1, with: [1, 100, 20], performDiff: true)

6.10.3 - 2019-03-26 20:06:24

  • Make Bond v6 work with Swift 5 / Xcode 10.2.

7.3.2 - 2019-03-26 08:04:02

  • Fix compilation with Swift 5 (Xcode 10.2). Thanks @jonathanfoster!

7.3.1 - 2019-03-25 19:54:17

  • Make Array2D.Section initializer public.

7.3.0 - 2019-03-16 13:30:17

Bond v7.3 improves ergonomics around Tree types.

Simpler conformance

Trees no long need to be index-based. Conforming to TreeProtocol (renamed from TreeNodeProtocol) now requires only that the children property is provided.

/// A protocol that provides abstraction over a tree type.
/// A tree can be any containter type that encapsulates objects or values that are also trees.
public protocol TreeProtocol {

    /// A collection of child nodes that are trees and whose children are also trees
    associatedtype Children: Collection where
        Children.Element: TreeProtocol,
        Children.Element.Children == Children,
        Children.Index == Int

    /// Child nodes of the current tree node.
    var children: Children { get }
}

Not that you'll ever need to conform UIView, bet let's use it as an example:

extension UIView: TreeProtocol {

    public var children: [UIView] {
        return subviews
    }
}

This will automatically provide you index path subscripting capability:

let pickerView = UIPickerView()

let secondSubviewOfFirstSubview = pickerView[childAt: [0, 1]]

Tree Views

Consuming trees is now done through "tree views". Bond provides two views out of the box: DFS and BFS views thorough .depthFirst and .breadthFirst properties on any TreeProtocol.

For example, the snippet

for descendent in pickerView. depthFirst {
    print(descendent)
}

will print all descendent views of the pickerView in depth-first search order. Note that descendent implies not only children, but also children of the children and so on.

Tree view is a flat collection of tree nodes. It's of type Collection whose indices are of type IndexPath. That means that we can do all the stuff on trees that we can do with collections, like:

let firstImageViewDescendent = pickerView.breadthFirst.first(where: { $0 is UIImageView })

let allVisible = pickerView.breadthFirst.allSatisfy { !$0.isHidden }

let totalWidth = pickerView.breadthFirst.reduce(0, { $0 + $1.frame.size.width })

Mutable Trees

To add support for various mutating methods on the tree, conform to RangeReplaceableTreeProtocol protocol. All that is needed is to add setter to children property:

extension UIView: RangeReplaceableTreeProtocol {

    public var children: [UIView] {
        get {
            return subviews
        }
        set {
            subviews.forEach { $0.removeFromSuperview() }
            newValue.forEach { addSubview($0) }
        }
    }
}

We can then do stuff like inserting, deleting or moving children:

// Moves child at index path [0, 1] to index path [2, 3]
pickerView.move(from: IndexPath(indexes: [0, 1]), to: IndexPath(indexes: [2, 3]))

(Mutable) Observable Trees

Now that our tree type conforms to TreeProtocol (or RangeReplaceableTreeProtocol) we can wrap it onto MutableObservableTree and use the wrapper to enable observation of the tree changes:

let tree = MutableObservableTree(pickerView)

tree.observeNext { changeset in
    print(changeset.collection, changeset.diff, changeset.patch)
}

tree.insert(UIImageView(), at: [0, 0])

Simpler Array2D

Array2D is refactored into a simple struct. Changes should be backward compatible if Array2D typealias has been used.

public struct Array2D<SectionMetadata, Item>: Array2DProtocol {

    /// Represents a single section of Array2D.
    public struct Section {

        /// Section metadata, e.g. section title.
        public var metadata: SectionMetadata

        /// Items contained in the section.
        public var items: [Item]
    }

    /// All sections of Array2D.
    public var sections: [Section]

    /// Create a new Array2D with the given sections.
    public init(sections: [Section] = []) {
        self.sections = sections
    }
}

7.2.1 - 2019-03-03 15:16:25

  • Support for Swift 5 compiler (in Swift 4.2 compatibility mode).

Note that this does not update the project to use Swift 5 syntax, it only makes the project compilable with Swift 5 compiler by reworking parts that are affected by Swift 5 breaking changes. You can keep using Swift 4.2 (Xcode 10/10.1).

7.2.0 - 2019-02-17 12:13:53

  • Add firstIndex method to trees (#587)
  • Fix broken removeAllItems method on trees / 2D arrays (#584)
  • Add support for binding of collection signals to changeset containers like MutableObservableArray (#580)
  • Add data source binding extensions to UIPickerView - thanks @jonathanfoster! (#577)

7.1.0 - 2019-02-09 11:38:53

  • Support for Tree diffing. Also implies Array2D diffing.

You can now calculate diffs between trees. For example, a mutable tree can be updated like this:

aMutableTree.replace(with: newTree, performDiff: true)

7.0.0 - 2018-12-16 15:59:10

Bond 7 brings refactored observable collections that are much more powerful and makes it easy to customize binders and create your own variants of observable collections. Anything that conforms to Swift.Collection can now be made observable. Bond supports observable trees now! Check out observable collections documentation, new playgrounds in the project workspace and the migration guide.

Bond 7 updates only observable collections APIs. All other APIs remain unchanged.

7.0.0-beta.2 - 2018-11-22 16:41:55

  • Expose convenience collection methods on immutable changeset container.
  • Add removeSubrange() method to mutable observable array.
  • Make UIButton image and backgroundImage bonds of optional type.
  • Make table and collection view properties of binder data source open.

7.0.0-beta.1 - 2018-11-11 14:49:14

Bond 7 brings refactored observable collections that are much more powerful and makes it easy to customize binders and create your own variants of observable collections. Anything that conforms to Swift.Collection can now be made observable. Bond also supports observable trees now! Check out observable collections documentation and new playgrounds in the project workspace.

Bond 7 updates only observable collections APIs. All other APIs remain unchanged. APIs for use cases like creating, mutating and binding collections remain mostly unchanged, however there are breaking changes in the collection binders and the observable collection event type. Make sure to check out playgrounds in the project workspace to learn about new stuff.

6.10.2 - 2018-10-13 13:14:36

  • Fix submodules protocol (ssh -> https).

6.10.1 - 2018-10-13 07:57:41

  • Update dependencies

6.10.0 - 2018-09-18 16:32:16

  • Swift 4.2 support - thanks @maurovc!