Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Bond Tutorial: Bindings in Swift

$
0
0
Bond tutorial

The name’s Bond, Swift Bond

Note: Updated for Xcode 8.3, iOS 10.3, and Swift 3.1 by Tom Elliott. Original tutorial by Colin Eberhardt.

Almost any app you can think of needs to be able to fetch some sort of data from somewhere (disk, the network, the user) and display it somewhere else, possibly transforming or translating it along the way. Often you may want to show the same data in multiple ways.

The process of keeping all your UI elements in sync (often referred to as ‘wiring up’ your UI) is harder than it looks. Left unchecked it can rapidly lead to unmaintainable, difficult to modify spaghetti monstrosities. We’ve all been there :]

In this tutorial, you will use Bond to clarify and simplify the process of attaching what your users do to what they see on the screen.

Consider the seemingly trivial example of a slider with a text field.

Bond tutorial

In this example, you will update the text field to always show the same value as the slider. Updates made to the text field will also update the slider. For extra measure, you will connect buttons to set the slider to its maximum and minimum values.

With UIKit, you might commonly write code in an imperative manner – that is, by telling the program how it should do something, rather than declaring what it should do. In this example, you would tell the program “Hey! When somebody changes the slider, make the text field show the same value. And if they change the text field, make the slider show the same value. And while you’re at it, when somebody clicks the ‘Set to Minimum’ button set the slider down to zero. Oh, and don’t forget to set the text field to ‘0’ as well!”.

Instead, Bond allows us to write declarative code, focussing on the what without worrying about the how. “Hey! I want the slider and the text field to show the same value. And when somebody clicks the ‘Set to Minimum’ button, set the slider to zero”. In the language of Bond, you bind the value of the text field to the value of the slider.

Bond is a binding framework which sits on top of the ReactiveKit framework. ReactiveKit provides the base classes needed for functional reactive programming (FRP) in Swift. If you aren’t familiar with ReactiveKit, you may want to stop and review the project documentation on github.

Bond provides AppKit and UIKit bindings, reactive delegates and datasources to ReactiveKit. Bond makes it easy to bridge the FRP world with Apple’s APIs. Time to get binding with Bond … Swift Bond.

A Quick Bond Binding

To get started, download the starter project, which contains a ‘skeleton’ application. This will allow you to concentrate on Bond instead of fiddling with Interface Builder.

This project uses CocoaPods, so open the project using the BindingWithBond.xcworkspace file. (If you are not familiar with CocoaPods, check out this tutorial to help you get started.)

Now build and run. You will be greeted with the following:

Bond tutorial

All good? Then it’s time to create your first binding!

Open PhotoSearchViewController.swift and add the following to viewDidLoad():

_ = searchTextField.reactive.text.observeNext {
  text in
  if let text = text {
    print(text)
  }
}

The Bond framework adds various properties to UIKit controls, under the reactive proxy. In this case, reactive.text is a signal based on the text property of UITextField. Signals represent sequences of events that can be observed.

observeNext does just that, and executes its closure when the event occurs. Don’t worry, you’ll learn more about this shortly. For now, build and run, and then type “hello” to see what happens.

You will observe the following in the Xcode console:

h
he
hel
hell
hello

Each time you press a key, the closure in the code you just added executes; which results in the application logging the current state of the text field. This illustrates how events such as a property changing can be observed. Now you’re going to see what power that can bring.

Manipulating Data

Returning to PhotoSearchViewController.swift, update the code after super.viewDidLoad() as follows:

let uppercase = searchTextField.reactive.text
  .map { $0?.uppercased() }
 
_ = uppercase.observeNext {
  text in
  if let text = text {
    print(text)
  }
}

The map operation returns a signal by transforming the output of reactive.text into an uppercase string. The result is again printed in the observeNext closure, which is called as events arise.

Note: If you’re not familiar with map, check out Colin Eberhardt’s excellent tutorial on Functional Swift.

Build and run. Then type “hello” again:

H
HE
HEL
HELL
HELLO

You might have noticed that the result of applying map is itself a signal. Therefore you don’t need the intermediate assignment. Update your code as follows:

_ = searchTextField.reactive.text
  .map { $0?.uppercased() }
  .observeNext {
    text in
    if let text = text {
      print(text)
    }
}

This gives rise to an elegant fluent syntax. Build and run your application to see the same behavior.

