Developing iOS 8 Apps Using Swift – Animations, Audio, and Custom Table View Cells (Part 7)

This section completely updated to reflect changes in Xcode 6.3, as of April 17, 2015

In parts 1 through 6 we went over some basics of Swift, and set up a simple example project that creates a Table View and a puts some API results from iTunes inside of them. If you haven’t read that yet, check out Part 1

In this tutorial we’re going to implement an Album detail view, that makes a second API call to retrieve a list of tracks for an album, downloads higher resolution album art, and allows of to play previews of the tracks within our app. As an optional extra, we are going to also implement some nifty animations using the Core Animation API provided by the iOS SDK. When we’re done, we’re going to have something like this (video taken in iOS 7 Simulator)

Setting up our API Controller

Because we’re going to be adding additional API calls in this part of the tutorial, we should modify our API Controller for some code re-use. Let’s start with a more generic get request.

In your API Controller add the function get(), which takes path as a String argument, and converts it to an NSURL:

func get(path: String) {
    let url = NSURL(string: path)
    ...

Now get the NSURLSession and send it using dataTaskWithURL as we did before, in fact the code is exactly the same as what is currently inside of our searchItunesFor() function, so just copy and paste it from there. Start cutting right after the line

let urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music&entity=album"

And move everything in to the get() method. Your complete APIController.swift file should look something like this now:

import Foundation

protocol APIControllerProtocol {
    func didReceiveAPIResults(results: NSArray)
}

class APIController {
    var delegate: APIControllerProtocol
    
    init(delegate: APIControllerProtocol) {
        self.delegate = delegate
    }
    
    func get(path: String) {
        let url = NSURL(string: path)
        let session = NSURLSession.sharedSession()
        let task = session.dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in
            println("Task completed")
            if(error != nil) {
                // If there is an error in the web request, print it to the console
                println(error.localizedDescription)
            }
            var err: NSError?
            if let jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as? NSDictionary {
                if(err != nil) {
                    // If there is an error parsing JSON, print it to the console
                    println("JSON Error \(err!.localizedDescription)")
                }
                if let results: NSArray = jsonResult["results"] as? NSArray {
                    self.delegate.didReceiveAPIResults(results)
                }
            }
        })
        
        // The task is just an object with all these properties set
        // In order to actually make the web request, we need to "resume"
        task.resume()
    }
    
    func searchItunesFor(searchTerm: String) {
        // The iTunes API wants multiple terms separated by + symbols, so replace spaces with + signs
        let itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
        
        // Now escape anything else that isn't URL-friendly
        if let escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) {
            let urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music&entity=album"
        }
    }
    
}

Now in our searchItunesFor function, we can simply call on our new get() function and slim it down to the bare essentials. Just add a call to the get(urlPath) method on the end. The final method should look like this:

func searchItunesFor(searchTerm: String) {
    // The iTunes API wants multiple terms separated by + symbols, so replace spaces with + signs
    let itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
    
    // Now escape anything else that isn't URL-friendly
    if let escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) {
        let urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music&entity=album"
        get(urlPath)
    }
}

See the difference? The only part that was specific to the search function was the escaping of search terms, and embedding the term inside of the URL, so there’s no reason not to just break the get() part out in to it’s own method.

Now, we can quickly add a second API function to lookup a specific album. But first, let’s modify our album model to store a collectionId variable, used by iTunes to identify individual albums.

In our Album struct, add a new variable collectionId of type Int.

let collectionId: Int

..modify the constructor to accept collectionId as an argument, and add a line to set the collectionId as one of our variables being passed in through init()

init(name: String, price: String, thumbnailImageURL: String, largeImageURL: String, itemURL: String, artistURL: String, collectionId: Int) {
    self.title = name
    self.price = price
    self.thumbnailImageURL = thumbnailImageURL
    self.largeImageURL = largeImageURL
    self.itemURL = itemURL
    self.artistURL = artistURL
    self.collectionId = collectionId
}

Great! We can now initialize Albums with a collectionId, but now our existing albumsWithJSON code is wrong, it’s missing the collectionId parameter.
Find the line that creates the newAlbum just before it appends to the array returned from albumsWithJSON().

