Birds, Cats and Dogs

We are going to make an app that stores and displays races of birds, cats and dogs. We are going to use UITableViewto show the list of species and races, NSUserDefaults for data persistence and UIWebView to get more information about each race on Wikipedia.

Step 1: Create a new project

Open Xcode and create a new iOS project using the Single View Application template. Name the project Birds, Cats and Dogs.

Step 2: Refactoring

Refactoring does not work for Swift yet, so we will need to do the steps by hand.

The templates creates a file named ViewController.swift. Rename it to SpeciesViewController.swift. Inside the file change the class name to SpeciesViewController.

Open the main storyboard and select View Controller. In the Identity Inspector change the class of the view controller to SpeciesViewController.

Run the project now to see if it works!

Step 3: Add Navigation Controller

Open the main storyboard and add a UINavigationController from the components library.

Remove the root view controller it created.

Make Navigation Controller the initial view controller of the storyboard, from the Attributes Inspector.

an arrow will point to Navigation Controller

Set Species View Controller as the root view controller of the Navigation Controller from the Connections Inspector.

In SpeciesViewController set the title to "Species" in viewDidLoad.

class SpeciesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Species"
    }

    ...

}

Run the project to see if it works! The navigation bar should be visible.

Step 4: Load data

NSUserDefaults provides a simple programmatic interface for local key-value persistence, usually for application options(ex. measuring units or font sizes):

// NSUserDefaults gives a shared instance through the standardUserDefaults class method
let userDefaults = NSUserDefaults.standardUserDefaults()

// valueForKey is used to retreive values
if let soundOn = userDefaults.valueForKey("sound_on") as? Bool {
    if soundOn {
        println("true")
    } else {
        println("false")
    }
}

// setValue:forKey: to set values
userDefaults.setValue(nil, forKey: "sound_on")

We are going to make a new class DataManager with a shared instance(singleton) that will get the data fromNSUserDefaults and store any changes we make to it. You can read more about singletons in our Object Oriented Programming in Swift post.

Create a new swift file. Name it DataManager.

Open DataManager.swift and add the singleton boilerplate code.

import Foundation

class DataManager {
    struct Static {
        static var onceToken : dispatch_once_t = 0
        static var instance : DataManager? = nil
    }

    class var sharedInstance : DataManager {
        dispatch_once(&Static.onceToken) {
            Static.instance = DataManager()
        }
        return Static.instance!
    }
}

Add a property names species of type [String:[String]]. In the init method, load the user defaults for the key"species". If no data was stored populate species with a default value.

class DataManager {
    var species: [String:[String]]

    init() {
        let userDefaults = NSUserDefaults.standardUserDefaults()
        if let speciesInfo = userDefaults.valueForKey("species") as? [String:[String]] {
            species = speciesInfo
        } else {
            // add default data
            species = [
                "Birds": ["Swift"],
                "Cats" : ["Persian Cat"],
                "Dogs" : ["Labrador Retriever"]
            ]
        }
    }

    ...
}

Now we can get the data by calling DataManager.sharedInstance.species.

Quick Tip: How to make a code snippet in XCode

If you find yourself writing the same code many times, it might be a good idea to make a snippet.

Open the code library. It should be in the bottom right corner of XCode.

Select the code you want to turn into a snippet.

Drag and drop the code into the code library.

A popup will appear. Give your snippet a name and other details.

You can have custom parameters in your snippets. Anyware you add the <#paramName#> pattern it will come up as a replaceable parameter that you can tab between just like in the default autocomplete.

I’ve replaced DataManager with <#SingletonClass#>

Step 5: Load the list of species

In DataManager.swift add a computed property named speciesList. Get the list of species from the keys of the dictionary that holds all the data. Dictionaries are not ordered collections so remember to sort the keys after collecting them.

class DataManager {
    var species: [String:[String]]

    var speciesList: [String] {
        var list: [String] = []
        for speciesName in species.keys {
            list.append(speciesName)
        }

        list.sort(<)

        return list
    }

    ...
}

In SpeciesViewController add a property that will store the list of species. Initialize that property withDataManager.sharedInstance.speciesList

class SpeciesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var species: [String] = DataManager.sharedInstance.speciesList

    ...
}

Step 6: Add a table view

Open Main.storyboard. Drag a table view from the component library into Species View Controller.

Select the table view and hold Control. Click and drag on to the view to add constraints for:

  • Equal widths
  • Equal heights
  • Center horizontally in container
  • Center vertically in container

Add the UITableViewDataSource protocol and a tableView: UITableView! property to SpeciesViewController.

class SpeciesViewController: UIViewController, UITableViewDataSource  {
    @IBOutlet var tableView: UITableView!

    ...    
}

Connect the tableView outlet in Interface Builder.

Connect the dataSource outlet.

In viewDidLoad register the UITableViewCell class to the cell identifier cell.

class SpeciesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    ...

    override func viewDidLoad() {
        ...

        tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    ...
}

Implement the data source methods.

class SpeciesViewController: UIViewController, UITableViewDataSource {

    ...

    // Table View Data Source methods

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

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell

        cell.textLabel.text = species[indexPath.row]

