Cocoa bindings have a simple goal: write less code. You’ll discover as you work through this Cocoa Bindings on OS X tutorial that they do indeed live up to this objective.
Cocoa bindings, aka bindings, fulfill a lot of responsibilities. Most importantly, they free you up from having to spend hours writing glue code — i.e. creating links between the model and the view in the controller when using the Model-View-Controller (MVC) pattern.
If you’ve spent time in Cocoa or Cocoa Touch, you’ve no doubt written countless lines of boilerplate code to handle the passing of data between your model and UI layers. You’ve got your delegates, notifications and observers to let you know when a control has been altered, in addition to code that helps with the task of validating and transforming data.
You may have wondered if there’s a better way.
The great news is that there is. Cocoa bindings comprise a set of technologies that makes a lot of the code you’ve previously written, frankly, unnecessary.
In this Cocoa Bindings on OS X tutorial, you’ll learn about how to use Cocoa bindings to:
- 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
Once the relationship is set up within Interface Builder, any user-generated changes to your UI are automatically pushed to your data model, and any changes made to the data model are automatically updated in the UI.
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.
Getting Started
In this tutorial, you’ll create an app that displays search results from the App Store via the iTunes API.
First, download the starter project here.
Build and run the project. You’ll see that there’s already a nice interface – but no data.
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 hence require the Objective-C runtime. Adding the dynamic
keyword guarantees that access to that property is always dynamically dispatched using the Objective-C runtime.
Hence, you must remember that bindings rely on key-value coding and need to use the Objective-C runtime in order to work.
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 it to an NSArrayController.
Open the 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)’.
Set Up an NSArrayController
NSArrayController
is an object that manages the content of an 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.
Find an NSArrayController
object in the Object Library. Drag it into the list of objects under the View Controller Scene grouping in the Document Outline:
Next, open the Assistant Editor and make sure ViewController.swift is the file being edited. 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 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. Select to create an IBAction
, call it searchClicked
, and then add the following code to the method:
@IBAction func searchClicked(sender: AnyObject!) { //1 if (searchTextField.stringValue == "") { return } //2 if let resultsNumber = Int(numberResultsComboBox.stringValue) { //3 iTunesRequestManager.getSearchResults(searchTextField.stringValue, results: resultsNumber, langString: "en_us") { (results, error) -> Void in //4 let itunesResults = results.flatMap { $0 as? NSDictionary } .map { return Result(dictionary: $0) } //Deal with rank here later //5 dispatch_async(dispatch_get_main_queue()) { //6 self.searchResultsController.content = itunesResults print(self.searchResultsController.content) } } } } //7 override func controlTextDidEndEditing(obj: NSNotification) { //enter pressed searchClicked(searchTextField) } |
Taking each line in turn:
- Check the text field, if it’s blank, you don’t send that query to iTunes search API.
- Get the value in the dropdown. This is a number passed to the API that controls how many search results it should return. There’s a number of pre-configured options in the dropdown, but you can also type in other numbers — 200 is the maximum.
- Make a call to
getSearchResults(query:, results:, language:)
. 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, an array ofNSDictionary
result objects or anNSError
object if there’s a problem completing the query. Note that the method already handles parsing the JSON. - Here you use some swift style array mapping to a) optionally cast the object to a dictionary, and b) pass the dictionary into an initialization method that creates a
Result
object from it. When that is done, theitunesResults
variable contains an array ofResult
objects. - Before you can set this new data on the
searchResultsController
, you need to make sure you’re on the main thread, therefore you usedispatch_async
to get to the main queue. You haven’t set up any bindings, but once you have, altering the content property on thesearchResultsController
will update theNSTableView
(and potentially other UI elements) on the current thread. Updating UI on a background thread is always a no-no. - Here you set the content property of the
NSArrayController
. The array controller has a number of different methods to add or remove objects that it manages, but each time you run a search, you want to clear out whatever is there and start over with just the results of the latest query. For now, print the content ofsearchResultsController
to the console to verify that everything is working. - The last thing you’ll do is add a small method that will invoke the
searchClicked()
method when the user presses enter in addition to clicking the button. This just makes it easier to run a quick search.
Build and run now. Type flappy into the search bar. You will see something like this show up in the console:
Your First Bindings
It’s time to get to the meat of this tutorial!
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 SearchResultsController is displayed in the dropdown box
- Make sure the Controller Key is set to arrangedObjects
Like this:
Build and run now. You’ll see at most five results unless you changed the number in the dropdown. However, they all say ‘Table View Cell’.
Binding Text Fields to Their Properties
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.
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.
- The Model Key Path should be 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.
In other words, objectValue
is, in this case, equal to the Result
model object for that row.
Repeat this process for Publisher TextField (Bind) by binding the value of this element to objectValue.artistName.
Build and run now. Look at that – both the title and publisher show themselves.
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.
So, with a little more work you can set the rank value.
Add the following code in ViewController
under this comment: //Deal with rank here later
.
.enumerate() .map({ (index, element) -> Result in element.rank = index + 1 return element }) |
This code calls enumerate()
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.
Finally, go back to the Storyboard, select Rank TextField (Bind)
, open the Bindings Inspector and do the following:
- In the Value section, bind to the
Table Cell View
- Make sure Controller Key is empty
- Set Model Key Path to
objectValue.rank
Build and run now.
Binding a Table View’s Selection
Now it’s time to bind the Result
object in the table that the user selects to the rest of the UI. Binding to a selection in a table involves two steps:
- You first bind the
NSArrayController
to the table selection - Then you can bind the properties of the
selection
object in theNSArrayController to the individual labels and other properties.
Select the Search Results Table View (Bind) and open the Bindings Inspector to do the following steps:
- Expand the Selection Indexes option in the Table Content section
- Check Bind to the SearchResultsController object
- Enter selectionIndexes into the Controller Key box. The table has a
selectionIndexes
property that contains set of indexes that the user has selected in the table. Note that in this case, I’ve set the table view to only allow a single selection, but you could work with more than one selection if needed — think about the finder window and how you can select multiple files at a time.
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 to the SearchResultsController
- Controller Key should be selection
- Model Key Path should be trackName
Build and run.
Formatting Bound Data
Now you know how easy it is to get data from your model into your UI, but what if the data needs to be formatted in some way, such as currency or 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.
Format as Price
Find the label titled Price Label (Bind), setting it up like this:
- Bind it to the SearchResultsController object
- Make sure Controller Key is selection
- Set Model Key Path to price
- Next, find a Number Formatter in the Object library. Drag it over to NSTextFieldCell under the
Price
label. - Select the Number Formatter and open the Attributes Inspector change the Style to Currency.
Like this:
Build and run.
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 also a bunch of other kinds of formatters. There are formatter objects for dates, for byte counts and several other less common ones.
Format as Bytes
You’ll be using a Byte Count Formatter next.
Find File Size Label (Bind) and set it up like so:
- Bind it to the SearchResultsController
- Controller Key is selection
- Model Key Path is fileSizeInBytes
- Then, find a Byte Count Formatter and attach it to the NSTextFieldCell. There’s no need to configure anything here, the default settings on a byte formatter will work.
Like this:
Build and run now.
You now know everything you need to know to bind the remaining labels, so here’s a short list of the keys you need to bind:
All these labels should be bound to the SearchResultsController and the selection controller key.
- Bind the Artist Label (Bind) to artistName
- Bind the Publication Date (Bind) to releaseDate
- Add a Date Formatter, 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
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 NSTextField
, which is several levels down in the hierarchy, not the NSScrollView
which is at the top.
Build and run. You should see now that most of the UI is 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 what you get from the JSON payload is not the image, but a URL location for the image. Result
includes a method to download the image file and make it available as an NSImage
on the artworkImage
property.
Download the Right Icon at the Right Time
You don’t want to download all the icons at once — just the one for the current selection in the table. You therefore need to invoke the method whenever the selection changes.
Add the following code to ViewController:
//1 func tableViewSelectionDidChange(notification: NSNotification) { //2 if let result = searchResultsController.selectedObjects.first as? Result { //3 result.loadIcon() } } |
tableViewSelectionDidChange()
gets fired every time the user selects a different row in the table- The array controller has a property,
selectedObjects
. It returns an array that contains all the objects for the indexes of the rows selected in the table. In your case, the table will only allow a single selection, so this array always contains a single object. You store the object in theresult
object. - Finally, you call
loadIcon()
. This method downloads the image on a background thread and then updates theResult
objectsartworkImage
property when the image is downloaded 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 and set things up:
- Bind to the SearchResultsController
- Set Controller Key to selection
- Set Model Key Path to artworkImage
You may notice that there is a Value Path and a Value URL section. Both of these bindings are intended to be used only with local resources. It is possible to connect them to a network resource, but if you do, the UI thread will be blocked until the resource is downloaded.
Build and run, search for fruit and then select a row. You’ll see the icon image appear once it has downloaded:
Populate the Collection View
The collection view beneath the description text view is currently looking a little bare — time to populate that with some screenshots. First you’ll bind the collection view to the screenShots
property before ensuring that the screenShots
array is correctly populated.
Select the Screen Shot Collection View (Bind). Open the Bindings Inspector and expand the Content binding in the Content group. Set it up thusly:
- Bind to the SearchResultsController
- Controller Key is selection
- Model Key Path is screenShots
The screenShots
array starts out empty. There’s a method named loadScreenShots()
that is similar to the method that loads the icon. It’ll download the image files and populate the screenShots
array with NSImage
objects.
Add this line in ViewController.swift
, in tableViewSelectionDidChange()
right after the result.loadIcon()
:
result.loadScreenShots() |
This will populate the screenshot images and create the right number of views. The next thing you need to do is set the right collection view item prototype.
Normally when you drag a collection view into a storyboard, you automatically create a collection view item and a segue. However, this feature is buggy in the latest Xcode and at the time of writing, it’s not working.
Although the collection view item scene is present in the storyboard, it’s not connected to the collection view. Therefore 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?.instantiateControllerWithIdentifier("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. Here are the settings for this one:
- Bind the Value option to the Collection View Item object.
- The controller key should be blank
- 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 now.
You can now see the screenshots appearing below the description text — great work! Just a few more features of Cocoa Bindings to cover before wrapping up.
Binding Other Properties
Thus far you’ve seen how you can bind model objects to labels and images for display to the user. However, you can also bind attributes such as fonts, colors, text styles and visibility to data in your model. This allows you to easily control the appearance of your UI directly through the model layer.
Set Up a Progress Spinner
Users don’t like to stare at static screens when something is loading — they tend to think the worst if there’s nothing to tell them something’s happening behind the scenes.
Instead of leaving a static screen, you can show a spinner to the user, indicating that the app is busy working. An easy way to do this is to bind a progress spinner to a new property in the ViewController
, so add the following property to ViewController
:
dynamic var loading = false |
Loading requires two things in order to work correctly. The dynamic
keyword and that the parent class is a subclass of NSObject
. Bindings relies on KVO (Key Value Observing). A swift class that doesn’t inherit from NSObject wouldn’t be able to use KVO.
Add the following line of code in the function searchClicked()
right before the line that executes the call to getSearchResults()
.
loading = true |
Locate the line that sets the content
property on searchResultsController
(self.searchResultsController.content = itunesResults
) and add the following immediately before it:
self.loading = false |
Next, select Search Progress Indicator (Bind) in Main.storyboard. You’re going to bind two properties of the progress spinner: hidden
and animate
.
First, expand the Hidden group, and set it up like so:
- Bind to the View Controller
- Controller Key should be blank
- Model Key Path should be self.loading
In this case you need to do one more thing, when loading is true, you actually want hidden
to be false and vice versa. Luckily, there’s an easy way to do that — using NSValueTransformer
to flip the value of the boolean.
Choose NSNegateBoolean from the Value Transformer dropdown list.
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.
Next, bind to the Animate value like this:
- Bind it to the View Controller object
- Controller Key should be blank.
- Model Key Path is self.loading
This boolean doesn’t need to be negated.
Build and run now. You might want to use a larger number of results so that there’s time to watch the spinner do its spin thing while results are retrieved.
Adding a Little More Spice
Color is the spice of design, and right now things are looking a little drab. You won’t be surprised to learn that you can set color with Cocoa bindings!
In this scenario, you’re dealing with apps that have a larger number of reviews, making them rank higher in the search results. More reviews tends to be a good sign, and users enjoy having a visual indicator of such a thing when they have to scroll through a long list and make split-second decisions.
So, you’ll build out that visual cue.
Create a Ranking Method
First, you need to add a method that calculates the color of each result based on the number of reviews.
Add this code to ViewController
inside ViewController.swift:
func setColorsOnData() { //1 let allResults = searchResultsController.arrangedObjects //2 let sortDescriptor = NSSortDescriptor(key: "userRatingCount", ascending: false) let sortedResults = allResults.sortedArrayUsingDescriptors([sortDescriptor]) as NSArray //3 for index in 0..<sortedResults.count { //4 let red = CGFloat(Float(index) / Float(sortedResults.count)) let green = CGFloat(1.0 - (Float(index) / Float(sortedResults.count))) let color = NSColor(calibratedRed: red, green: green, blue: 0.0, alpha: 1.0) //5 if let result = sortedResults.objectAtIndex(index) as? Result { result.cellColor = color } } } |
Here’s what you’re cooking up in there:
- Get the array from the array controller
- Create a sort descriptor that is based on the ‘userRatingCount’ key. It’s descending so that the largest number of reviews is first in the list. You then create a
sortedResults
array that contains the sorted list - Iterate through the results
- Create a color; the objects at the beginning of the set will be the most green and the values at the end will be the most red
- Finally, you set the
cellColor
Next, you need to call that new method. Add this line to searchClicked()
in ViewController.swift
right after this line self.arrayController.content = itunesResults
:
self.setColorsOnData() |
Bind Font Color
Now, bind the font color of the rank column to that value.
Select the Rank TextField (Bind) object and set it up like this:
- Bind to the Text Color value, the Table Cell View object.
- Controller Key should be blank
- Set Model Key Path to objectValue.cellColor
Build and run.
You can now see that the rank numbers are color-coded, according to the number of reviews each app has.
Where to Go From Here
That’s the gist of Cocoa Bindings, and you can see just how much easier it makes life when you need to connect data and UI. You learned:
- How to use Interface Builder to quickly and easily bind objects to data
- How to make keep models and views in sync with the user’s current selection
- How to use methods and bindings together to control behaviors and organize data
- How to quickly build out UI features like progress spinners
You can download the final project here. Hopefully, you can see how much time and code you can save by adopting this technology.
Each binding has a lot of little settings and options, many of which we didn’t explore. One specific thing to take a look at is this resource provided by Apple. It will cover a lot of the details about what the options in the bindings windows do.
I hope you enjoyed this Cocoa Bindings on OS X tutorial and picked up some new techniques to use to accelerate your development process. You’ve just opened up a whole new universe! Let’s talk about it in the forums — I look forward to your questions, comments and findings.
The post Cocoa Bindings on OS X Tutorial appeared first on Ray Wenderlich.