This section completely updated to reflect changes in Xcode 6.3, as of April 16, 2015
In parts 1 & 2 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 and Part 2. If you enjoy these tutorials, I’m also working on a book full of quality content for Swift developers, and it’s available for Pre-Order now.
In this section we’re going to take a stop and clean up some of the code we’ve created so far by removing our network logic from our view controller code, and fixing some other issues that will hurt our performance. This may not be the most glamorous part of writing a new app, but it’s important! Let’s do it…
If you’re looking something that goes a little deeper than these tutorials, I also have written a book and created video tutorials which are available now. If that’s more your speed, check out my Swift Book & Video Packages ».
Split up the code
First things first, let’s rename our View Controller to be something more meaningful. Open up your ViewController.swift file and replace the class name ‘ViewController’ with our new name, ‘SearchResultsViewController’. Rename the file as well to SearchResultsViewController.swift.
If you try to run the app from here you’ll get a crash. This is because our storyboard file hasn’t been updated to know about the new class! So open up the Main.storyboard, select your ‘View Controller’ object in the scene (the left-hand side nav), and then select the Identity Inspector (right-hand side, third button.)
From here let’s change the class from ‘ViewController’ to ‘SearchResultsViewController’. Now we should be back on track. Check that the project still works, and let’s proceed.
Let’s now move the API code out to it’s own class. Right click in the xcode navigation pane and select ‘New File…’. This one is going to be a regular Swift file, under the iOS->Source navigation option.
This one handles all API work, so I’m going to call it APIController.
Now let’s grab our searchItunesFor() function, and cut and paste it out of the Search controller and in to the APIController, inside of a new class ‘APIController’.
Your complete APIController.swift file should look something like this:
import Foundation class APIController { 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 = "http://itunes.apple.com/search?term=\(escapedSearchTerm)&media=software" let url = NSURL(string: urlPath) 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? var 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)") } let results: NSArray = jsonResult["results"] as NSArray dispatch_async(dispatch_get_main_queue(), { self.tableData = results self.appsTableView!.reloadData() }) }) task.resume() } } }
If you try to build this straight away you’ll see three errors:
1) searchItunesFor() is now undefined in our SearchResultsViewController.
2) self.tableData is now undefined in the APIController.
3) self.appsTableView is now undefined in the APIController.
To deal with this we need to let these objects know about each other. So let’s make APIController a child object of our SearchResultsViewController. It’s pretty easy to do, just add the following line underneath your SearchResultsViewController class definition:
let api = APIController()
Now modify the line that calls searchItunesFor() to this:
api.searchItunesFor("Angry Birds")
The only difference here is that we’re calling the method from an instance of an APIController, as opposed to doing this all from a method in the View Controller.
That takes care of the first set of errors, but now we need to also get APIController’s results back to the SearchResultsViewController. We would like for our API controller to be able to respond to arbitrary API calls, so we should define a protocol that views can subscribe to in order to get results!
Defining an API protocol
There are two erroring lines in our APIController right now referencing the tableData results, let’s just remove these. We’re going to use something a little cleaner.
dispatch_async(dispatch_get_main_queue(), { // DELETE ME self.tableData = results // DELETE ME self.appsTableView!.reloadData() // DELETE ME }) // DELETE ME
Above the class definition in our APIController, let’s add a protocol that declare the function didReceiveAPIResults() as a required implementation. Note that the protocol goes outside of the class definition, so make sure it’s outside of the APIController{} curly braces.
protocol APIControllerProtocol { func didReceiveAPIResults(results: NSArray) }
This doesn’t do anything on it’s own, but now we can add the protocol to our SearchResultsViewController. Not adhering to the protocol will now cause an error, so we don’t make the silly mistake of not implementing didReceiveAPIResults!
Adhering to the protocol
Now modify your SearchResultsViewController to adhere to the protocol:
class SearchResultsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, APIControllerProtocol {
Building now should give an error that SearchResultsViewController doesn’t conform to the APIControllerProtocol, great! This is exactly what a protocol does. It causes the compiler to spit out errors if you intended to implement a protocol, but didn’t completely do it. In this case it’s barfing because we’re missing the didReceiveAPIResults() callback method. Let’s add it…
We just need to add the function within our SearchResultsViewController class. It will look pretty much like the contents of our dataTaskWithURL closure from before.
func didReceiveAPIResults(results: NSArray) { dispatch_async(dispatch_get_main_queue(), { self.tableData = results self.appsTableView!.reloadData() }) }
The one thing left to do is change our API Controller to include a delegate object, and to call this method when the connection finished loading some API data.
Using the Protocol
Back in our APIController.swift file, let’s add the delegate and an empty constructor, just under the class definition.
var delegate: APIControllerProtocol?
The question mark at the end here indicates that delegate is an *optional* value. Without the question mark, we will get a compiler error for not using an initial value for this variable, but with it we’re okay. The delegate object can be of any class here, as long as it adheres to the APIControllerProtocol by defining the method didReceiveAPIResults(), as we have done in our SearchResultsViewController
Now that we have the delegate variable added, let’s head back to our SearchResultsViewController, and in the method viewDidLoad, let’s set the delegate for our api controller to itself, so it will receive delegate function calls.
api.delegate = self
Finally, in the dataTaskWithURL closure inside of our searchItunesFor() method, let’s add our protocol method where our tableview reloading code used to be. This will pass the responsibility of reloading the UI to the delegate, which in this case is SearchResultsViewController.
This goes in place of where we delete the lines refreshing the tableview earlier on.
if let results: NSArray = jsonResult["results"] as? NSArray { self.delegate?.didReceiveAPIResults(results) }
Here is the completed APIController.swift code up to this point:
import Foundation protocol APIControllerProtocol { func didReceiveAPIResults(results: NSArray) } class APIController { var delegate: APIControllerProtocol? 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 = "http://itunes.apple.com/search?term=\(escapedSearchTerm)&media=software" let url = NSURL(string: urlPath) 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() } } }
Common issue here
*** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[
If you get this issue, it could be caused by a few bugs in Xcode surrounding the renaming of View Controller subclasses. The best way to fix this is to open up the storyboard, select the ‘Search Results View Controller’ object. Then in the identity inspector change its class back to UIViewController, build the app, then change it back to ‘SearchResultsViewController’. This resets some things and fixes the issue.
PHEW!
Okay, I know that might have seemed like a lot of boilerplate, and now our app does the exact same thing as it did before, but now we have something much more flexible. We can now use APIController for any API call to the iTunes search API, and have a custom delegate get the response. I think we ran out of time on this one, so we’ll move on to the interaction in Part 4. If you want to follow along make sure to sign up for them via e-mail.
I felt this step was more important than fixing the performance issues, but I promise we’ll speed things up before we’re done. There are just some things we need to get done first. Hold tight!
Full code for this section visible here.
Thanks for the tutorial! I just wanted to point out one line of code you forgot to mention in your tutorial (but is in your Github code) that causes the app not to show the loaded data. The line api.delegate = self in viewDidLoad() needs to be in there in order for the api controller to call the controller’s implemented didReceiveAPIResults method.
Thanks!
When Creating the APIController class, it will be a subclass of NSObject for anyone unsure
If you’re not getting any results showing, make sure to do api.delegate=self in viewDidLoad
Holy shit I was about to tear my hair out figuring out why the APIController wasn’t sending back the info correctly, even though the dict info was correctly being stored, thank you for the tip~!
First off, thank you for this tutorial.
Since this is entitled best practices, this is probably the best place fro my question.
Take the func searchItunesFor, shouldn’t all of the var’s in this routine be let’s?
If I’m reading the Swift documentation correctly (which hey I’m learning, so I might not be) one should use let unless one actually changes the value. This being helpful to the compiler and general code safety.
Again, since this is the best practices section.
Curious about opinions on readability/style
var api: APIController = APIController()
versus
var api = APIController()
(or as per previous comment
let api = APIController()
)
I get APIController not constructible with ‘0’
(on line var api: APIController = APIController() )
Add an init method to the APIController.
init(){}
Should work fine.
Unfortunately, when putting an empty constructor, it says: ‘Must call a designated initializer of the class ‘UIView’ ‘
It seems to work by simply leaving out the init() method. looks like the default constructor is handling it well. This fixed the issue for me
First off, thanks for the great series. And as a sucker for clean and modular code, I love this part of the tutorial. Keep them coming 🙂
I want to say that I like this tutorial very much. But when I get to the point
“Let’s now move the API code out to it’s own class. Right click in the xcode navigation pane and select ‘New File…’. This one is going to be a Cocoa Touch Class, under the iOS->Source navigation option.
This one handles all API work, so I’m going to call it APIController.
Now let’s grab our searchItunesFor() function, and all our delegate functions like connection(didRecieveResponse….). Cut and paste these out of the Search controller and in to the APIController. You’ll also need to copy over the data variable for the response to build it’s results.”
This is when I get lost and everything on my files are giving me 9+ errors. I looked at your code and it doesn’t seem to fit what you said in tutorial. I thought it meant cut and paste said code into “APIController.swift” but those code remains in “SearchResultviewController.swift”…? I’m bit lost. Can you clarify?
Oh, and one more… when you said to make new file, you didn’t specify which type and I picked UIViewController and the result did not match your file as well. So I had to edit that out. Prior errors remains, however.
That’s a super easy fix! Just remove the “: UIViewController” from the class definition, and delete any functions it generated.
You might should just clone my copy of part 3 to get past these issues. Sorry about that :/
I did but I want to get better understanding of what’s happening so I can develop my own app. I’m doing a job relating to developing an app for an organization.
Question: do you think it’s critical that I learn xcode program itself or swift language? I think I have good understanding of swift but handling xcode program is kinda sketchy… Is it just me? 🙂
Also, thanks for your response!
I am getting Thread 1- EXC_BAD_INSTRUCTION at delegate?.didReciveAPIResults();
So far my guess is EXC_BAD_INSTRUCTION occurs if some uninitialized or unallocated object/variable is used for something. But as I am following your tutorial, and delegate is initialized in ViewController class, I think this shouldn’t be the case.
What do you think, why am I getting this? Waiting for your reply
Regards
Hard to say, can you post your code on github so I can take a look?
I got EXC_BAD_INSTRUCTION because I was accidentallly using an older iPhone in my emulator. Updaing the iPhone made this go away.
This error means that the emulator is seeing an machine instruction it doesn’t understand.
Ignore my previous comment on EXC_BAD_INSTRUCTION. I was mistaken.
Wouldn’t APIControllerDelegate be a better name than APIControllerProtocol? 🙂
Probably so, I always use one or the other but I’m not super consistent with it.
I’m getting this error:
2014-06-09 10:12:39.101 CheckPlease[4594:218563] *** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key dataSource.’
What gives?
You are trying to set a key named dataSource on an object that doesnt support it. Without your code and which line is erroring I’m not sure.
Can you tell me why when I comment out the dispatch_async in the didReceiveAPIResults, I don’t get any data. Why do you have to explicitly specify to Xcode that you need those lines sent immediately.
Thanks
I’ve tried to figure out why i’m getting this error. Even your Github code has the same error!
SearchResultsViewController:
self.api.delegate = self
The error is: ‘APIController’ does not have a member named ‘delegate’
Same are in the APIController for this line:
self.delegate?.didReceiveAPIResults(jsonResult)
Any Ideas what we’re doing wrong.
Carl
Ps pre-ordered your book.
Hey Carl, this would mean that line 16 here is missing: https://github.com/jquave/Swift-Tutorial/blob/Part3/SwiftTutorial/APIController.swift
Excellent, I had the line but placed it into the function and not above it. It’s always the simplest things that can just grind you to a halt.
Thanks!
Carl
P.s In your book could you possible talk about the following as some suggestions:
• Using Swift how can I set up the Tabs, Split view, Page view, Segmented Control & Navigation controllers. Then to possibly use a custom made navigation?
•Using swift to set up table views & table cells – possibly link to core data/cloud kit and search.
•Using the animation effects showing ways of implementation of swift and showing it’s different capabilities and visual effects to aid design.
•Using swift to show how user controls can be done – Date Picker, Picker view, Steppers, Sliders and Scroll View?
🙂
Carl, these are all great suggestions and I’m pretty confident that almost everything you mentioned is going to be included 🙂
It seems you’ve got a strong reference cycle between the SearchResultsViewController and the APIController. Neither of them will be automatically deleted once both are created and point to each other.
Back in the old Objective C days, delegates were always supposed to be weak. Should that be the case here?
(By the way, I’m really enjoying this tutorial. Great job!)
I’m a beginner of iOS development and Swift, so your tutorial is very helpful. Thank you!!
I typed your code and build it, but the app clashed at SearchResultsViewController’s “var api: APIController = APIController()” statement. I found the cause is that I write this before “@IBOutlet strong var appsTableView: …” statement.
Do you know the differences?
Is that possible to remove “let results: NSArray = jsonResult[“results”] as NSArray” in APIController.swift?
If it’s not used then yes. I think it might not be after some of the changes that happen.
You should exit the function when you encounter any type of error, i.e. just doing a simple return.