Modify this to get the collectionId our of the result dictionary, and pass it in to the Album constructor. Since we really need the collectionId to not be nil in order for this app to work, we’ll bundle the whole album creation inside of an if let clause so that only valid albums will show up on the list.

if let collectionId = result["collectionId"] as? Int {
    var newAlbum = Album(name: name!,
        price: price!,
        thumbnailImageURL: thumbnailURL,
        largeImageURL: imageURL,
        itemURL: itemURL!,
        artistURL: artistURL,
        collectionId: collectionId)
    albums.append(newAlbum)
}

The reason we need to add this collectionId variable is so that we can perform lookups of albums when they are selected. With the collectionId, it’s easy to do a second query of the iTunes API to gather lots of details about an individual album. For example, we can get a list of tracks with media URLs that will give us a 30 second preview.


Setting up the Details View

In the last tutorial we added a DetailsViewController to our storyboard. Let’s add a TableView to this view as well. You can lay it out however you like, but I recommend giving the Table View the majority of the screen space. This is where we’re going to load in our list of tracks.

Let’s now connect this new TableView to a property in DetailsViewController called tracksTableView.

@IBOutlet weak var tracksTableView: UITableView!

Now, set the dataSource and delegate of the table view to the DetailsViewController, and implement the protocol as we did before:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 0
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    return UITableViewCell()
}

It’s probably useful at this point to try and run the app. You should be able to drill in to an album and see an empty list of tracks.


Everything working? Cool, let’s keep going…

If we’re going to show tracks we’re going to need another model. Create a new Swift file called ‘Track.swift’, and give it three String properties for title, price, and previewUrl.

import Foundation
struct Track {
    let title: String
    let price: String
    let previewUrl: String
    
    init(title: String, price: String, previewUrl: String) {
        self.title = title
        self.price = price
        self.previewUrl = previewUrl
    }
}

This model is set up in pretty much exactly the same way as the Album model, not much new here.

In DetailsViewController, let’s add an array of tracks as a new property.

var tracks = [Track]()

Now, to get track information for the album, we need to modify our API Controller again. Fortunately for us, we have an easy to use get() function that makes this pretty simple.

Let’s add a new function to APIController that takes an Int collectionId argument, and tell it to use get() to get track information

func lookupAlbum(collectionId: Int) {
    get("https://itunes.apple.com/lookup?id=\(collectionId)&entity=song")
}

We’re going to need to use this in our DetailsViewController, so we now need to implement the APIControllerProtocol we wrote earlier in to DetailsViewController. So modify the class definition of DetailsViewController to include this, and our api object.

class DetailsViewController: UIViewController, APIControllerProtocol {
    lazy var api : APIController = APIController(delegate: self)
    ...

Your project will have an error at this point about the protocol we haven’t yet implemented, but that’s ok let’s keep moving.

In the DetailsViewController viewDidLoad method, we want to add a portion to pull down tracks based on the selected album, so let’s add the following:

// Load in tracks
if self.album != nil {
    api.lookupAlbum(self.album!.collectionId)
}

This is all stuff we’ve seen before. We create an instance of our APIController with the delegate set to self, and use our new lookupTrack method to get details on the tracks in the selected album. Here we use the lazy keyword to indicate we don’t want the APIController instance api to be instantiated until it is used. We need to do this to avoid the circular dependency of DetailsViewController needing to be initialized to pass it in as an argument to the APIController(delegate:) constructor. Earlier we used an optional APIController to solve this problem, but using the lazy keyword is another way to solve this problem and it’s a little cleaner.

To fully adhere to our APIControllerProtocol, we need to implement the didReceiveAPIResults() function in this class too. We’ll use this to load in our track data. We’ll implement this exactly as we did for the SearchResultsViewController, by offloading the responsibility of converting the JSON response in to a list of tracks to the Track model.

// MARK: APIControllerProtocol
func didReceiveAPIResults(results: NSArray) {
    dispatch_async(dispatch_get_main_queue(), {
        self.tracks = Track.tracksWithJSON(results)
        self.tracksTableView.reloadData()
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    })
}

We’re using a non-existent tracksWithJSON() static method on Track. So we need to add that before this will compile. Open up Track.swift and add a method similar to our albumsWithJSON method.

static func tracksWithJSON(results: NSArray) -> [Track] {
    var tracks = [Track]()
    for trackInfo in results {
        // Create the track
        if let kind = trackInfo["kind"] as? String {
            if kind=="song" {
                var trackPrice = trackInfo["trackPrice"] as? String
                var trackTitle = trackInfo["trackName"] as? String
                var trackPreviewUrl = trackInfo["previewUrl"] as? String
                if(trackTitle == nil) {
                    trackTitle = "Unknown"
                }
                else if(trackPrice == nil) {
                    println("No trackPrice in \(trackInfo)")
                    trackPrice = "?"
                }
                else if(trackPreviewUrl == nil) {
                    trackPreviewUrl = ""
                }
                var track = Track(title: trackTitle!, price: trackPrice!, previewUrl: trackPreviewUrl!)
                tracks.append(track)
            }
        }
    }
    return tracks
}

This API call returns the album before it returns the list of tracks, so we also add a check to make sure the “kind” key is set to “song”, as you see on line 8. Otherwise this function is just extracting some data from the JSON; Then we check that the three fields we need aren’t null, and if so set some reasonable defaults.

Now in DetailsViewController let’s modify the numberOfRowsInSection to be the track count

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return tracks.count
}

