Bond Tutorial: Bindings in Swift

Bond is a simple, powerful, type-safe binding framework for Swift. Learn how to use it with the popular MVVM architectural pattern in this Bond tutorial. By Tom Elliott.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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?! :]

A piece of cake, right?

bond tutorial