You’re probably only mildly impressed at the moment, so get ready to try something absolutely awesome.

Replace your current code with the following:

_ = searchTextField.reactive.text
  .map { $0!.characters.count > 0 }
  .bind(to: activityIndicator.reactive.isAnimating)

The map transforms each text string to a boolean, based on the presence of characters in the searchTextField. The bind, unsurprisingly, binds that boolean to the isAnimating property of activityIndicator.

Build and run your application. You will notice that the activity indicator animates only when the text field has text within it! That’s a pretty impressive feat with just three lines of code — perhaps even more impressive than a classic James Bond car chase :]

As you can see, Bond provides a fluent and expressive way to describe how data flows between your application and your UI. This makes your applications simpler, easier to understand, and ultimately gets the job done more quickly.

In the next section you’ll learn more about how bindings actually work. But for now, it’s worth appreciating the power of Bond … Bond.

Bond tutorial

I’ll drink to that!

A Peek Under the Hood

Before building a more meaningful app, take a quick look at how Bond functions. I’d recommend navigating around the Bond types (via cmd+click) as you read the explanation below.

Starting with your current code, the map function returns a Signal which fires Events whenever the search text field text is changed. You can bind that signal to the activityIndicator.reactive.isAnimating property, which is of type Bond.

A Bond just holds a weak reference to the target (the activity indicator) and acts as an observer of the signal. Whenever the signal emits a value, the Bond calls its setter and updates the target with the given value.

Note: SignalProtocol is where the map operation is defined via a protocol extension. You might have heard of “protocol oriented programming,” and this is a great example.

As well as binding to signals you can also observe them. For example, consider the code you first added:

_ = searchTextField.reactive.text.observeNext {
    text in
    print(text)
  }

The call to observeNext sets up the given closure to be executed every time the text property of searchTextField changes.

Note: This is based on a classic design pattern known as the Observer Pattern.

Finally, consider the reactive.text property of searchTextField. Clearly, this is a Signal as you are able to observe it. reactive.text is referred to as an Observer – and these allow you to get values from the observed object.

You may also like to set the text. That is, you would like the text property of the text field to observe other objects. This requires a Subject – an object that is both a Signal and Observer.

Bond extends UIKit, adding corresponding Bond or Subject properties. You’ve already seen UITextField has a reactive.text observable property. You’ll explore observables later in this tutorial.

MVVM

While Bond makes it easy to bind UI properties directly to each other, this isn’t how you’ll likely want to use it in practice. Bond is a framework that supports the Model-View-ViewModel (MVVM) pattern. If you haven’t come across this pattern before, I’d recommend reading the first few sections of this tutorial on that topic.

That’s enough theory for now — you’re probably itching to write some more code. :]

Adding a View Model

It’s time to get started with a more realistic application structure. Create a new file named PhotoSearchViewModel.swift in the ViewModel group. Replace the contents with the following:

import Foundation
import Bond
import ReactiveKit
 
class PhotoSearchViewModel {
  let searchString = Observable<String?>("")
 
  init() {
    searchString.value = "Bond"
 
    _ = searchString.observeNext {
      text in
      if let text = text {
        print(text)
      }
    }
  }
}

This creates a simple view model with a single searchString property. The closure for observeNext will print the contents of searchString when signaled by a change.

Open PhotoSearchViewController.swift and add the following property near the top of the class:

private let viewModel = PhotoSearchViewModel()

Next add the following method to PhotoSearchViewController:

func bindViewModel() {
  viewModel.searchString.bind(to:searchTextField.reactive.text)
}

bindViewModel() is where you’ll add further bindings, but for now you just connect searchTextField to the underlying view model.

Now replace the contents of viewDidLoad() with the following:

super.viewDidLoad()
bindViewModel()

Build and run your application. You’ll see the text: “Bond”:

bond tutorial

Your view model currently observes changes to searchString, but you’ll find that as you type text into the text field, the view model is not being updated. What gives?

Your current binding is one-way, which means it only propagates changes from the source (your viewModel property) to the destination (the text field reactive.text property).

How do you make changes propagate the other way? Simple – in PhotoSearchViewController update the binding in bindViewModel() to appear as follows:

viewModel.searchString.bidirectionalBind(to:searchTextField.reactive.text)

