Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon

Creating a City Information App with Customized Table Views

Save for later
  • 19 min read
  • 08 Oct 2015

article-image

In this article by Cecil Costa, the author of Swift 2 Blueprints, we will cover the following:

  • Project overview
  • Setting it up
  • The first scene
  • Displaying cities information

(For more resources related to this topic, see here.)

Project overview

The idea of this app is to give users information about cities such as the current weather, pictures, history, and cities that are around.

How can we do it? Firstly, we have to decide on how the app is going to suggest a city to the user. Of course, the most logical city would be the city where the user is located, which means that we have to use the Core Location framework to retrieve the device's coordinates with the help of GPS.

Once we have retrieved the user's location, we can search for cities next to it. To do this, we are going to use a service from http://www.geonames.org/.

Other information that will be necessary is the weather. Of course, there are a lot of websites that can give us information on the weather forecast, but not all of them offer an API to use it for your app. In this case, we are going to use the Open Weather Map service.

What about pictures? For pictures, we can use the famous Flickr. Easy, isn't it? Now that we have the necessary information, let's start with our app.

Setting it up

Before we start coding, we are going to register the needed services and create an empty app. First, let's create a user at geonames. Just go to http://www.geonames.org/login with your favorite browser, sign up as a new user, and confirm it when you receive a confirmation e-mail. It may look like everything has been done, however, you still need to upgrade your account to use the API services. Don't worry, it's free! So, open http://www.geonames.org/manageaccount and upgrade your account.

Don't use the user demo provided by geonames, even for development. This user exceeds its daily quota very frequently.

With geonames, we can receive information on cities by their coordinates, but we don't have the weather forecast and pictures. For weather forecasts, open http://openweathermap.org/register and register a new user and API.

Lastly, we need a service for the cities' pictures. In this case, we are going to use Flickr. Just create a Yahoo! account and create an API key at https://www.flickr.com/services/apps/create/.

While creating a new app, try to investigate the services available for it and their current status. Unfortunately, the APIs change a lot like their prices, their terms, and even their features.

Now, we can start creating the app. Open Xcode, create a new single view application for iOS, and call it Chapter 2 City Info. Make sure that Swift is the main language like the following picture:

creating-city-information-app-customized-table-views-img-0

The first task here is to add a library to help us work with JSON messages. In this case, a library called SwiftyJSON will solve our problem. Otherwise, it would be hard work to navigate through the NSJSONSerialization results.

Download the SwiftyJSON library from https://github.com/SwiftyJSON/SwiftyJSON/archive/master.zip, then uncompress it, and copy the SwiftyJSON.swift file in your project.

Another very common way of installing third party libraries or frameworks would be to use CocoaPods, which is commonly known as just PODs. This is a dependency manager, which downloads the desired frameworks with their dependencies and updates them. Check https://cocoapods.org/ for more information.

Ok, so now it is time to start coding. We will create some functions and classes that should be common for the whole program. As you know, many functions return NSError if something goes wrong. However, sometimes, there are errors that are detected by the code, like when you receive a JSON message with an unexpected struct. For this reason, we are going to create a class that creates custom NSError. Once we have it, we will add a new file to the project (command + N) called ErrorFactory.swift and add the following code:

import Foundation
class ErrorFactory {{
   static let Domain = "CityInfo"
   enum Code:Int {
       case WrongHttpCode = 100,
       MissingParams = 101,
       AuthDenied = 102,
       WrongInput = 103
   }

   class func error(code:Code) -> NSError{
     let description:String
       let reason:String
       let recovery:String
       switch code {
       case .WrongHttpCode:
           description = NSLocalizedString("Server replied wrong
           code (not 200, 201 or 304)", comment: "")
           reason = NSLocalizedString("Wrong server or wrong
           api", comment: "")
           recovery = NSLocalizedString("Check if the server is
           is right one", comment: "")
       case .MissingParams:
           description = NSLocalizedString("There are some
           missing params", comment: "")
           reason = NSLocalizedString("Wrong endpoint or API
           version", comment: "")
           recovery = NSLocalizedString("Check the url and the
           server version", comment: "")
       case .AuthDenied:
           description = NSLocalizedString("Authorization
           denied", comment: "")
           reason = NSLocalizedString("User must accept the
           authorization for using its feature", comment: "")
           recovery = NSLocalizedString("Open user auth panel.",
           comment: "")
       case .WrongInput:
           description = NSLocalizedString("A parameter was
           wrong", comment: "")
           reason = NSLocalizedString("Probably a cast wasn't
           correct", comment: "")
           recovery = NSLocalizedString("Check the input
           parameters.", comment: "")
       }
return NSError(domain: ErrorFactory.Domain, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey: description, NSLocalizedFailureReasonErrorKey: reason, NSLocalizedRecoverySuggestionErrorKey: recovery ]) } }

The previous code shows the usage of NSError that requires a domain, which is a string that differentiates the error type/origin and avoids collisions in the error code.

The error code is just an integer that represents the error that occurred. We used an enumeration based on integer values, which makes it easier for the developer to remember and allows us to convert its enumeration to an integer easily with the rawValue property.

The third argument of an NSError initializer is a dictionary that contains messages, which can be useful to the user (actually to the developer). Here, we have three keys:

  • NSLocalizedDescriptionKey: This contains a basic description of the error
  • NSLocalizedFailureReasonErrorKey: This explains what caused the error
  • NSLocalizedRecoverySuggestionErrorKey: This shows what is possible to avoid this error

As you might have noticed, for these strings, we used a function called NSLocalizedString, which will retrieve the message in the corresponding language if it is set to the Localizable.strings file.

So, let's add a new file to our app and call it Helpers.swift; click on it for editing. URLs have special character combinations that represent special characters, for example, a whitespace in a URL is sent as a combination of %20 and a open parenthesis is sent with the combination of %28. The stringByAddingPercentEncodingWithAllowedCharacters string method allows us to do this character conversion.

If you need more information on the percent encoding, you can check the Wikipedia entry at https://en.wikipedia.org/wiki/Percent-encoding. As we are going to work with web APIs, we will need to encode some texts before we send them to the corresponding server.

Type the following function to convert a dictionary into a string with the URL encoding:

func toUriEncoded(params: [String:String]) -> String {
   var records = [String]()
   for (key, value) in params {
       let valueEncoded = value.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
       records.append("(key)=(valueEncoded!)")
   }
   return "&".join(records)
}

Another common task is to call the main queue. You might have already used a code like dispatch_async(dispatch_get_main_queue(), {() -> () in … }), however, it is too long. We can reduce it by calling it something like M{…}. So, here is the function for it:

func M(((completion: () -> () ) {
   dispatch_async(dispatch_get_main_queue(), completion)
}

A common task is to request for JSON messages. To do so, we just need to know the endpoint, the required parameters, and the callback. So, we can start with this function as follows:

func requestJSON(urlString:String, params:[String:String] = [:],
completion:(JSON, NSError?) -> Void){
   let fullUrlString = "(urlString)?(toUriEncoded(params))"
   if let url = NSURL(string: fullUrlString) {
   NSURLSession.sharedSession().dataTaskWithURL(url) { (data,
   response, error) -> Void in
       if error != nil {
           completion(JSON(NSNull()), error)
           return
       }

       var jsonData = data!
       var jsonString = NSString(data: jsonData, encoding:
       NSUTF8StringEncoding)!

Here, we have to add a tricky code, because the Flickr API is always returned with a callback function called jsonFlickrApi while wrapping the corresponding JSON. This callback must be removed before the JSON text is parsed. So, we can fix this issue by adding the following code:

    // if it is the Flickr response we have to remove the
       callback function jsonFlickrApi()
       // from the JSON string
       if (jsonString as
     String).characters.startsWith("jsonFlickrApi(".characters)
{
           jsonString =
jsonString.substringFromIndex("jsonFlickrApi(".characters.count)
           let end = (jsonString as String).characters.count - 1
           jsonString = jsonString.substringToIndex(end)
           jsonData =
jsonString.dataUsingEncoding(NSUTF8StringEncoding)!
       }
Now, we can complete this function by creating a JSON object and calling the callback:
       let json = JSON(data:jsonData)
       completion(json, nil)
   }.resume()
   }else {
       completion(JSON(NSNull()),
       ErrorFactory.error(.WrongInput))
   }
}

At this point, the app has a good skeleton. It means that, from now on, we can code the app itself.

The first scene

Create a project group (command + option + N) for the view controllers and move the ViewController.swift file (created by Xcode) to this group. As we are going to have more than one view controller, it is also a good idea to rename it to InitialViewController.swift:

creating-city-information-app-customized-table-views-img-1

Now, open this file and rename its class from ViewController to InitialViewController:

class InitialViewController: UIViewController {

Once the class is renamed, we need to update the corresponding view controller in the storyboard by:

  • Clicking on the storyboard.
  • Selecting the view controller (the only one we have till now).
  • Going to the Identity inspector by using the command+ option + 3 combination. Here, you can update the class name to the new one.
  • Pressing enter and confirming that the module name is automatically updated from None to the product name.

The following picture demonstrates where you should do this change and how it should be after the change:

creating-city-information-app-customized-table-views-img-2

Great! Now, we can draw the scene. Firstly, let's change the view background color. To do it, select the view that hangs from the view controller. Go to the Attribute Inspector by pressing command+ option + 4, look for background color, and choose other, as shown in the following picture:

creating-city-information-app-customized-table-views-img-3

When the color dialog appears, choose the Color Sliders option at the top and select the RGB Sliders combo box option. Then, you can change the color as per your choice. In this case, let's set it to 250 for the three colors:

creating-city-information-app-customized-table-views-img-4

Before you start a new app, create a mockup of every scene. In this mockup, try to write down the color numbers for the backgrounds, fonts, and so on. Remember that Xcode still doesn't have a way to work with styles as websites do with CSS, meaning that if you have to change the default background color, for example, you will have to repeat it everywhere.

On the storyboard's right-hand side, you have the Object Library, which can be easily accessed with the command + option + control + 3 combination. From there, you can search for views, view controllers, and gestures, and drag them to the storyboard or scene. The following picture shows a sample of it:

creating-city-information-app-customized-table-views-img-5

Now, add two labels, a search bar, and a table view. The first label should be the app title, so let's write City Info on it. Change its alignment to center, the font to American Typewriter, and the font size to 24.

On the other label, let's do the same, but write Please select your city and its font size should be 18. The scene must result in something similar to the following picture:

creating-city-information-app-customized-table-views-img-6

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at €18.99/month. Cancel anytime

Do we still need to do anything else on this storyboard scene? The answer is yes. Now it is time for the auto layout, otherwise the scene components will be misplaced when you start the app.

There are different ways to add auto layout constraints to a component. An easy way of doing it is by selecting the component by clicking on it like the top label. With the control key pressed, drag it to the other component on which the constraint will be based like the main view. The following picture shows a sample of a constraint being created from a table to the main view:

creating-city-information-app-customized-table-views-img-7

Another way is by selecting the component and clicking on the left or on the middle button, which are to the bottom-right of the interface builder screen. The following picture highlights these buttons:

creating-city-information-app-customized-table-views-img-8

Whatever is your favorite way of adding constraints, you will need the following constraints and values for the current scene:

  • City Info label Center X equals to the center of superview (main view), value 0
  • City Info label top equals to the top layout guide, value 0
  • Select your city label top vertical spacing of 8 to the City Info label
  • Select your city label alignment center X to superview, value 0
  • Search bar top value 8 to select your city label
  • Search bar trailing and leading space 0 to superview
  • Table view top space (space 0) to the search bar
  • Table view trailing and leading space 0 to the search bar
  • Table view bottom 0 to superview

Before continuing, it is a good idea to check whether the layout suits for every resolution. To do it, open the assistant editor with command + option + .return and change its view to Preview:

creating-city-information-app-customized-table-views-img-9

Here, you can have a preview of your screen on the device. You can also rotate the screens by clicking on the icon with a square and a arched arrow over it:

creating-city-information-app-customized-table-views-img-10

Click on the plus sign to the bottom-left of the assistant editor to add more screens:

creating-city-information-app-customized-table-views-img-11

Once you are happy with your layout, you can move on to the next step. Although the storyboard is not yet done, we are going to leave it for a while.

Click on the InitialViewController.swift file. Let's start receiving information on where the device is using the GPS. To do it, import the Core Location framework and set the view controller as a delegate:

import CoreLocation

class InitialViewController: UIViewController,
CLLocationManagerDelegate {

After this, we can set the core location manager as a property and initialize it on viewDidLoadMethod. Type the following code to set locationManager and initialize InitialViewController:

   var locationManager = CLLocationManager()

   override func viewDidLoad() {
       super.viewDidLoad()
       locationManager.delegate = self
       locationManager.desiredAccuracy =
       kCLLocationAccuracyThreeKilometers
       locationManager.distanceFilter = 3000
       if
locationManager.respondsToSelector(Selector("requestWhenInUseAuthorization")) {
           locationManager.requestWhenInUseAuthorization()
       }
       locationManager.startUpdatingLocation()
   }

After initializing the location manager, we have to check whether the GPS is working or not by implementing the didUpdateLocations method. Right now, we are going to print the last location and nothing more:

   func locationManager(manager: CLLocationManager!,
   didUpdateLocations locations: [CLLocation]!){
       let lastLocation = locations.last!
       print(lastLocation)
   }

Now, we can test the app. However, we still need to perform one more step. Go to your Info.plist file by pressing command + option + J and the file name. Add a new entry with the NSLocationWhenInUseUsageDescription key and change its type to String and its value to This app needs to know your location.

creating-city-information-app-customized-table-views-img-12

This last step is mandatory since iOS 8. Press play and check whether you have received a coordinate, but not very frequently.

Displaying cities information

The next step is to create a class to store the information received from the Internet. In this case, we can do it in a straightforward manner by copying the JSON object properties in our class properties. Create a new group called Models and, inside it, a file called CityInfo.swift. There you can code CityInfo as follows:

class CityInfo {
   var fcodeName:String?
   var wikipedia:String?
   var geonameId: Int!
   var population:Int?
   var countrycode:String?
   var fclName:String?
   var lat : Double!
   var lng: Double!
   var fcode: String?
   var toponymName:String?
   var name:String!
   var fcl:String?

   init?(json:JSON){){){
       // if any required field is missing we must not create the
       object.
       if let name = json["name"].string,,, geonameId =
       json["geonameId"].int, lat = json["lat"].double,
           lng = json["lng"].double {
           self.name = name
           self.geonameId = geonameId
           self.lat = lat
           self.lng = lng
       }else{
           return nil
       }

       self.fcodeName = json["fcodeName"].string
       self.wikipedia = json["wikipedia"].string
       self.population = json["population"].int
       self.countrycode = json["countrycode"].string
       self.fclName = json["fclName"].string
       self.fcode = json["fcode"].string
       self.toponymName = json["toponymName"].string
       self.fcl = json["fcl"].string
   }
}

Pay attention that our initializer has a question mark on its header; this is called a failable initializer. Traditional initializers always return a new instance of the newly requested object. However, with failable initializers, you can return a new instance or a nil value, indicating that the object couldn't be constructed.

In this initializer, we used an object of the JSON type, which is a class that belongs to the SwiftyJSON library/framework. You can easily access its members by using brackets with string indices to access the members of a json object, like json ["field name"], or using brackets with integer indices to access elements of a json array.

Doesn't matter, the way you have to use the return type, it will always be a JSON object, which can't be directly assigned to the variables of another built-in types, such as integers, strings, and so on. Casting from a JSON object to a basic type can be done by accessing properties with the same name as the destination type, such as .string for casting to string objects, .int for casting to int objects, .array or an array of JSON objects, and so on.

Now, we have to think about how this information is going to be displayed. As we have to display this information repeatedly, a good way to do so would be with a table view. Therefore, we will create a custom table view cell for it.

Go to your project navigator, create a new group called Cells, and add a new file called CityInfoCell.swift. Here, we are going to implement a class that inherits from UITableViewCell. Note that the whole object can be configured just by setting the cityInfo property:

import UIKit

class CityInfoCell:UITableViewCell {
   @IBOutlet var nameLabel:UILabel!
   @IBOutlet var coordinates:UILabel!
   @IBOutlet var population:UILabel!
   @IBOutlet var infoImage:UIImageView!

   private var _cityInfo:CityInfo!
   var cityInfo:CityInfo {
       get {
           return _cityInfo
       }
       set (cityInfo){
           self._cityInfo = cityInfo
           self.nameLabel.text = cityInfo.name
           if let population = cityInfo.population {
               self.population.text = "Pop: (population)"
           }else {
               self.population.text = ""
           }

           self.coordinates.text = String(format: "%.02f, %.02f",
           cityInfo.lat, cityInfo.lng)

           if let _ = cityInfo.wikipedia {
               self.infoImage.image = UIImage(named: "info")
           }
       }
   }
}

Return to the storyboard and add a table view cell from the object library to the table view by dragging it. Click on this table view cell and add three labels and one image view to it. Try to organize it with something similar to the following picture:

creating-city-information-app-customized-table-views-img-13

Change the labels font family to American Typewriter, and the font size to 16 for the city name and 12 for the population and the location label..Drag the info.png and noinfo.png images to your Images.xcassets project. Go back to your storyboard and set the image to noinfo in the UIImageView attribute inspector, as shown in the following screenshot:

creating-city-information-app-customized-table-views-img-14

As you know, we have to set the auto layout constraints. Just remember that the constraints will take the table view cell as superview. So, here you have the constraints that need to be set:

  • City name label leading equals 0 to the leading margin (left)
  • City name label top equals 0 to the super view top margin
  • City name label bottom equals 0 to the super view bottom margin
  • City label horizontal space 8 to the population label
  • Population leading equals 0 to the superview center X
  • Population top equals to -8 to the superview top
  • Population trailing (right) equals 8 to the noinfo image
  • Population bottom equals 0 to the location top
  • Population leading equals 0 to the location leading
  • Location height equals to 21
  • Location trailing equals 8 to the image leading
  • Location bottom equals 0 to the image bottom
  • Image trailing equals 0 to the superview trailing margin
  • Image aspect ratio width equals 0 to the image height
  • Image bottom equals -8 to the superview bottom
  • Image top equals -8 to the superview top

Has everything been done for this table view cell? Of course not. We still need to set its class and connect each component. Select the table view cell and change its class to CityInfoCell:

creating-city-information-app-customized-table-views-img-15

As we are here, let's do a similar task that is to change the cell identifier to cityinfocell. This way, we can easily instantiate the cell from our code:

creating-city-information-app-customized-table-views-img-16

Now, you can connect the cell components with the ones we have in the CityInfoCell class and also connect the table view with the view controller:

@IBOutlet var tableView: UITableView!!

There are different ways to connect a view with the corresponding property. An easy way is to open the assistant view with the command + option + enter combination, leaving the storyboard on the left-hand side and the Swift file on the right-hand side. Then, you just need to drag the circle that will appear on the left-hand side of the @IBOutlet or the @IBAction attribute and connect with the corresponding visual object on the storyboard.

After this, we need to set the table view delegate and data source, and also the search bar delegate with the view controller. It means that the InitialViewController class needs to have the following header. Replace the current InitialViewController header with:

class InitialViewController: UIViewController,
CLLocationManagerDelegate, UITableViewDataSource,
UITableViewDelegate, UISearchBarDelegate {

Connect the table view and search bar delegate and the data source with the view controller by control dragging from the table view to the view controller's icon at the top of the screen, as shown in the following screenshot:

creating-city-information-app-customized-table-views-img-17

Summary

In this article, you learned how to create custom NSError, which is the traditional way of reporting that something went wrong. Every time a function returns NSError, you should try to solve the problem or report what has happened to the user.

We could also appreciate the new way of trapping errors with try and catch a few times. This is a new feature on Swift 2, but it doesn't mean that it will replace NSError. They will be used in different situations.

Resources for Article:


Further resources on this subject: