Cocoa Bindings on macOS

Andy Pereira

Update note: This Cocoa Bindings on macOS tutorial has been updated to Xcode 8 and Swift 3 by Andy Pereira. The original tutorial was written by Jake Gundersen.

Cocoa-Bindings-on-macOS

Cocoa bindings, or simply bindings, free you up from having to spend hours writing glue code; that is, creating links between the model and the view in the controller when using the Model-View-Controller (MVC) pattern.

Cocoa bindings have a simple goal: write less code. You’ll discover as you work through this that they do indeed live up to this objective.

In this Cocoa Bindings on macOS tutorial, you’ll create an app that displays search results from the App Store via the iTunes API as you learn how to use Cocoa bindings to do the following:

  • Set the relationship within Interface Builder between a data model property and a UI element, such as a label or a button
  • Set up default values
  • Apply formatting, such as a currency or date format
  • Change the data structure, e.g. when converting a value into a representing color

cb-rageface1

Getting Started

Although you won’t have to spend the next several of your hours coding, there’s still a fair amount of work ahead in Interface Builder using Auto Layout, so familiarity with both of these tools is a prerequisite.

Download the starter project here.

Build and run the project, and you’ll see it has a nice interface — but no data.

First run

You’ll also see there are a couple of files to help you along. iTunesRequestManager.swift contains a struct with two static methods. The first sends a query to the iTunes API and downloads a JSON payload that contains iOS apps results for a provided search string, while the second is a helper method to download an image asynchronously.

The second file, iTunesResults.swift, defines a data model class that matches the data downloaded by the iTunes search method.

Note: All variables in the Result class are defined as dynamic. This is because bindings rely on key-value coding, and therefore require the Objective-C runtime. Adding the dynamic keyword guarantees that access to that property is always dynamically dispatched using the Objective-C runtime.

The class inherits from NSObject; this is also a requirement for bindings. You’ll discover why a little later, when you add a variable to your view controller class.

Searching via iTunes

First, you’re going to retrieve search results via the iTunes API and add them to an NSArrayController.

Open Main.storyboard and look at the objects in the View Controller Scene. Note that objects on which you’ll set bindings all have the labels ‘(Bind)’.

Setting Up an NSArrayController

The NSArrayController object manages the content of a NSTableView. This content often takes the form of an array of model objects.

Note: NSArrayController offers much more than a simple array, including managing object selection, sorting and filtering. Cocoa Bindings make heavy use of this functionality.

Open Main.storyboard. Find an NSArrayController object in the Object Library, and drag it into the list of objects under the View Controller Scene grouping in the document outline:

add array controller

Next, open the assistant editor and make sure ViewController.swift is the file you’re working on. Control-drag from the Array Controller object in the Storyboard to the ViewController.swift source to add an outlet to it, and name it searchResultsController:

add outlet

Adding the Search Box and Button

Now you’re ready to use the search box and button to get a list of search results and add them to the searchResultsController object.

Control-drag from the search button in the storyboard to the ViewController.swift source to create an action method. Select to create an Action connection and name it searchClicked.

addConnectionMethod

Now add the following code inside searchClicked(:_) :

//1
if (searchTextField.stringValue == "") {
  return
}
//2
guard let resultsNumber = Int(numberResultsComboBox.stringValue) else { return }
//3
iTunesRequestManager.getSearchResults(searchTextField.stringValue,
  results: resultsNumber,
  langString: "en_us") { results, error in
    //4
    let itunesResults = results.map { return Result(dictionary: $0) }
 
    //Deal with rank here later  
 
    //5
    DispatchQueue.main.async {
      //6
      self.searchResultsController.content = itunesResults
      print(self.searchResultsController.content)
  }
}

Taking each line in turn:

  1. Check the text field; if it’s blank, don’t send that query to iTunes search API.
  2. Get the value in the dropdown. This number is passed to the API and controls how many search results to return. There’s a number of preconfigured options in the dropdown, but you can also type in other numbers — 200 is the maximum.
  3. Make a call to getSearchResults(_:results:langString:completionHandler:). This passes in the number of results from the combo box and the query string you typed into the text field. It returns, via a completion handler, either an array of Dictionary result objects or an NSError object if there’s a problem completing the query. Note that the method already handles the JSON parsing.
  4. Here you use some Swift-style array mapping to pass the dictionary into an initialization method that creates a Result object. When done, the itunesResults variable contains an array of Result objects.
  5. Before you can set this new data on the searchResultsController, you need to make sure you’re on the main thread. Therefore you use DispatchQueue.main.async to get to the main queue. You haven’t set up any bindings, but once you have, altering the content property on the searchResultsController will update the NSTableView (and potentially other UI elements) on the current thread. Updating UI on a background thread is always a no-no.
  6. Finally you set the content property of the NSArrayController. The array controller has a number of different methods to add or remove objects it manages. Each time you search, you want to clear out the previous results and work with the results of the latest query. For now, print the content of searchResultsController to the console to verify that everything works as planned.

Now, add the following ViewController extension:

extension ViewController: NSTextFieldDelegate {
  func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
    if commandSelector == #selector(insertNewline(_:)) {
      searchClicked(searchTextField)
    }
    return false
  }
}

This invokes searchClicked(_:) when the user presses Enter in the text field, just as if you’d clicked the search button. This simply makes it easier to run a quick search using the keyboard.

Build and run, type flappy into the search bar and press Enter or click the Search button. You should see something like the following show up in the console:

Search result

Your First Bindings

It’s time to get to the meat of this tutorial!

allthethings1

The first step is to bind the array controller to the table view.

Open up Main.storyboard and select the table view titled Search Results Table View (Bind). Open the Bindings Inspector: it’s the second-to-last-icon in the right pane, just before the View Effects Inspector.

Expand the Content option under the Table Contents heading. Check the box next to ‘Bind to ‘ and make sure Search Results Controller is displayed in the dropdown box. Finally, make sure the Controller Key is set to arrangedObjects, like so:

TableViewBinding

Build and run, and search for something you know will return a large number of results. You’ll see at most five results, unless you changed the number in the dropdown. Thanks to the bindings, the array controller automatically represents its content in the table view. However, they all say “Table View Cell”.

arrayboundnotitlesintable

You’re getting a bunch of duplicate hits because the text fields in the cells have no idea which properties on the data model they should read.

Binding Text Fields to Their Properties

Open up Main.storyboard and go to the View Controller Scene. Expand the objects in the table view until you find the text field named Title TextField (Bind). Select this object and open the Bindings Inspector.

Expand the Value option and bind to the Table Cell View object. Ensure the Model Key Path is objectValue.trackName.

objectValue is a property on the table cell view that gets set by the NSTableView on each cell view object from its binding to the table.

objectValue is, in this case, equal to the Result model object for that row.

trackName

Repeat the above process for Publisher TextField (Bind) by binding the value of this element to objectValue.artistName.

Build and run, and search again. This time, the title and publisher show up:

title and publisher

Adding in Rank

How about that missing rank column? Rank isn’t set on the data model object you get from iTunes. However, the order of the results from iTunes does tell you the order in which they display on a device when searching iTunes.

With a little more work you can set the rank value.

Add the following code in ViewController under the //Deal with rank here later comment:

.enumerated()
.map({ index, element -> Result in
  element.rank = index + 1
  return element
})

This code calls enumerated() in order to get the index and the object at the index. Then it calls map(:_) to set the rank value for each object and return an array with that result.

Now, go back to Main.storyboard, select Rank TextField (Bind) and open the Bindings Inspector. In the Value section, bind to the Table Cell View. Make sure Controller Key is empty, and set Model Key Path to objectValue.rank.

Build and run, and the app shows the rank in the first column of the table view:

ranktitlepublisher

Now you need to bind the Result object selected by the user to the rest of the UI.

Binding a Table View’s Selection

Binding to a selection in a table involves two steps:

  1. You first bind the NSArrayController to the table selection.
  2. Then you can bind the properties of the selection object in the NSArrayController to the individual labels and other properties.

Open Main.storyboard. Select the Search Results Table View (Bind) and open the Bindings Inspector.

Expand the Selection Indexes option in the Table Content section. Check Bind to the Search Results Controller object.

Enter selectionIndexes into the Controller Key box. The table has a selectionIndexes property that contains a set of indexes that the user has selected in the table.

In this case, I’ve set the table view to only allow a single selection. You could work with more than one selection if your app requires it, similar to how Finder lets you select multiple files.

selectionIndexes

The NSArrayController object has a selection property that returns an array of objects. When you bind the selectionIndexes property from the table view to the array controller, the selection property will be populated with the objects in the array controller that correspond to the indexes selected in the table.

The next step is to bind the labels and other UI elements to the selected object.

Find and select the App Name Label (Bind). Bind its value to the Search Results Controller. Controller Key should be selection, and Model Key Path should be trackName.

mainTrackName

Build and run, select any app in the table view and its title will appear in the text field:

Main Title

You’ve seen how easy it can be to get data from your model into your UI. But what if the data needs to be formatted in some way, such as a currency or as a date?

Luckily, there’s a built-in set of objects that make it easy to change the way a specific piece of data is displayed in a label.