That was easy! bidirectionalBind(to:context:) establishes a bond that also signals changes in the destination back to the source.

Now, to clean up your prior test, open PhotoSearchViewModel.swift and remove the following line from init():

searchString.value = "Bond"

Build and run, then type “Bond Love”:

B
Bo
Bon
Bond
Bond
Bond L
Bond Lo
Bond Lov
Bond Love

Great – you confirmed that the text field updates are being propagated back to the view model.

Creating Observable from Observables

It’s time to do something a bit more useful with an observable mapping. You’ll enforce a requirement that searches contain more than three characters by making the text red until this is true.

Returning to PhotoSearchViewModel.swift, add the following property to the view model:

let validSearchText = Observable<Bool>(false)

This boolean property will indicate whether the searchString value is valid or not.

Now, replace the code in the initializer with the following:

searchString
  .map { $0!.characters.count > 3 }
  .bind(to:validSearchText)

This maps the searchString observable to a boolean which is true when the string length is greater than three characters, then binds it to the validSearchText property.

In PhotoSearchViewController.swift, locate bindViewModel() and add the following:

viewModel.validSearchText
  .map { $0 ? .black : .red }
  .bind(to: searchTextField.reactive.textColor)

This maps the validSearchText property to a color, based on the boolean value. It then binds the resulting color to the textColor property of searchTextField.

Now build and run and type some text:

bond tutorial

When the text is too short to constitute a valid search, it is now colored red. It changes to black as soon as it’s considered valid.

Bond makes it very easy for you to connect and transform properties. As easy as…

bond tutorial

Pie! Yep, it really is this easy.

Preparing To Search

The app you’re building will query 500px to retrieve photos as the user types, giving the same kind of immediate feedback users are used to from Google.

You could trigger a search each time the searchString view model property changes, however this could result in TONS of requests. It’s much better to throttle the queries so that at most only one or two are sent per second.

With Bond, this is really easy to do!

In PhotoSearchViewModel.swift add the following method to the class for searching:

func executeSearch(_ text: String) {
  print(text)
}

For now this just logs the passed search string so that you can see the throttle in action.

Now add the following to the bottom of init():

_ = searchString
  .filter { $0!.characters.count > 3 }
  .throttle(seconds: 0.5)
  .observeNext {
    [unowned self] text in
    if let text = text {
      self.executeSearch(text)
    }
}

This filters the searchString to exclude invalid (i.e. length <= 3) values, then applies the Bond throttle operation, which does exactly what you require, throttling changes so that at most you receive one notification each 0.5 seconds. When the event fires, executeSearch(_:) is called to complete the search.

Build and run, then try typing some text.

You will see something similar to the following, where the throttle disposes of events so that at most the text is logged once per 0.5 seconds once the four character threshold is reached:

Bond
Bond Th
Bond Throttles
Bond Throttles Good!

It would take forever to add this sort of functionality without Bond. Impressive stuff!

Using the 500px API

The app you are building uses the 500px API. This was selected because it has a relatively simple interface and authentication mechanism.

Note: The starter application already has all the code you need to query 500px; you’ll find the code in the Model group.

To use the interface, you’ll have to register as a developer. Fortunately this is free, quick and easy!

Sign up via the following URL: https://500px.com/signup

Once you have signed up, navigate to your applications: https://500px.com/settings/applications. Here you will see an interface that allows you to register an application.

bond tutorial

Select Register your application and fill in the registration form (only a few fields are mandatory). Your application will be created immediately.

bond tutorial

Click on your app link and grab a copy of your consumer key. This is passed to 500px along with every request.

Open up Info.plist and edit the apiKey, adding the consumer key supplied by 500px:

bond tutorial

Return to PhotoSearchViewModel.swift and add the following lazy property to PhotoSearchViewModel:

private let searchService: PhotoSearch = {
  let apiKey = Bundle.main.object(forInfoDictionaryKey: "apiKey") as! String
  return PhotoSearch(key: apiKey)
}()

This initializes the PhotoSearch class, which provides a Swift API for querying 500px.

In PhotoSearchViewModel.swift, update the executeSearch(_:) method contents as follows:

var query = PhotoQuery()
query.text = searchString.value ?? ""
 
searchService.findPhotos(query) {
  result in
  switch result {
  case .success(let photos):
    print("500px API returned \(photos.count) photos")
  case .error:
    print("Sad face :-(")
  }
}

