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 NSArrays.

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.

0 comments


Or enter your name and Email
No comments have been posted yet.