And let’s modify the cellForRowAtIndexPath method to load in our track data.

First, we need to add a prototype cell to the TableView in our storyboard, because we’re going to use a custom cell.
So select the Table View in the storyboard, and set the number of prototype cells to 1.
Then, select the cell itself and set the Identifier to “TrackCell” in the Attributes Inspector (on right-hand panel while selecting the Table View.)

Adding a Custom Table View Cell

To demonstrate what the prototype cells are really for, I think we should add some custom controls to this one. Create a new Swift class called TrackCell that inherits from UITableViewCell, and give it two IBOutlet UILabels called playIcon and titleLabel.

Now, back in your Storyboard file. Change the prototype cell’s class to ‘TrackCell’ under the Identity Inspector in the right-hand panel.
Next, add two UILabel’s to the cell by dragging the views on to the cell itself. Put one on the left for our play/pause button, and one taking up most of the space on the right to say the title of the track.

Drag two labels on to the prototype cell. Make one of them small and on the left, around 23×23 points, for a ‘Play/Stop’ icon. The second one will be the track title and should take up the rest of the cell. Click in to your play button label and then in the Mac OS menu bar hit Edit->Emoji & Symbols and find a play button looking icon. I found some under Emoji->Objects & Symbols. As an optional challenge, try using an image for the button icon!

import UIKit
class TrackCell: UITableViewCell {
    @IBOutlet weak var playIcon: UILabel!
    @IBOutlet weak var titleLabel: UILabel!
}

When you’re done you should have a prototype cell looking something like this:

In the DetailsViewController, we can now implement the custom cells by getting the TrackCell object and casting it to our class with ‘as TrackCell’

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("TrackCell") as! TrackCell
    let track = tracks[indexPath.row]
    cell.titleLabel.text = track.title
    cell.playIcon.text = "YOUR_PLAY_ICON"
    return cell
}

The logic is mostly the same as our other table view, with the exception that we cast cell to our custom class, TrackCell, on the first line. The “YOUR_PLAY_ICON” text should be replaced with the play icon, which again, you can get by hitting Edit->Emoji & Symbols in the Mac OS menu bar. Don’t forget to put quotes around it!

Next we grab the track we need from our tracks array, just as before with albums.

Finally we access our custom IBOutlet variable, titleLabel, set it’s text to be the track title, and do the same with playIcon.


Congrats on getting this far, we’re in the home stretch!


Play some music

Okay, next we want to set up a way to actually hear some audio. We’re going to use the MPMoviePlayerController class to do this. It’s easy to work with, and works just fine with audio-only streams.

First off, in our DetailsViewController class let’s add the mediaPlayer as a property, right under the class definition add:

var mediaPlayer: MPMoviePlayerController = MPMoviePlayerController()

ERROR! Use of undeclared type MPMoviePlayerController.

It’s okay, this is just because we need to import the framework MediaPlayer, it isn’t included by default in our project.

Just add the following to the top of your DetailsViewController:

import MediaPlayer

Next, let’s kick off the audio playing when a user selects one of the track’s rows. Add the following to our DetailsViewController:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    var track = tracks[indexPath.row]
    mediaPlayer.stop()
    mediaPlayer.contentURL = NSURL(string: track.previewUrl)
    mediaPlayer.play()
    if let cell = tableView.cellForRowAtIndexPath(indexPath) as? TrackCell {
        cell.playIcon.text = "YOUR_STOP_ICON"
    }
}

The line mediaPlayer.stop() stop’s the currently playing track. If there isn’t one playing, nothing happens. We don’t want to play multiple tracks at once so let’s make sure we stop a track if another one is clicked πŸ™‚

Next, mediaPlayer.contentURL sets a url for where the media player should load it’s content. In our case it’s from the url stored in track.previewUrl.

Finally, we call mediaPlayer.play(), and get the track cell for the tapped row index.
If this row is still visible, it’ll set ‘cell’ and here we can change the playIcon label to instead show the stopped icon, which we set again by using Edit->Emoji & Symbols on the Mac OS menu bar.

If you run your app, you should now have a fully working iTunes music preview application! This by itself is pretty great, but let’s add one more thing to make it even more slick, some smooth table view cell animations.

Adding Animations

This is actually really easy, and has a very cool visual effect.

All we’re going to do is add the following function to both our SearchResultsViewController, and our DetailsViewController:

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    cell.layer.transform = CATransform3DMakeScale(0.1,0.1,1)
    UIView.animateWithDuration(0.25, animations: {
        cell.layer.transform = CATransform3DMakeScale(1,1,1)
        })
}

Now run the app and scroll around, neat right?

So how’s it work?
The function willDisplayCell is called from the TableView delegate, similar to the rest of our callback functions that set up the row. But this one is only called the moment before a cell appears on-screen, either through initial loading or through scrolling.

cell.layer.transform = CATransform3DMakeScale(0.1,0.1,1)

This first line uses CATransform3DMakeScale() to create a transform matrix that scales down any object in x, y, and z. If you are familiar with linear algebra you’ll know what this means right away. If not, it’s not super important. The point is, it makes things scale, and here we’re scaling it down to 10% by setting the x and y values to 0.1.

So, we are basically just setting the cell layer’s transform to be 90% smaller.

Next we set the cell layer’s transform to a new scale, this time of (1,1,1). This just means that it should return to it’s original scale. Because this line is run inside of the animateWithDuration() block, we get the animation for free courtesy of Core Animation.

Experienced Obj-C developers will probably recognize this is not the only way to perform such an animation. However, I believe this method is the easiest to understand, in addition to being the most Swifty.

In my upcoming book I go in to great detail about how to efficiently use Core Animation to make beautiful animations for your apps. Using Core Animation in this way really makes your app pop.

The full source code for this section is available here.

A reader of this tutorial series contributed the next section, which covers producing a nicer play/pause icon purely in code. Check it out here.
Make sure to sign up to be notified of the next tutorial series.


Sign up now and get a set of FREE video tutorials on writing iOS apps coming soon.

Subscribe via RSS

Developing iOS 8 Apps Using Swift – Interaction with multiple views (Part 6)

This section completely updated to reflect changes in Xcode 6.3, as of April 16, 2015

In parts 1 through 5 we went over some basics of Swift, and set up a simple example project that creates a Table View and a puts some API results from iTunes inside of them. If you haven’t read that yet, check out Part 1

If not, and you just want to start from here, download the code for Part 5 to get started. We’ll use it as a template to begin.

In this tutorial we’re going to do quite a few things, so let’s get started!

Modifying The API Controller code

First off, our actual plan for this app is to show iTunes music information. So let’s modify the API controller to better handle this information.

One thing’s been bothering me though. When we create our APIController, we set a delegate after it’s created. But, an API Controller without a delegate isn’t all that useful, so why even offer the option to make one?

Let’s add a constructor that accepts the delegate as it’s only argument.

init(delegate: APIControllerProtocol) {
    self.delegate = delegate
}

Now, our delegate variable in the APIController isn’t actually going to be an optional any more. There is no APIController without a delegate!
So also change the delegate property to be an every day, non-optional APIControllerProtocol object.