This constructs a PhotoQuery, which represents the query parameters, and then executes the search with the searchService. The result is returned asynchronously, with the result enumeration representing success or failure.

Build and run the application and type a string. The view model will execute the query and you’ll see the following logged:

500px API returned 20 photos

If something went wrong then you’ll see the following:

Sad face :-(

If this happens, double-check your API key, your internet connection, cross your fingers, and try again! If it still doesn’t work, then it’s likely the 500px API is down.

Rendering the Results

It would be much more interesting if you could actually see the photos that your query returns, wouldn’t it?

Open PhotoSearchViewModel.swift, and add the following property to PhotoSearchViewModel:

let searchResults = MutableObservableArray<Photo>([])

As the name suggests, MutableObservableArray is a special type of observable, one that supports arrays.

Before trying it out, take a look at the MutableObservableArray (and ObservableArray) in a bit more detail. Use cmd+click to navigate around the various Bond APIs.

ObservableArray is similar to Observable: it is also a Signal. This means you can subscribe to events from that signal when the array changes. In this case, the event emits ObservableArrayEvent instances.

ObservabelArrayEvent encodes the change that occurred to the array via the ObservableArrayChange enumeration. The events emitted by the ObservableArray are quite detailed. Rather than informing observers of the new array value, they instead describe the changes that have occurred.

There is a very good reason for this level of detail. You could use a Bond Observable to bind an array to the UI, via a table view perhaps. However, if you add a single item to this array, the Observable can only indicate that *something* has changed, and as a result the entire UI would have to be re-built. The detail provided by MutableObservableArray allows for much more efficient UI updates.

Now that you know what an observable array is, it’s time to put one to use.

In PhotoSearchViewModel.swift, locate executeSearch(_:) and update the success case as follows:

case .success(let photos):
  self.searchResults.removeAll()
  self.searchResults.insert(contentsOf: photos, at: 0)

This clears the array, adding the new results.

Open PhotoSearchViewController.swift, and add the following to the end of bindViewModel():

viewModel.searchResults.bind(to: resultsTable) { dataSource, indexPath, tableView in
  let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! PhotoTableViewCell
  let photo = dataSource[indexPath.row]
  cell.title.text = photo.title
 
  let backgroundQueue = DispatchQueue(label: "backgroundQueue",
                                      qos: .background,
                                      attributes: .concurrent,
                                      autoreleaseFrequency: .inherit,
                                      target: nil)
  cell.photo.image = nil
  backgroundQueue.async {
    if let imageData = try? Data(contentsOf: photo.url) {
      DispatchQueue.main.async() {
        cell.photo.image = UIImage(data: imageData)
      }
    }
  }
  return cell
}

Bond has a protocol extension for SignalProtocol that is specifically intended for binding observable arrays to table views. You use it here to bind searchResults to the resultsTable. The closure takes on a similar role to the standard table view datasource.

The closure is your standard table view cell wire-up. A cell is dequeued and various properties are set. Notice that the image is downloaded via a background queue in order to keep the UI responsive.

Build and run to see your application coming to life:

bond tutorial

Notice that as you type into the text field, it automagically updates. Very cool.

A Bit of UI Flair

Time to wire up a bit more of your UI!

Within PhotoSearchViewModel.swift, add the following property:

let searchInProgress = Observable<Bool>(false)

This Observable will be used to indicate when a search is in progress. Now update the contents of executeSearch(_:) to make use of it:

var query = PhotoQuery()
query.text = searchString.value ?? ""
 
searchInProgress.value = true
 
searchService.findPhotos(query) {
  [unowned self] result in
  self.searchInProgress.value = false
  switch result {
  case .success(let photos):
    self.searchResults.removeAll()
    self.searchResults.insert(contentsOf: photos, at: 0)
  case .error:
    print("Sad face :-(")
  }
}

This sets searchInProgress.value to true before querying 500px, then returns it with false when the result is returned.

In PhotoSearchViewController.swift, add the following to the bottom of bindViewModel():

viewModel.searchInProgress
  .map { !$0 }.bind(to: activityIndicator.reactive.isHidden)
 
viewModel.searchInProgress
  .map { $0 ? CGFloat(0.5) : CGFloat(1.0) }
  .bind(to: resultsTable.reactive.alpha)

This shows the activity indicator and reduces the opacity of resultsTable when a query is in progress.

Build and run to see this in action:

bond tutorial

By now you should really be starting to feel the benefits of Bond! And the effects of that martini :]