        return cell
    }
}

Run the project now to see if it works.

Step 7: Create the Races Controller

Add a new file using the Cocoa Touch Class template. Set the name to RacesViewController and the subclass toUIViewController.

Open Main.storyboard and add a View Controller from the component library.

Select the newly added view controller and open the Identity Inspector. Set the class and Storyboard ID toRacesViewController.

In RacesViewController.swift add the property species and set the title to the value of species in viewDidLoad.

class RacesViewController: UIViewController {
    var species: String!

    override func viewDidLoad() {
        super.viewDidLoad()

        title = species
    }
}

Step 8: Handle Table Selection

Open SpeciesViewController and add a disclosure indicator to the table view cells in order to indicate that they can be selected.

class SpeciesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    ...

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell

        cell.textLabel.text = species[indexPath.row]
        cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator

        return cell
    }

    ...

}

Connect the tableView delegate outlet to the view controller in Interface Builder.
Implement the didSelectRowAtIndexPath delegate method on SpeciesViewController.

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)

        var racesViewController = storyboard?.instantiateViewControllerWithIdentifier("RacesViewController") as RacesViewController

        racesViewController.species = species[indexPath.row]

        navigationController?.pushViewController(racesViewController, animated: true)
    }

Step 9: Show Races

In Interface Builder add the table view in RacesViewController. Set the constraints for the table view like in Step 6.

Add a tableView property on RacesViewController and add the UITableViewDataSource andUITableViewDelegate protocols. Connect the tableView the outlets in Interface Builder.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    @IBOutlet var tableView: UITableView!

    ...
}

Add a races computed property on RacesViewControllr.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    ...

    var races: [String] {
        return DataManager.sharedInstance.species[species]!
    }

    ...

}

Register the UITableViewCell class.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        title = species

        tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
}

Implement the data source methods.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

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

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell

        cell.textLabel.text = races[indexPath.row]
        cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator

        return cell
    }
}

Run the project now.

Step 10: Saving Changes

We want to create new races. To do that we need a method for adding a new race to a species and one that will save the data in NSUserDefaults.

class DataManager {
    ...

    func saveData() {
        let userDefaults = NSUserDefaults.standardUserDefaults()
        userDefaults.setValue(species, forKey: "species")
    }

    func addRace(species inSpecies: String, race: String) {
        if var races = species[inSpecies] {
            races.append(race)
            species[inSpecies] = races
        }

        saveData()
    }

Step 11: Add button

Open the storyboard and select Race View Controller. In the Attributes Inspector set the top bar to a Translucent Navigation Bar.

Drag a Navigation Item from the components library on the view controller. This should add a title element on the navigation bar.

Drag a Bar Button Item from the components library on the navigation bar on the right side. Select the button and change the type to Add in the Attributes Inspector.

Add a method for the button event.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    ...

    @IBAction func didTapAdd() {

    }
}

In the storyboard select the Add button and connect the selector outlet with didTapAdd.

Step 12: AlertView

In the didTapAdd method create a new UIAlerView with the PlainTextInput style. Set RacesViewController as the delegate for the alert view. Then show the alert.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIAlertViewDelegate {
    ...

    @IBAction func didTapAdd() {
        var alert = UIAlertView(title: "New Race", message: "Type in a new race", delegate: self,
            cancelButtonTitle: "Cancel",
            otherButtonTitles: "Add")

        alert.alertViewStyle = UIAlertViewStyle.PlainTextInput

        alert.show()
    }
}

If you press the Add button now you should see an alert view like this one:

To get the text we need to implement the alertView:didDismissWithButtonIndex: method from the UIAlertViewdelegate protocol.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIAlertViewDelegate {
    ...

    func alertView(alertView: UIAlertView, didDismissWithButtonIndex buttonIndex: Int) {
        if buttonIndex == 1 {
            // there is only one text field
            var textField = alertView.textFieldAtIndex(0)!

            // get the new races and capitalize the string
            var newRace = textField.text.capitalizedString

            // add the new race in the list
            DataManager.sharedInstance.addRace(species: species, race: newRace)

            // create the index path for the last cell
            var newIndexPath = NSIndexPath(forRow: races.count - 1, inSection: 0)

            // insert the new cell in the table view and show an animation
            tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
        }
    }
}

Try it out!

Step 13: Remove Race

Implement a method on DataManager that will remove a race from a species.

class DataManager {
    ...

    func removeRace(species inSpecies: String, race inRace: String) {
        if var races = species[inSpecies] {
            var index = -1

            for (idx, race) in enumerate(races) {
                if race == inRace {
                    index = idx
                    break
                }
            }

            if index != -1 {
                races.removeAtIndex(index)
                species[inSpecies] = races
                saveData()
            }

        }
    }

}

Implement the canEditRowAtIndexPath and commitEditingStyle methods from the UITableViewDataSourceprotocol.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIAlertViewDelegate {
    ...

    func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        return true
    }

    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        var raceToRemove = races[indexPath.row]

        DataManager.sharedInstance.removeRace(species: species, race: raceToRemove)

        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
    }

    ...

}

Step 14: Create the WebViewController

Create a new view controller using the Cocoa Touch Class template. Name it WebViewController.

Open Main.storyboard and add a View Controller from the component library.

Select the newly added view controller and open the Identity Inspector. Set the class and Storyboard ID toWebViewController.

Add a Web View from the components library and add constraints for:

  • Equal widths
  • Equal heights
  • Center horizontally in container
  • Center vertically in container

In WebViewController.swift add a webView property and then connect the outlet in Interface Builder.

class WebViewController: UIViewController {
    @IBOutlet var webView: UIWebView!

    ...

}

Add a url: NSURL! property. In viewDidLoad load the url in the webView.

class WebViewController: UIViewController {
    @IBOutlet var webView: UIWebView!

    var url: NSURL!

    override func viewDidLoad() {
        super.viewDidLoad()

        webView.loadRequest(NSURLRequest(URL: url))
    }

}

Step 15: Generate Wiki URL

In DataManager create a class method that will create the Wikipedia search link for a race. When you search for something on wikipedia it redirects you to http://en.wikipedia.org/wiki/<search_keywords>.

    class func urlForRace(race: String) -> NSURL {
        // replace spaces with _
        var safeString = race.stringByReplacingOccurrencesOfString(" ", withString: "_", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)

        return NSURL(string: "http://en.wikipedia.org/wiki/" + safeString)!
    }

Step 16: Handle Selection

In RaceViewController implement the tableView:didSelectRowAtIndexPath: method from the delegate protocol. When a race is selected generate the wiki url and push a WebViewController to open it.

class RacesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIAlertViewDelegate {

    ...

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        var race = races[indexPath.row]

        var url = DataManager.urlForRace(race)

        var webViewController = storyboard?.instantiateViewControllerWithIdentifier("WebViewController") as WebViewController

        webViewController.url = url

        navigationController?.pushViewController(webViewController, animated: true)
    }

    ...

}

If you haven’t missed any step you should be able to see the wikipedia pages of all the races you stored.

Hope you liked the tutorial. Here is the full code.

Thank you for reading! :)

  19 comments for “Birds, Cats and Dogs

  1. December 5, 2014 at 9:30 am

    Hey guys, you have a “Step 12” after “Step 12” :) Good post

    • December 5, 2014 at 2:46 pm

      Thanks! Can’t believe I missed that :P

  2. Chris
    December 10, 2014 at 5:07 pm

    This tutorial was great!! I’m brand new to coding, and am a little lost, but things were explained very well :)

    I’m trying to adapt this idea to fit for a gift list, where users can add people AND gifts. I’m having trouble figuring out how to add a person to the main list the way you added a race to the race list. Is there a quick explanation for this? I’d really appreciate it. Thanks!

    • December 10, 2014 at 6:46 pm

      Thank you! :)

    • Zach
      February 23, 2015 at 12:12 am

      Great Tutorial! Can you please explain how to add a new species (like how you add a new race)? Thanks so much.

      • February 23, 2015 at 4:39 pm

        You will need to change the way you store species. Instead of using a dictionary use an array. This will solve the ordering problem as well.

  3. Chris
    December 11, 2014 at 8:59 am

    A little lost on the lesson. Can you include a link to download your source code?

  4. December 15, 2014 at 3:50 pm

    Excellent tutorial. It is extremely helpful for me. I’ve learned a lot Thank you.

  5. Jirka
    January 8, 2015 at 10:44 pm

    Tutorial from Andrei Puni are really the best. I’ve made entire app with API because of his tutorials.

    I have just one question.

    What should I do if I have defined: var names: [String] = []

    In names I have stored names from API.

    and then want to do this: racesViewController.names = names[indexPath.row]

    Compiler shows me error ‘String’ is not convertible to ‘[String]’.

    • January 8, 2015 at 10:54 pm

      names is an array of strings ([String]) -> names[indexPath.row] is a String
      The autocomplete might have tricked you into this error. Do you have a name property as well?

      • Jirka
        January 10, 2015 at 3:46 pm

        Now it’s ok. My mistake. But I have another problem. pushViewController does not work. It just don’t redirect me to another controller.

    • Zach
      February 23, 2015 at 12:20 am

      Hopefully you can solve another problem: How can I make it so that users can reorder the species? I know you have to remove list.sort(), but how do you proceed from there?

  6. Anuj
    February 16, 2015 at 1:46 pm

    Hey! Thanks a lot for this tutorial! :)

  7. Benson
    July 9, 2015 at 10:43 pm

    great work~~!!! just one problem UIAlertView is deprecated and the method can not be called that way. see:http://stackoverflow.com/questions/24022479/how-would-i-create-a-uialertview-in-swift

    great work~~!!! thanks

  8. James
    November 7, 2015 at 4:05 am

    Can you explain why species is declared this way?

    var species: [String:[String]]

    If it is a dictionary, why not:

    var species: [String: String]

    • November 8, 2015 at 9:07 pm

      It’s still a dictionary. But the values are arrays of strings not just strings.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe
We send about one email per week with our latest tutorials and updates
Never display this again :)