var delegate: APIControllerProtocol

There’s also going to be an error at the end of the searchItunesFor method, because we’re treating the delegate object as an optional, but it’s not optional any more. So change the erroneous line to say this:

self.delegate.didReceiveAPIResults(results)

The only difference is we removed the ? from after the delegate property, to indicate it’s not an optional.

Now in our SearchResultsController, we need to change a few things. First, since the APIController constructor now needs the delegate object to be instantiated before *it* can be instantiated itself, we need to make it an implicitly unwrapped optional, and wait until viewDidLoad to assign it.

So in the api variable declaration change to this:

var api : APIController!

In the viewDidLoad method we need to unwrap the api object in order to call searchItunesFor(). You should end up with this

override func viewDidLoad() {
    super.viewDidLoad()
    api = APIController(delegate: self)
    api.searchItunesFor("JQ Software")
}

 

Searching for Albums
Let’s also modify our call to the searchItunesFor() in the APIController to use a search term for music. We’ll also show a networkActivityIndicator, to tell the user a network operation is happening. This will show up on the top status bar of the phone.

override func viewDidLoad() {
    super.viewDidLoad()
    api = APIController(delegate: self)
    UIApplication.sharedApplication().networkActivityIndicatorVisible = true
    api.searchItunesFor("Beatles")
}

Now in our urlPath in the APIController, let’s modify the API parameters to look specifically for albums.

let urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music&entity=album"

We’ll now get results in the form of Album data, but this schema is a little different from apps. In fact running the app right now will we’ll just get default cells because the expected JSON data isn’t there. This is really fragile code, but we can make it slightly less fragile by doing some modeling of the data we expect.

Creating a Swift model for iTunes Albums

In order to facilitate passing around information about albums, we should create a model of what an album is exactly. Create a new swift file and call it Album.swift with the following contents:

import Foundation

struct Album {
    let title: String
    let price: String
    let thumbnailImageURL: String
    let largeImageURL: String
    let itemURL: String
    let artistURL: String
    
    init(name: String, price: String, thumbnailImageURL: String, largeImageURL: String, itemURL: String, artistURL: String) {
        self.title = name
        self.price = price
        self.thumbnailImageURL = thumbnailImageURL
        self.largeImageURL = largeImageURL
        self.itemURL = itemURL
        self.artistURL = artistURL
    }
}

It’s a pretty simple struct, it just holds a few properties about albums for us. We create the 6 different properties as strings, and add an initializer that sets all the properties based on our parameters.

So now we have a struct for albums, let’s use it!

Using our new Swift Album model

Back in our SearchResultsController, let’s modify the tableData array variable, and instead opt for a Swift native array for Albums. In swift, this is as easy as:

var albums = [Album]()

We can do away with do line var tableData = [], we won’t be using that any more.

This creates an empty array containing strictly Albums. We’ll now need to change our tableView dataSource and delegate methods to understand albums.
In the numberOfRowsInSection method, let’s change the number of items to the count of albums in our albums array:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return albums.count
}

Now in cellForRowAtIndexPath, let’s swap out those dictionary lookups for a single album lookup:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as! UITableViewCell
    let album = self.albums[indexPath.row]

    // Get the formatted price string for display in the subtitle
    cell.detailTextLabel?.text = album.price
    // Update the textLabel text to use the title from the Album model
    cell.textLabel?.text = album.title
    
    // Start by setting the cell's image to a static file
    // Without this, we will end up without an image view!
    cell.imageView?.image = UIImage(named: "Blank52")
    
    let thumbnailURLString = album.thumbnailImageURL
    let thumbnailURL = NSURL(string: thumbnailURLString)!
    
    // If this image is already cached, don't re-download
    if let img = imageCache[thumbnailURLString] {
        cell.imageView?.image = img
    }
    else {
        // The image isn't cached, download the img data
        // We should perform this in a background thread
        let request: NSURLRequest = NSURLRequest(URL: thumbnailURL)
        let mainQueue = NSOperationQueue.mainQueue()
        NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
            if error == nil {
                // Convert the downloaded data in to a UIImage object
                let image = UIImage(data: data)
                // Store the image in to our cache
                self.imageCache[thumbnailURLString] = image
                // Update the cell
                dispatch_async(dispatch_get_main_queue(), {
                    if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
                        cellToUpdate.imageView?.image = image
                    }
                })
            }
            else {
                println("Error: \(error.localizedDescription)")
            }
        })
    }
    return cell
}

Then there is the didSelectRowAtIndexPath method that needs to be modified to use the albums array. But, actually we’re not going to need this any more, so let’s just delete the whole method.

Creating Album objects from JSON

Now, all of this is not much use if we aren’t creating our album information in the first place. We need to modify our didReceiveAPIResults method of SearchResultsViewController to take album JSON results, create Album objects, and save them in to the albums array. Since we have a model for Albums now, it makes sense to move this functionality in to the Album model itself. So let’s make a minor adjustment to didReceiveAPIResults and delegate the responsibility of construction the albums array to the Album class.

func didReceiveAPIResults(results: NSArray) {
    dispatch_async(dispatch_get_main_queue(), {
        self.albums = Album.albumsWithJSON(results)
        self.appsTableView!.reloadData()
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    })
}

Note that since this is where the api request comes to it’s conclusion, we also turn off the networkActivityIndicator.

Now in the Album.swift file we need to add a static method that creates a list of albums from a JSON list.

static func albumsWithJSON(results: NSArray) -> [Album] {
    // Create an empty array of Albums to append to from this list
    var albums = [Album]()
    
    // Store the results in our table data array
    if results.count>0 {
        
        // Sometimes iTunes returns a collection, not a track, so we check both for the 'name'
        for result in results {
            
            var name = result["trackName"] as? String
            if name == nil {
                name = result["collectionName"] as? String
            }
            
            // Sometimes price comes in as formattedPrice, sometimes as collectionPrice.. and sometimes it's a float instead of a string. Hooray!
            var price = result["formattedPrice"] as? String
            if price == nil {
                price = result["collectionPrice"] as? String
                if price == nil {
                    var priceFloat: Float? = result["collectionPrice"] as? Float
                    var nf: NSNumberFormatter = NSNumberFormatter()
                    nf.maximumFractionDigits = 2
                    if priceFloat != nil {
                        price = "$\(nf.stringFromNumber(priceFloat!)!)"
                    }
                }
            }
            
            let thumbnailURL = result["artworkUrl60"] as? String ?? ""
            let imageURL = result["artworkUrl100"] as? String ?? ""
            let artistURL = result["artistViewUrl"] as? String ?? ""
            
            var itemURL = result["collectionViewUrl"] as? String
            if itemURL == nil {
                itemURL = result["trackViewUrl"] as? String
            }
            
            var newAlbum = Album(name: name!, price: price!, thumbnailImageURL: thumbnailURL, largeImageURL: imageURL, itemURL: itemURL!, artistURL: artistURL)
            albums.append(newAlbum)
        }
    }
    return albums
}

I know this looks like a lot of new code, but actually there’s not much going on here. It’s really just looping through the list coming from allResults, and its grabbing values for some keys, and setting defaults if they’re missing.

The ?? operator used here is pretty neat. It works like this:

let finalVariable = possiblyNilVariable ?? "Definitely Not Nil Variable"

The finalVariable value is set to possiblyNilVariable if it is not nil. But if it is nil? It uses the value of the thing on the right-hand side of the ?? operator. In this case, the string “Definitely Not Nil Variable”.

We use this here in order to prevent getting nil values passed in to our Album.

On line 39, we create an Album object. On line 40 the album is added to the list.
Finally on line 43 the list of albums is returned.

If you run the app now you should see a new list of Album’s pop up. Cool, right?

 

Creating a second view

Now to actually show the details of an album, we’ll need a new view. First let’s create the class.
Add a new file called DetailsViewController.swift that inherits from UIViewController.

Our view controller will be pretty simple to start. We’re just going to add an album, and implement UIViewController’s init method as well as viewDidLoad().

import UIKit

class DetailsViewController: UIViewController {
    