Handling Errors

Currently if the 500px query fails, your app just logs it to the console. It really should report any failure back to the user in a helpful and constructive fashion.

The problem is, how should this be modeled? An error doesn’t feel like it should be a view model property, since it is a transient occurrence rather than a change in state.

The answer is simple enough: rather than an Observable, all you need is a PublishSubject. In PhotoSearchViewModel.swift, add the following property:

let errorMessages = PublishSubject<String, NoError>()

Next, update the Error case of executeSearch(_:) as follows:

  case .error:
    self.errorMessages.next("There was an API request issue of some sort. Go ahead, hit me with that 1-star review!")

Within PhotoSearchViewController.swift, add the following to the bottom of bindViewModel:

_ = viewModel.errorMessages.observeNext {
   [unowned self] error in
 
  let alertController = UIAlertController(title: "Something went wrong :-(", message: error, preferredStyle: .alert)
  self.present(alertController, animated: true, completion: nil)
  let actionOk = UIAlertAction(title: "OK", style: .default,
    handler: { action in alertController.dismiss(animated: true, completion: nil) })
 
  alertController.addAction(actionOk)
}

This subscribes to the events emitted by the errorMessages property, displaying the supplied error message via a UIAlertController.

Build and run your application, then disconnect from the internet or remove your consumer key to see the error message in action:

bond tutorial

Perfect. :]

Adding Search Settings

The current application queries the 500px API based on a simple search term. If you look at the documentation for the API endpoint, you’ll see that it supports a number of other parameters.

If you tap the Settings button on your app, you’ll see it already has a simple UI for refining the search. You’ll wire that up now.

Add a new file named PhotoSearchMetadataViewModel.swift to the ViewModel group, with the following contents:

import Foundation
import Bond
 
class PhotoSearchMetadataViewModel {
  let creativeCommons = Observable<Bool>(false)
  let dateFilter = Observable<Bool>(false)
  let minUploadDate = Observable<Date>(Date())
  let maxUploadDate = Observable<Date>(Date())
}

This class is going to back the settings screen. Here you have an Observable property associated with each setting on that screen.

In PhotoSearchViewModel.swift, add the following property:

let searchMetadataViewModel = PhotoSearchMetadataViewModel()

Update executeSearch(_:), adding the following lines just after the current one that sets the query.text property:

query.creativeCommonsLicence = searchMetadataViewModel.creativeCommons.value
query.dateFilter = searchMetadataViewModel.dateFilter.value
query.minDate = searchMetadataViewModel.minUploadDate.value
query.maxDate = searchMetadataViewModel.maxUploadDate.value

This simply copies the view model state to the PhotoQuery object.

This view model will be bound to the settings view controller. It’s time to perform that wire-up, so open SettingsViewController.swift and add the following property after the outlets:

var viewModel: PhotoSearchMetadataViewModel?

So just how does this property get set? When the Settings button is tapped, the storyboard performs a segue. You can intercept this process in order to pass data to the view controller.

In PhotoSearchViewController.swift add the following method:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if segue.identifier == "ShowSettings" {
    let navVC = segue.destination as! UINavigationController
    let settingsVC = navVC.topViewController as! SettingsViewController
    settingsVC.viewModel = viewModel.searchMetadataViewModel
  }
}

This ensures that when the ShowSettings segue is executed, the view model is correctly set on the destination view controller.

In SettingsViewController.swift, add the following method:

func bindViewModel() {
  guard let viewModel = viewModel else {
    return
  }
  viewModel.creativeCommons.bidirectionalBind(to: creativeCommonsSwitch.reactive.isOn)
}

This code binds viewModel to the creativeCommonsSwitch.

Now add the following to the end of viewDidLoad():

bindViewModel()

Build and run the application, open settings and toggle the Creative Commons switch. You will notice its state persists, i.e. if you turn it on, it stays on, and the photos that are returned are different (you will need to click in the search bar again to force a new request).

bond tutorial

Binding the Dates

The settings view has a few other controls that need to be wired up, and that’s your next task.

Within SettingsViewController.swift, add the following to bindViewModel():

