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.
Did this tutorial help you?
Your support on Patreon allows me to make better tutorials more often.
Subscribe via RSS