Formatting Bound Data

Find the label titled Price Label (Bind). Bind it to the Search Results Controller object, and ensure Controller Key is selection.

Set Model Key Path to price. Next, find a Number Formatter in the Object Library. Drag it to the NSTextFieldCell named Label, just under the Price text field.

Finally, select the Number Formatter, open the Attributes Inspector and change the Style to Currency.

When you’re done, your storyboard and inspector should look like the following:

currencyFormatter

Build and run, select any app from the list and the currencies should all display correctly:

priceformatted

Note: Number formatters are very powerful. In addition to currencies, you can also control how many digits follow a decimal point, percentages, or have the number spelled out in words.

There are formatter objects for dates, byte counts and several other less-common situations. If none of those suit your needs, you can even create your own custom formatters.

Formatting as Bytes

You’ll be using a Byte Count Formatter next to show the file size.

Find and select File Size Label (Bind), open the Bindings Inspector and bind it to the Search Results Controller. Set Controller Key to selection and Model Key Path to fileSizeInBytes.

Then find a Byte Count Formatter in the Object Library and attach it to the NSTextFieldCell. There’s no need to configure anything here; the default settings on a byte formatter will work just fine.

You should see your byte count formatter in the document outline like so:

byte count formatter

Build and run, select an app in the list, and you’ll see file size using the proper units, such as KB, MB, and GB:

byte count final

You’re quite used to binding things by now, so here’s a short list of the remaining keys you need to bind:

  • Bind the Artist Label (Bind) to artistName.
  • Bind the Publication Date (Bind) to releaseDate.
  • Add a Date Formatter; the default settings are fine.
  • Bind the All Ratings Count (Bind) to userRatingCount.
  • Bind the All Ratings (Bind) to averageUserRating.
  • Bind the Genre Label (Bind) to primaryGenre.

All these labels should be bound to the Search Results Controller and the selection controller key.

For more precision in your UI, you can also bind the Description Text View (Bind), the Attributed String binding, to the itemDescription Model Key Path. Make sure you bind the NSTextView, which is several levels down in the hierarchy, not the NSScrollView which is at the top.

selecttextView

Build and run, and you’ll see most of the UI populate:

mostly populated

Binding Images

The next step is to bind the image for the icon to the Icon Image View. This is a little trickier because the JSON doesn’t contain the actual image, but instead a URL for the image.

Result includes a method to download the image file and make it available as an NSImage on the artworkImage property.

Just-in-Time Downloads

You don’t want to download all the icons at once — just the one for the current selection in the table. You’ll download a new icon whenever the selection changes.

Add the following method to ViewController:

//1
func tableViewSelectionDidChange(_ notification: NSNotification) {
  //2
  guard let result = searchResultsController.selectedObjects.first as? Result else { return }
  //3
  result.loadIcon()
}

Here’s the play-by-play:

  1. tableViewSelectionDidChange(_:) fires every time the user selects a different row in the table.
  2. The array controller property selectedObjects returns an array containing all the objects for the indexes of the rows selected in the table. In your case, the table will only permit a single selection, so this array always contains a single object. You store the object in the result object.
  3. Finally, you call loadIcon(). This method downloads the image on a background thread and then updates the Result objects artworkImage property when the image downloads on the main thread.

Binding the Image View

Now that your code is in place, you’re ready to bind the image view.

Head back to Main.storyboard, select the Icon Image View (Bind) object and open the Bindings Inspector.

Go to the Value section and bind to the Search Results Controller, set Controller Key to selection and Model Key Path to artworkImage.

Did you notice the Value Path and a Value URL sections? Both of these bindings are intended for local resources only. You can connect them to a network resource, but that will block the UI thread until the resource has finished downloading.

Build and run, search for fruit and then select a row. You’ll see the icon image appear once it has downloaded:

icon populated

The collection view beneath the description text view is currently looking a little bare. Time to populate that with some screenshots.

Populating the Collection View

First you’ll bind the collection view to the screenShots property and then ensure the screenShots array populated correctly.

Select the Screen Shot Collection View (Bind). Open the Bindings Inspector and expand the Content binding in the Content group.

Bind to the Search Results Controller, set Controller Key to selection and Model Key Path to screenShots.

The screenShots array starts out empty. loadScreenShots() downloads the image files and populates the screenShots array with NSImage objects.

Add the following line to ViewController.swift, in tableViewSelectionDidChange(_:), right after result.loadIcon():

result.loadScreenShots()

This populates the screenshot images and creates the right number of views.

The next thing you need to do is set the right collection view item prototype. Although the collection view item scene is present in the storyboard, it’s not connected to the collection view. You’ll have to create this connection in code.