viewModel.dateFilter.bidirectionalBind(to: filterDatesSwitch.reactive.isOn)
 
let opacity = viewModel.dateFilter.map { $0 ? CGFloat(1.0) : CGFloat(0.5) }
opacity.bind(to: minPickerCell.leftLabel.reactive.alpha)
opacity.bind(to: maxPickerCell.leftLabel.reactive.alpha)
opacity.bind(to: minPickerCell.rightLabel.reactive.alpha)
opacity.bind(to: maxPickerCell.rightLabel.reactive.alpha)

This binds the date filter switch to the view model, and also reduces the opacity of the date picker cells when they are not in use.

The date picker cells contain a date picker and a few labels, with the implementation provided by the aptly-named DatePickerCell CocoaPod. However, this poses a little problem: Bond does not supply bindings for the properties exposed by this cell type!

If you take a peek at the API for DatePickerCell, you’ll see that it has a date property which sets both the label and the picker that it contains. You could bind bidirectionally to the picker, but this would mean the label setter logic is bypassed.

Fortunately a manual two-way binding, where you observe both the model and the date picker, is quite straightforward.

Add the following method to SettingsViewController.swift:

fileprivate func bind(_ modelDate: Observable<Date>, picker: DatePickerCell) {
  _ = modelDate.observeNext {
    event in
    picker.date = event
  }
 
  _ = picker.datePicker.reactive.date.observeNext {
    event in
    modelDate.value = event
  }
}

This helper method accepts a Date and a DatePickerCell and creates bindings between the two. The modelDate is bound to the picker.date and vice versa.

Now add these two lines to bindViewModel():

bind(viewModel.minUploadDate, picker: minPickerCell)
bind(viewModel.maxUploadDate, picker: maxPickerCell)

These bind the upload dates to the picker cells using your new helper method.

Build, run and rejoice! The dates are now correctly bound:

bond tutorial

Note: The 500px API does not support date filtering; this is being applied to the photos returned by the code in the Model group; as a result, you can easily filter out all of the photos returned by 500px. For a better result, try integrating with Flickr, which does support server-side date filtering.

Date Constraints

Currently, the user can create date filters that don’t make sense, i.e. where the minimum date is after the maximum.

Enforcing these constraints is really rather easy. Within PhotoSearchMetadataViewModel.swift, add the following initializer:

init() {
  _ = maxUploadDate.observeNext {
    [unowned self]
    maxDate in
    if maxDate.timeIntervalSince(self.minUploadDate.value) < 0 {
      self.minUploadDate.value = maxDate
    }
  }
  _ = minUploadDate.observeNext {
    [unowned self]
    minDate in
    if minDate.timeIntervalSince(self.maxUploadDate.value) > 0 {
      self.maxUploadDate.value = minDate
    }
  }
}

The above simply observes each date, and if the situation where min > max arises, the values are changed accordingly.

Build and run to see the code in action. You should try setting the max date to something less than the min date. It’ll change the min date accordingly.

You might have noticed a small inconsistency: when you change the search settings, the app doesn’t repeat the query. This could be a bit confusing for your user. Ideally the application should execute the search if any of the settings change.

Each setting is an observable property, so you could observe each of one of them; however, that would require a lot of repetitive code. Fortunately, there’s a better way!

Within PhotoSearchViewModel.swift, add the following to the end of init():

_ = combineLatest(searchMetadataViewModel.dateFilter, searchMetadataViewModel.maxUploadDate,
                  searchMetadataViewModel.minUploadDate, searchMetadataViewModel.creativeCommons)
  .throttle(seconds: 0.5)
  .observeNext {
    [unowned self] _ in
    self.executeSearch(self.searchString.value!)
}

The combineLatest function combines any number of observables, allowing you to treat them as one. The above code combines, throttles, then executes the query.

Build and run to see it in action. Try changing the dates, or either of the toggles. The search results will update after each time you change something!

How easy was that?! :]

bond tutorial

A piece of cake, right?

Where To Go From Here?

You can find the finished project here.

Hopefully you’ve enjoyed this Bond tutorial and seen how easily you can wire-up your UI with Bond. Now it’s time to go forth and Bond … Swift Bond.

If you have any questions or comments, feel free to join the discussion below!

The post Bond Tutorial: Bindings in Swift appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles