We are going to make an app that stores and displays races of birds, cats and dogs. We are going to use UITableView
to 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 UIAlertView
delegate 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 UITableViewDataSource
protocol.
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!
Hey guys, you have a “Step 12” after “Step 12” :) Good post
Thanks! Can’t believe I missed that
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!
Thank you!
Great Tutorial! Can you please explain how to add a new species (like how you add a new race)? Thanks so much.
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.
A little lost on the lesson. Can you include a link to download your source code?
Sure! Here you go
Excellent tutorial. It is extremely helpful for me. I’ve learned a lot Thank you.
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]’.
names
is an array of strings ([String]
) ->names[indexPath.row]
is aString
The autocomplete might have tricked you into this error. Do you have a name property as well?
Now it’s ok. My mistake. But I have another problem. pushViewController does not work. It just don’t redirect me to another controller.
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?
Hey! Thanks a lot for this tutorial!
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
Can you explain why species is declared this way?
var species: [String:[String]]
If it is a dictionary, why not:
var species: [String: String]
It’s still a dictionary. But the values are arrays of strings not just strings.