Converting an Objective-C App to Swift: Working With Web Data
Apple’s new language, Swift, is supposed to make writing native iOS apps a lot faster and more fun. However, porting apps from Objective-C to this new language might take some getting used to. In this post, we’ll be converting our Objective-C app that works with web data to Swift. We’ll start with the process of converting the Xcode project, and continue with rewriting our code in Swift.
Swift is supported in Xcode 6, which currently (as if July 2014) is a beta available to registered iOS developers. Download the Objective-C version for the project here to get started.
Converting the project
Apple makes it pretty easy to transition a project to Swift. You can migrate your code one class at a time. For our simple project, we’re going to migrate all three classes — the app delegate, main view controller, and detail view controller — at the same time.
Open the JSONRead project in Xcode. We’ll start by transitioning the AppDelegate files. Create a new file in Xcode (File > New > File…) and select a Cocoa Touch Class. Call it AppDelegate, make it a subclass of UIResponder and change the language to Swift. Once created, the file will have the following code (the exact code may change if you’re using a different version of Xcode):
import UIKit
class AppDelegate: UIResponder {
}
We’re starting to see the new syntax that Swift brings. The import
statement is used to bring in frameworks, but you don’t need to use quotes or angled brackets anymore. You no longer need to explicitly import your class files either.
Classes are declared with the class
keyword, and all properties and methods will go inside the braces. The example above indicates that our AppDelegate class is a subclass of the UIResponder, denoted with the colon.
We need to add @UIApplicationMain
to this file, right above the class declaration:
import UIKit
@UIApplicationMain
class AppDelegate // …
This represents the start of the program — the main()
function. We can now delete the main.m file (maybe in the Supporting Files group in Xcode), as this declaration replaces the need to have that main method.
Next, let’s create new Swift files for PeopleListViewController
and PeopleDetailsViewController
. You can delete the corresponding Objective-C .h
and .m
files now, or keep them around for comparison. However, they won’t mess up your project if you keep them around.
Filling in AppDelegate
Here is the code we need for the app delegate:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
var controller: UIViewController = PeopleListViewController(style: .Plain)
var navController: UINavigationController = UINavigationController(rootViewController: controller)
self.window!.rootViewController = navController
self.window!.makeKeyAndVisible()
return true
}
}
The first line in the class declares the property var window: UIWindow?
. We’ll talk about what the question mark is later in this post.
This is the same way a variable is declared anywhere in Swift. In fact, a property is effectively a variable in the class, just like you can create a variable within a method or loop. The var
keyword indicates that window
can have its value changed later. There is a corresponding keyword let
which indicates a “variable” that can only be set when it is declared and cannot be set later (it will trigger a compiler error if you try). This lets the compiler optimize for values that you want to refer to by name, but never change. We’ll have examples of that in a bit.
For this class, we’re only grabbing the code for the delegate method we need. In comparison, here is the code to do the same thing in Objective-C:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
PeopleListViewController *vc = [[PeopleListViewController alloc] init];
UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:vc];
self.window.rootViewController = nvc;
[self.window makeKeyAndVisible];
return YES;
}
In Swift, all functions and methods are defined with the func
keyword. Then comes the name of the function, followed by a list of arguments. In this case, the name of the function is simply application
, which takes two arguments. The first is application
, an instance of UIApplication
(as denoted by the colon — the same way we specify the type of a variable). Nothing special here, we see the same thing with the Objective-C version. The second argument takes advantage of Swift’s external parameter names syntax. The argument is called launchOptions
(and is of type NSDictionary?
— again, we’ll talk about the question mark a bit later) within the method, but it’s “labelled” as didFinishLaunchingWithOptions
. This is essentially a description of the second parameter, along with a shorter name to actually use within the method. Finally, the function declaration ends with -> Bool
. The arrow separates the function declaration from its return type, which in this case is Bool
.
In both versions, we set self.window
to a new instance of UIWindow
with a frame that covers the whole screen. Next, we create an instance of our list view controller and assign it to a variable called controller
:
var controller: UIViewController = PeopleListViewController(style: .Plain)
We’re declaring controller
to be of type UIViewController
and assigning it to a new instance of PeopleListViewController
. In Swift, classes are initialized using a constructor syntax similar to that found in Java, where the name of the class is followed by parentheses and arguments list. In this case, our view controller is actually a table view controller, so we initialize it with a style: .Plain
. The argument is an enum of type UITableViewStyle
, and because the type is known you can simply use the dot-syntax to refer to the value you want, rather than the verbose UITableViewStylePlain
.
We then initialize a navigation controller with our list controller as the root. We assign the rootViewController
on our window to the nav controller, and call the makeKeyAndVisible()
method on our window. In Swift, all methods are called with a dot and parentheses, rather than the square brackets. Finally, we return true
to finish the method.
Optionals
Let’s talk about the question mark and exclamation marks we saw above. They’re designed to prevent you from calling a method on nil
, which may cause a crash.
You’re not allowed to assign a (potentially) nil
value to a variable in Swift — it will trigger a compiler error. In order to do so (such as when dequeuing table view cells), you have to explicitly say so using a question mark:
var questionableString: NSString?
This line says that questionableString
may hold a string, or it may be nil
. The actual value is then ‘wrapped up’ by the compiler into an Optional
object. To get the value back, you use the exclamation mark:
var questionableLength: Int = questionableString!.length()
There are a few more things you can do with Optionals, most of which can make your code a little more terse. For details, see this post.
PeopleListViewController
Here is the code for PeopleListViewController
:
import UIKit
class PeopleListViewController: UITableViewController {
let JSON_FILE_URL: NSString = "https://raw.githubusercontent.com/Binpress/learn-objective-c-in-24-Days/master/Working%20With%20Web%20Data/JSONRead.json"
var names: NSArray
var data: NSArray
init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
self.names = []
self.data = []
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
init(style: UITableViewStyle) {
self.names = []
self.data = []
super.init(style: style)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "JSONRead"
// Download JSON
let url = NSURL(string: JSON_FILE_URL)
let JSONData = NSData(contentsOfURL: url)
let JSONResult = NSJSONSerialization.JSONObjectWithData(JSONData, options: nil, error: nil) as NSArray
var _names: NSMutableArray = NSMutableArray()
for item: AnyObject in JSONResult {
let fname: NSString = item["fname"] as NSString
let lname: NSString = item["lname"] as NSString
let name: NSString = "\(fname) \(lname)"
_names.addObject(name)
}
self.names = _names
self.data = JSONResult
}
override func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
return 1
}
override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return self.names.count
}
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let CellID: NSString = "Cell"
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier(CellID) as? UITableViewCell
if !cell {
cell = UITableViewCell(style: .Default, reuseIdentifier: CellID)
}
cell!.textLabel.text = self.names[indexPath.row] as NSString
return cell
}
override func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
let detailsController: PeopleDetailsViewController = PeopleDetailsViewController(style: .Grouped)
detailsController.details = self.data[indexPath.row] as NSDictionary
self.navigationController!.pushViewController(detailsController, animated: true)
}
}
In this class, we’re using a constant (let
) to represent the URL of the JSON file we’re downloading. That’s a value that won’t change, so we can let the compiler optimize the code it generates by marking it as such. We also declare two properties as NSArray
s.
Next, we find two initializers. These are the methods which get called when we instantiate a class. Although both are called init
, they are overloaded because they take different arguments.
When we override inherited methods, we need the override
keyword. This makes us explicitly state that we’re overriding an existing implementation and will trigger a compiler error if you don’t. This prevents overriding a method you didn’t know about, and allows the compiler to check that your version has the same arguments list and return value as an existing one.
Within viewDidLoad
, we download the JSON data, parse the names for the current view and split out the details for the next view controller.
let JSONResult = NSJSONSerialization.JSONObjectWithData(JSONData, options: nil, error: nil) as NSArray
Here, we’re casting the return value of JSONObjectWithData
, which is AnyObject!
(analogous to id
in ObjC) to an NSArray
by using the as
keyword. This allows us to enumerate over the contents in a for-in
loop:
`for item: AnyObject in JSONResult`
Inside the loop, we combine strings using the interpolation format:
`"\(fname) \(lname)"`
By using backslashes and surrounding variables in parenthesis, we can interpolate them into a string.
We can also cast to an Optional, as we do in cellForRowAtIndexPath
:
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier(CellID) as? UITableViewCell
Here, it’s possible that there will be no cells to dequeue, so the variable may be nil
. Note that we’re casting using as?
, with the question mark.
Finally, note that many of the datasource methods are all called tableView
, and are overloaded by the parameters list. The methods use the external parameter name, which correspond to the names the methods had in ObjC. For example,
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
…becomes…
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
PeopleDetailsViewController
Here is the code for PeopleDetailsViewController
:
import UIKit
class PeopleDetailsViewController: UITableViewController {
var details: NSDictionary
init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
details = NSDictionary()
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// Custom initialization
}
init(style: UITableViewStyle) {
details = NSDictionary()
super.init(style: style)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = self.name()
}
func name() -> NSString {
let fname: NSString = self.details["fname"] as NSString
let lname: NSString = self.details["lname"] as NSString
return "\(fname) \(lname)"
}
override func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
return 1
}
override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let CellID: NSString = "Cell"
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier(CellID) as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: .Value1, reuseIdentifier: CellID)
}
switch indexPath.row {
case 0:
cell!.textLabel.text = self.name()
cell!.detailTextLabel.text = "Name"
case 1:
var email: NSString? = self.details["email"] as? NSString
if !email {
email = "No email"
}
cell!.textLabel.text = email
cell!.detailTextLabel.text = "Email"
case 2:
cell!.textLabel.text = self.details["phone"] as NSString
cell!.detailTextLabel.text = "Phone"
default:
break
}
return cell
}
}
Here again, we find less boilerplate code than the corresponding Objective-C version. The only new construct here is the switch
statement. Although it doesn’t matter here, note that execution no longer ‘falls through’ the cases, so you don’t need a break
statement at the end of every case. You also no longer need to use braces around each case.
Moving forward
Getting into Swift, I found that the hardest part was figuring out how all the new stuff fits in with existing code and paradigms. This post should serve as a solid introduction. You can now delete the Objective-C files from your project.
Apple has published a solid guide to Swift, available on iBooks as an ePub. Give it a read for a closer look at the new language.
Download the source code of this tutorial’s app right here.
Author: Feifan Zhou