Add the following code to the end of viewDidLoad() in ViewController.swift:

let itemPrototype = self.storyboard?.instantiateController(withIdentifier:
  "collectionViewItem") as! NSCollectionViewItem
collectionView.itemPrototype = itemPrototype

Now that the collection view knows how to create each item via the prototype, you need to provide the content for each item via a binding.

Open Main.storyboard and select Screen Shot Image View (Bind) inside the Collection View Item Scene. You’ll find this floating next to the main view controller.

Bind the Value option to the Collection View Item object. The controller key should be blank, and Model Key Path should be representedObject.

The representedObject property represents the object in the collection view array for that item; in this case, it’s an NSImage object.

Build and run, and you’ll see see the screenshots appearing below the description text:

screenshots

Great work! There’s just a few more features of Cocoa Bindings to cover before wrapping up.

Binding Other Properties

Your UI could use a bit of feedback. Users don’t like to stare at static screens when something is loading, and they’ll assume the worst when nothing happens in response to a user action.

Instead of leaving a static screen, you’ll show a spinner to the user while the image downloads.

Seting Up a Progress Spinner

You can easily bind a progress spinner to a new property in the ViewController. Add the following property to ViewController:

dynamic var loading = false

Loading requires two things in order to work correctly: the dynamic keyword, and the parent class to be a subclass of NSObject. Bindings relies on KVO (Key Value Observing), and a Swift class that doesn’t inherit from NSObject wouldn’t be able to use KVO.

Add the following line of code inside searchClicked(:_), right before the line that executes the call to getSearchResults(_:results:langString:completionHandler:):

loading = true

Locate the line in the same method that sets the content property on searchResultsController. Add the following code immediately before that line:

self.loading = false

Next, open Main.storyboard and select Search Progress Indicator (Bind). You’re going to bind two properties of the progress spinner: hidden and animate.

First, expand the Hidden group, and bind to the View Controller. Controller Key should be blank, and Model Key Path should be self.loading.

In this case, you want hidden to be false when loading is true, and vice versa. There’s an easy way to do that: use NSValueTransformer to flip the value of the boolean.

NSValueTransformer is a class that helps you convert the form or value of data when moving between UI and data model.

You can subclass this object in order to do much more complex conversions, you can learn more about NSValueTransformers in this tutorial: How to Use Cocoa Bindings and Core Data in a Mac App.

Choose NSNegateBoolean from the Value Transformer dropdown list.

Bind the Animate value to the View Controller object. Set Controller Key as blank, and set Model Key Path as self.loading.

This Boolean doesn’t need to be negated. The bindings look like this:

Binding animating

Build and run; search for something that will return a large number of results so you have time to watch the spinner do its thing:

Spinner animating

Adding a Little More Detail

Cocoa Bindings can do more than what you’ve learned so far: you can bind colors and fonts to labels, enable and disable controls, and even set different values for labels depending on their state.

Build and run your app, and you’ll notice that you see No Selection on all your labels before. This isn’t very nice for a user to see when they start your app.

Find the label titled Price Label (Bind), and expand Value. Under No Selection Placeholder, put -- as shown below:

Screen Shot 2016-08-30 at 12.37.50 PM

Build and run your app, and you’ll see that the Price label now has a nice placeholder value:

Screen Shot 2016-08-30 at 12.38.00 PM

Set all labels that say No Selection to have a placeholder of your choice.

Note: If you want the label to be blank, you can set the No Selection Placeholder to a space.

Where to Go From Here?

That’s the basics of Cocoa Bindings on macOS. You’ve seen how easy it can be to connect data and UI.

In this tutorial, you learned the following:

  1. How to use Interface Builder to quickly and easily bind objects to data.
  2. How to keep models and views in sync with the user’s current selection.
  3. How to use methods and bindings together to control behaviors and organize data.
  4. How to quickly build out UI features like progress spinners.

You can download the final project here. Hopefully, you can save a lot of time (and code!) by adopting this technology.

Each binding has a lot of little settings and options, many of which you didn’t explore in this tutorial. Check out this resource on Cocoa bindings provided by Apple. It covers a lot of the details about what the options in the bindings windows do.

I hope you enjoyed this Cocoa Bindings on macOS tutorial and picked up some new techniques to use to accelerate your development process. You’ve just opened up a whole new universe!

Questions and comments are always welcomed in the discussion below.

Andy Pereira

Andy is a Senior iOS Developer in Atlanta, GA for Delta Air Lines.

You can find Andy on LinkedIn or Twitter.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 16 total!

iOS Team

... 28 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 11 total!

Resident Authors Team

... 15 total!