    var album: Album?
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

This code doesn’t do much yet, but that’s okay. We just need the class to exist in order to set up our storyboard.

Since we’ll be pushing views back and forth on the stack we’ll want a navigation bar as well. It’s hard to explain in text, and there is an NDA preventing me from showing parts of Xcode 6 in screenshots, so instead I created a short video demonstrating how to do this in Xcode 5. The process is nearly identical for Xcode 6 Beta, and is not under any sort of NDA.

In the video we did the following:

  1. Embedded our view controller in a navigation controller using the Xcode shortcut in the Editor menu, by clicking the view controller, then selecting Editor->Embed In->Navigation Controller
  2. Added a new view controller
  3. Set it’s class and storyboard ID to ‘DetailsViewController’
  4. Control+Clicked+Dragged from the table view cell in our first view controller to the new view controller we just created, and selected ‘show’ for the type of segue.

What this last step does is creates a segue on our navigation controller that pushes the new view on top of the stack. If you run the app now and click a cell, you should see this new view animate in.

Let’s build out a simple UI for this new view. It’ll contain a UIImageView that is 100×100 pixels, and a title Label. Drag these objects out of the object library and arrange them any way you like on the new view.

 

Providing the new view with Album information

When the storyboard segue fires off, it first calls a function on whatever view controller is currently on the screen called prepareForSegue. We’re going to intercept this call in order to tell our new view controller which album we’re looking at. Add the following in SearchResultsViewController:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let detailsViewController: DetailsViewController = segue.destinationViewController as? DetailsViewController {
        var albumIndex = appsTableView!.indexPathForSelectedRow()!.row
        var selectedAlbum = self.albums[albumIndex]
        detailsViewController.album = selectedAlbum
    }
}

What’s happening here is the segue parameter being passed in has a member called destinationViewController, which is our fancy new DetailsViewController we just created. In order to set the album member on it, we first need to cast it to DetailsViewController using the ‘as’ keyword as shown above.
Then, by using the indexPathForSelectedRow() method of our table view we can determine which album is selected at the moment this segue happens.
Using this information, well tell our detailsViewController which album was clicked before it is displayed.

Now I’m going to show you a pretty nifty feature of Xcode. We’re going to let it write some code for us.

Open up your storyboard again let’s start creating IBOutlets for our image view, label, button, and text view. On the top-right hand corner of Xcode there is the ‘assistant’ button. The icon looks like a bowtie and suit jacket. Clicking on this will open up a code window right next to your storyboard window. Make sure that one of the panels is showing DetailsViewController.swift, and the other is showing Main.storyboard.

Now, hold control, and click+drag from your image view to your code file. Just a line under your class definition for DetailsViewController. It’ll prompt you for a name, so let’s call it ‘albumCover’. The default options should be fine here. After doing this you should see this line of code newly added:

@IBOutlet weak var albumCover: UIImageView!

We just created a new IBOutlet, and now it’s connected to our storyboard’s DetailsViewController. How cool is that?

Do the same thing for the label you added to your view, and call it ‘titleLabel’.

Next, let’s modify viewDidLoad so that it will load in the info we’re being passed to our view objects, here’s the final DetailsViewController code:

import UIKit

class DetailsViewController: UIViewController {
    var album: Album?
    @IBOutlet weak var albumCover: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = self.album?.title
        albumCover.image = UIImage(data: NSData(contentsOfURL: NSURL(string: self.album!.largeImageURL)!)!)
    }
}

The @IBOutlets are the UI connections made by our storyboards, and our viewDidLoad method sets the title and album cover variables to load in from our Album object.

Now try running the app and taking a look. We can now drill in to details for albums and get a nice big detail view with the album cover and title. Because we pushed in a navigation controller, we also get a functional Back button for free!

If you made it this far, I want to personally congratulate you so let me know on twitter that you pulled it off! You are well on your way to creating real iOS apps with Swift.

I’ve decided this tutorial series is going to be expanded upon and refined, along with several other tutorials and essays on working with swift, Xcode, and Apple in a new book, which I have available for pre-order here. Also, I’ve decided to open up a new forum for all the followers of this tutorial.

Make sure to sign up to be notified of the new sessions.

The full source code for this section is available here.

In part 7, we set up a full Detail view with a working music player, and implement some great animations.

Go To Part 7 ->


Sign up now and get a set of FREE video tutorials on writing iOS apps coming soon.

Subscribe via RSS