© Radoslava Leseva Adams and Hristo Lesev 2016

Radoslava Leseva Adams and Hristo Lesev, Migrating to Swift from Flash and ActionScript, 10.1007/978-1-4842-1666-8_14

14. Networking

Radoslava Leseva Adams and Hristo Lesev2

(1)London, UK

(2)Kazanlak, Bulgaria

Today most mobile applications are part of an ecosystem of different channels for presenting information to the user. Whether you are reading your e-mail or checking the weather forecast in a web browser, in a desktop client, or in a mobile app all the information and devices are connected via the invisible meta-space called Internet. In this chapter we will focus on how to connect to and retrieve information from a web service and load it into your app.

In this chapter you will do the following:

  • Learn how to request and retrieve data from a web service.

  • Learn how to create a network session and initiate a download task.

  • Learn how to work with JSON (Java Script Object Notation) and how to convert it to Swift objects.

  • Learn how to download an image from a URL (uniform resource locator) and how to display the image in a table view.

  • Build an app that does all of the above.

When you are done, you will have an app that gets a weather forecast for a chosen place around the world. The forecast will be retrieved from an open online weather service using the network communication capabilities of iOS.

Setting Up the App and Designing the UI

The app we are about to develop will let the user read the weather forecast for any known city around the world. The app will present the information as a five-day forecast, and the data for each day will be sampled every three hours. It will include the expected temperature and an icon to depict the weather conditions.

Start by creating a Single View iOS Application project (FileNewProject…, then iOSApplicationSingle View Application) and naming it WeatherApp. We will design the user interface (UI) of the application as part of the setup, so we can focus on code in the rest of the chapter.

Figure 14-1 shows you what the UI will look like: on the left there is a mock-up with the UI elements you will need to place in the view on the storyboard and on the right is a screenshot of the storyboard with the elements already positioned on it.

A371202_1_En_14_Fig1_HTML.jpg
Figure 14-1. The app’s user interface

As you can see in Figure 14-1, at the top of the screen there is a text field, which will be used to input the name of the city, for which we want to see a forecast, and a button to start a data search for it. The forecast results will be displayed in a table view, where each row will contain a piece of forecast information: the expected temperature, the day and the time, and an image that represents the weather conditions.

Open Main.storyboard and arrange the UI as shown in Figure 14-1 by adding a text field, a button, and a table view. Do not forget to add constraints, so that the app adapts its look to whatever device it runs on. If you need a reminder for how to set constraints and make adaptive UI, have a look at Chapter 5 .

Next, open ViewController.swift and create two outlets, one for the city text field and another for the table view. Then create an action for the Search button’s Touch Up Inside event. (See Chapter 2 for how to create outlets and actions.) Your code should look like that in Listing 14-1.

Listing 14-1. Adding an Outlet and an Action to the ViewController Class
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var cityTextField: UITextField!
    @IBOutlet weak var tableView: UITableView!


    //The rest of the code goes here

    @IBAction func searchForCity(sender: AnyObject) {
    }


}

Next we will set up the app to connect to a web service. With iOS 9, Apple introduced a feature called App Transport Security (ATS). It forces apps to use the more secure HTTPS protocol by blocking less secure connections that use HTTP. This, on the one hand, is great, as it improves the security and privacy for your users’ data. If you have control at the server end, you can help your app support ATS by setting up the server to use Secure Sockets Layer (SSL). This enables it to send and receive encrypted information. On the other hand, if your app needs to connect to a third-party service, which may or may not support SSL, you may want to bypass ATS.

To do this, open project’s Info.plist file and add new key with name App Transport Security Settings, then add a subkey named Allow Arbitrary Loads and set its value to YES (Figure 14-2). This instructs the app to allow any network connections, including insecure ones.

A371202_1_En_14_Fig2_HTML.jpg
Figure 14-2. Disable App Transport Security

Note that ATS also allows you to configure behavior for individual domains and only allow non-SSL communications for certain ones (see https://goo.gl/neuUz9 for more details).

With the app set up, let us have a look at the weather web service, which we will query for data.

The Weather Forecast Web Service

The app will get its data from a public weather web service . There are a few of these available on the Internet. For this tutorial we will use OpenWeatherMap. We chose it because it

  • has a free account option.

  • offers plain and simple URL structure.

  • returns data in JSON format.

  • has an easy authorization policy: it uses only an application programming interface (API) key at the end of each request.

Before we can use OpenWeatherMap we have to create an account. Every account is given a unique application programming interface (API) key, which is used to authorize any apps you create that send queries to the server.

To create your free account open your favorite web browser and navigate to this URL: https://home.openweathermap.org/users/sign_up . The web site will ask you to enter a username, an e-mail address, and a password (Figure 14-3).

A371202_1_En_14_Fig3_HTML.jpg
Figure 14-3. OpenWeatherMap’s account registration page

Once your account has been created, you will be redirected to the account settings page. Here you can copy your API key (Figure 14-4). We will use the key later for authorizing the app’s requests to the web service.

A371202_1_En_14_Fig4_HTML.jpg
Figure 14-4. Get an OpenWeatherMap API key

OpenWeatherMap offers an API that you can explore if you navigate to http://openweathermap.org/api in a web browser. The web site has a detailed manual, which explains how to connect to the service and retrieve weather information. In this tutorial we will focus on how to get the 5-day/3-hour forecast (i.e., a forecast for five days with data every three hours (see http://openweathermap.org/forecast5 for more details).

According to the manual, to get a forecast for a given city using only its name, we need to send a request that has the following format:

api.openweathermap.org/data/2.5/forecast?q={city_name}&units=metric&APPID={api_key}

Let us dissect the request:

  • api.openweathermap.org/data/2.5/forecast is the entry point URL for the 5-day/3-hour forecast service.

  • The q parameter instructs the weather service that we want to search for a city by its name. The value of the parameter is the name of the city.

  • The units parameter specifies the units for the data. If we omit this parameter, we will get temperature data in Kelvins. To use Celsius, set units to metric.

  • The APPID parameter expects the unique API key from your account. This parameter needs to be appended to every request to OpenWeatherMap made by the app. If it is missing, the server will respond with an error string.

The response to this request will come in JSON format. JSON is a text format for describing objects with the help of key-value pair hierarchies. Listing 14-2 shows a typical response from the weather service in JSON format.

Listing 14-2. JSON Weather Forecast Response
{
   "city":{
      "id":1851632,
      "name":"Shuzenji",
      "coord":{
         "lon":138.933334,
         "lat":34.966671
      },
      "country":"JP",
      "cod":"200",
      "message":0.0045,
      "cnt":38,
      "list":[
         {
            "dt":1406106000,
            "main":{
               "temp":298.77,
               "temp_min":298.77,
               "temp_max":298.774,
               "pressure":1005.93,
               "sea_level":1018.18,
               "grnd_level":1005.93,
               "humidity":87,
               "temp_kf":0.26
            },
            "weather":[
               {
                  "id":804,
                  "main":"Clouds",
                  "description":"overcast clouds",
                  "icon":"04d"
               }
            ],
            "clouds":{
               "all":88
            },
            "wind":{
               "speed":5.71,
               "deg":229.501
            },
            "sys":{
               "pod":"d"
            },
            "dt_txt":"2014-07-23 09:00:00"
         }
      ]
   }
}

JSON distinguishes the following data types: number, string, Boolean, object, and array. In Listing 14-2 an example of an object is the value following the city key. The weather forecast data is stored in the list array. Every object in the list describes a weather forecast for a certain time of the day.

To get the forecast, we need to read the values of the following keys from the JSON response:

  • temp—the temperature of the air, measured in the units we specified in the request.

  • icon—a string identifier, which contains an icon name. This name can later be used to download a weather icon.

  • dt_txt—a timestamp in a string format.

In the next section we will create a data model class to contain the data, which we extract from the JSON server response.

Preparing the Forecast Data Model

Following the Model-View-Controller (MVC) paradigm , we will create a model to store and work with the forecast data. For a reminder about MVC, see Chapter 5 .

We will create two Swift classes to implement the data model. The first one will represent a single forecast item with all of its data properties. The second one will store a collection of forecast items and will provide helper functions to access those items. We will host both of the classes inside the same swift source file.

In Xcode add a new file to your project, name it ForecastModel.swift, and open it for editing.

Let us start with the first class, ForecastItem. You can see its definition in Listing 14-3. Each forecast item has several properties of type String: a temperature measurement, a day and time for the forecast, and an identifier for the weather image icon.

There is one more property, named uiImageCache of type UIImage. UIImage is a class from the UIKit framework, which stores image data and provides a number of helper functions for loading and displaying data from various formats, such as PNG or JPEG. We will use uiImageCache to store an icon for the forecast.

ForecastItem contains a helper method, which stores image data in uiImageCache: addImageCache. As we will see later, the image data we download comes in an NSData object. You can think of NSData as analogous to ActionScript’s ByteArray. addImageCache uses NSData to construct a UIImage instance and store it in uiImageCache.

Listing 14-3. ForecastItem Class Implementation
import UIKit

public class ForecastItem {
    //Forecast temperature stored as String
    public var temparature : String
    //Forecast date and time entry
    public var dayAndTime : String
    //Weather icon name
    public var imageID : String
    //Cached weather image
    public var uiImageCache : UIImage?


    init( temperature: String, dayAndTime : String, imageID : String) {
        self.temparature = temperature
        self.dayAndTime = dayAndTime
        self.imageID = imageID
        self.uiImageCache = nil
    }


    //Load image from data and cache it
    public func addImageCache(fromData data : NSData) {
        //Create UIImage from NSData
        uiImageCache = UIImage(data: data)
    }
}

Our app will show the weather forecast for five days and the data for each day will be sampled every three hours. This makes 8 forecast items per day or 40 items for the whole five-day period. The next class we are going to implement, ForecastModel, will be responsible for storing these items and for providing access to them to the other parts of the application. The class has four methods:

  • getItem: atIndex, which returns a forecast data item from the list by its index.

  • removeAll(), which removes all previously stored forecast items from the model.

  • appendItem: newItem, which is used to populate the forecast list with new items.

  • getItemCount(), which returns the number of the forecast items in the list.

You can see the implementation of ForecastModel in Listing 14-4.

Listing 14-4. The ForecastModel Class
public class ForecastModel {

    //An array of forecast items
    private var forecastData : [ForecastItem]


    init() {
        forecastData = []
    }


    //Remove all forecast items
    func removeAll() -> Void {
        forecastData.removeAll()
    }


    //Get a forecast item at a given index in the forecastData array
    func getItem( atIndex index : Int ) -> ForecastItem {
        return forecastData[index]
    }


    //Add a new forecast item to the list
    func appendItem( newItem newItem : ForecastItem ) {
        forecastData.append(newItem)
    }


    //Get the number of forecast items
    func getItemCount() -> Int {
        return forecastData.count
    }
}

Writing the Network Communication Logic

We will create another class that will be responsible for connecting to the remote server, getting data, and parsing it for the forecast model. This will help with keeping all network communication code in one place.

Add a new source code file to the project and name it WeatherForecastService.swift. Listing 14-5 shows the WeatherForecastService class definition with all the methods stubbed out. We will add functionality to them in a minute.

Listing 14-5. WeatherForecastService Class
public class WeatherForecastService {

    //Path to the API entry point
    private let apiBasePath : String =
        "http://api.openweathermap.org/data/2.5/"


    //Unique key to access the OpenWeatherMap service
    private let apiKey : String = "739c23f0ecc3b86bf0545471b*******"


    //An aray of data tasks
    var dataTasks = [NSURLSessionTask]()


    //Unique error domain of the app
    let errorDomain = "com.diadraw.WeatherApp.WeatherForecastService"


    //Enum of possible error conditions
    enum ErrorCode {
        case noForecastData(Int)
        case urlFormatError(Int)
        case serverError(Int, String)
        case jsonParsingError(Int, String)
    }


    //Get a forecast from OpenWeatherMap service
    //and populate the forecast model
    public func getForecast( forecastModel forecastModel : ForecastModel,
        forCity city: String, completionHandler: (NSError?) -> Void) {
      //The body of the method goes here
    }


    //Request data from the server and decode it into an NSDictionary
    private func requestToServer( apiEndpoint url : NSURL,
        completionHandler: (NSDictionary?, NSError?) -> Void ) {
        //The body of the method goes here
    }


    //Download a forecast PNG icon and cache it in the forecastItem
    public func downloadForecastImage( forecastItem forecastItem :
        ForecastItem , completionHandler: (NSError?) -> Void ) {
        //The body of the method goes here
    }


    //Transforms ErrorCode to NSError
    func getNSError( fromCode errorCode:ErrorCode ) -> NSError {
        //The body of the method goes here
    }


    deinit {
        //The body of the method goes here
    }
}

The class has two constants that are used to build a connection string to the weather service. apiBasePath holds the entry point of the API. The apiKey property, as its name implies, holds the unique API key that we must use to connect to OpenWeatherMap.

For communicating with the weather service we will create several asynchronous tasks to retrieve data from the server. We will keep track of them in an array called dataTasks. This gives us control over safely stopping tasks that may still be running when the app is closed.

Retrieving remote data is not entirely in our control, however. During this process a number of errors may occur. We will use an enumeration for the error states that can happen during data retrieval and decoding.

There are five methods in the WeatherForecastService class.

  • getForecast(:forCity:) initiates a request to the forecast API of the weather service and populates the forecast model with the data that comes back.

  • downloadForecastImage(:) is called when we need to download an icon to depict the weather conditions.

  • requestToServer(apiEndpoint:completionHandler) serves as a generic function that sends request to the web service API, waits for a response, and converts JSON into equivalent Swift objects.

  • getNSError:fromCode transforms an error code into an NSError instance that will be used by other parts of the application to inform the user that something went wrong inside the WeatherForecastService class.

  • deinit is the place where all open connections to the server and unfinished jobs will be closed.

Downloading Images from a URL

We will start the implementation with downloadForecastImage(forecastItem: completionHandler). The purpose of this method is to download one of OpenWeatherMap’s weather icons and to store the result into a forecast item. You can see the method implementation in Listing 14-6.

OpenWeatherMap offers a set of weather icons that can be used to visually illustrate forecast conditions: from clear sunny sky to heavy rain clouds. Weather images are stored in PNG format and can be accessed on the OpenWeatherMap server with the following URL scheme: http://openweathermap.org/img/w/{icon_file_name}.png , where weather_icon_file name comes from the imageID property of the ForecastItem class.

To download an image from the server we will use the NSURLSession class from the iOS SDK. It has methods, which can request and download data from URLs via various protocols like FTP, HTTP, HTTPS, and so on.

From a developer’s point of view NSURLSession opens a session to a remote server in which we can create series of so-called tasks. Each task is responsible for performing a request to the server and asynchronously downloading the response data.

There are two ways to use NSURLSession. The first way is to create an instance and configure the session properties manually. The second way is to use a reference to a shared session that comes configured with default settings. What is the difference between the two ways and which one should we use? Creating and configuring a session manually could be a tedious process, but it gives us control. We can set a specific timeout for the request or specify if we want the session to stay alive even when the app is not running. A shared session, on the other hand, comes with predefined parameters and without the ability for background downloads, but it is a perfect choice if you just want to grab data from a URL without the need to manage configurations.

In our app we will go with the default shared session. To obtain a reference to a shared session just call the static sharedSession() method of the NSURLSession class.

We will create a data task constant of type NSURLSessionDataTask named downloadImageTask by calling the dataTaskWithURL( url:completionHandler ) method of the session reference. The role of the data task will be to download data from a URL and to call a completion handler function when it is done.

There are three types of session tasks: data, upload, and download tasks. The main difference between them is where and how they store their end results. For example, a data task returns the server’s response as an NSData object stored in memory, while a download task could save the data in a disc file.

We will use a closure as the completion handler, which will be called when there is a response from the server. (For more details on how closures work in Swift see Chapter 22 .) Inside the closure we first check if there were any errors during the execution of the task. Then if there were no errors we pass the received data to forecastItem.addImageCache(:), which will convert the PNG information into a UIImage instance. Finally, we call completionHandler(:) (which is the second argument of the downloadForecastImage method) to signal to the other parts of the app that the download of the weather image has finished.

Now there is only one thing left to do: add the task to the task list and start it. To start a task, call its resume() method. The name of the method may sound a bit weird, but it does the job.

Listing 14-6. Implementation of downloadForecastImage Method
//Download a forecast PNG icon and cache it in the forecastItem
public func downloadForecastImage( forecastItem forecastItem : ForecastItem ,
    completionHandler: (NSError?) -> Void ) {
    //Get a URL to the weather image from the imageID string
    let url = NSURL(string:
        "http://openweathermap.org/img/w/(forecastItem.imageID).png")


    //Get a shared session instance and start a task
    //to download the image data from the url
    let downloadImageTask = NSURLSession.sharedSession().dataTaskWithURL(url!) {
        (data, response, error) in


        if error == nil {
            print("Download Finished")


            //Get a UIImage from the returned data and cache it
            forecastItem.addImageCache(fromData: data!)
        }


        //The task is complete, call the completion handler
        completionHandler(error)
    }


    // Add the task to the tasks list
    dataTasks.append(downloadImageTask)


    //Start the download task
    downloadImageTask.resume()
}

Requesting JSON from a Server

Next to implement is requestToServer( apiEndpoint:completionHandler ). This method is marked as private for the WeatherForecastService class and is called internally by the getForecast(:) method. Its purpose is to get JSON-encoded forecast data from the server and to convert it into an NSDictionary object, which will be used by getForecast to extract the forecast data.

In Listing 14-7 you can see the implementation of requestToServer. The function takes two parameters: an apiEndpoint, which is a URL referring to a specific API entry point, and a completionHandler to call when the request is complete. The result of the response and any error information are returned as parameters of the completion handler.

This function follows the logic that we used to implement downloadForecastImage: obtain a shared session, create a data task to download the response as NSData, and start the task. The interesting part is how we convert the JSON-formatted data to a dictionary object that we can use in Swift.

To convert JSON to a Swift object we use the NSJSONSerialization helper class from the iOS SDK. In fact, this class can also convert Swift objects to JSON-formatted strings. The function that performs the conversion is called JSONObjectWithData:data:options. As the first parameter to the function we pass the NSData object, returned from the server, which contains JSON data. The second parameter is of type NSJSONReadingOptions and allows us to configure how the internal JSON parser will process the input data. In this example we set its value to AllowFragments, which instructs the JSON parser to work even if the root-level data is not wrapped in an object or an array.

If there are any errors during the parsing JSONObjectWithData:data:options will throw an error, so we must wrap the call in a do-catch statement. You can find more information about the do-catch statements in Chapter 17 .

Inside the body of the function there are several points of interest.

  • First we must check the task completion handler’s error parameter.

  • Then we check if there is any data in the server’s response.

  • Next there is a check for whether the JSON object can be parsed and then we ensure that the result from the parser is of type NSDictionary.

  • The last check is a precaution. From OpenWeatherMap API’s documentation we know that the top-level item in the JSON that its server produces is expected to be an object, which corresponds to an NSDictionary object in Swift. If the result of the parsing is of some other type, then the JSON response probably has different format from what we expected.

In all those cases we call the getNSError:FromCode method to create an NSError instance, in which we describe the problem and call the requestToServer completion handler to notify the caller.

Listing 14-7. Implementation of requestToServer Method
//Request data from the server and decode it into an NSDictionary
private func requestToServer( apiEndpoint url : NSURL,
    completionHandler: (NSDictionary?, NSError?) -> Void ) {


    //Get a shared session instance and define a task
    let task = NSURLSession.sharedSession().dataTaskWithURL(url) {
        (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in


        guard error==nil else {
            //There was an error returned from the server
            completionHandler(nil, error)
            return
        }


        // Check for JSON serialization errors
        guard let unwrappedData = data else {
            completionHandler(nil, self.getNSError(
                        fromCode: ErrorCode.jsonParsingError(50,
                        "Unexpected data format.")))
            return
        }


        var jsonObject : AnyObject? = nil

        do {
            //Deserialize JSON data to a dictionary
            jsonObject = try
                NSJSONSerialization.JSONObjectWithData(unwrappedData,
                        options: .AllowFragments)


            //Check if the resulting object from the serialization
            //is a dictionary
            guard let deserializedDictionary = jsonObject as? NSDictionary
            else {


                //We expected dictionary, but some other
                //object was returned. Return error.
                completionHandler(nil, self.getNSError(
                                fromCode: ErrorCode.jsonParsingError(50,
                                "Unexpected format.")))


                return
            }


            completionHandler(deserializedDictionary, nil)
        }
        //There was an error with JSON deserialization
        catch {
            completionHandler(nil, self.getNSError(
                        fromCode: ErrorCode.jsonParsingError(50,
                        "There was an error with JSON deserialization.")))
        }
    }


    // Add the task to the tasks list
    dataTasks.append(task)


    //Start the task
    task.resume()
}

Populating the Data Model

We will complete the WeatherForecastService class with the implementation of the getForecast(:forCity:) method (Listing 14-8). The method is responsible for connecting to the server and populating the forecast model with the data that comes back.

Listing 14-8. Implementation of getForecast Method
//Get a forecast from OpenWeatherMap service
//and populate the forecast model
public func getForecast( forecastModel forecastModel : ForecastModel,
    forCity city: String, completionHandler: (NSError?) -> Void) {


    //Clean all previeously entered items from the model
    forecastModel.removeAll()


    //Set the forecast API entry point
    let urlString =
        "(apiBasePath)forecast?q=(city)&units=metric&APPID=(apiKey)"


    //Encrypt the URL string to use percent characters instead of blank spaces
    let encodedUrlString =
        urlString.stringByAddingPercentEncodingWithAllowedCharacters(
            NSCharacterSet.URLQueryAllowedCharacterSet())


    //Try to create a NSURL instance from the encoded URL string
    guard let url = NSURL( string:encodedUrlString! ) else {
        completionHandler( self.getNSError(
                fromCode: ErrorCode.urlFormatError(50) ) )
        return
    }


    //Connect to the web service and get data as dictionary
    requestToServer(apiEndpoint: url) {
        (json:NSDictionary?, error:NSError?) -> Void in


        guard error==nil else {
            //Signal that the operation has finished with an error
            completionHandler(error)
            return
        }


        //Check if there is a list of forecast data
        guard let forecastList = json!["list"] as? NSArray else {


            //Check if the server responded with a
            //valid JSON but with error code inside
            if let errorCode = json!["cod"] as? String {


                //Get the server's error message
                let errorMesage : String? = json!["message"] as? String


                //Construct en error message
                let serverError = self.getNSError(
                    fromCode: ErrorCode.serverError(Int(errorCode) ?? 50,
                    errorMesage ?? "unknown error") )


                //Signal with server error
                completionHandler(serverError)
                return
            }
            else {
                //Signal with error: there is no valid forecast data
                completionHandler( self.getNSError(
                                fromCode: ErrorCode.noForecastData(50) ))
                return
            }
        }


        for entry in forecastList
        {
            //Check if all needed forecast properties
            //are present in the dictionary
            guard let forecastTempDegrees =
                        entry["main"]!!["temp"] as? Double,
                rawDateTime = entry["dt_txt"] as? String,
                forecastIcon = entry["weather"]!![0]!["icon"] as? String


                else {
                    //Signal with error that there is no forecast data
                    completionHandler( self.getNSError(
                            fromCode: ErrorCode.noForecastData(50) ) )
                    return
            }


            //Hydrate the forecast model with data
            forecastModel.appendItem(
                newItem: ForecastItem(temperature: "(forecastTempDegrees)",
                    dayAndTime: "(rawDateTime)",
                    imageID: forecastIcon)
            )
        }
        //Signal that the operation has finished
        completionHandler(nil)
    }
}

To retrieve data from the server, this method calls requestToServer( apiEndpoint: completionHandler ), which requests a JSON-formatted forecast. Note that before constructing an NSURL instance for the desired endpoint we first encode the endpoint string to ensure it is in a valid URL format. We use the stringByAddingPercentEncodingWithAllowedCharacters method of the String class to convert any blank spaces and Unicode characters into a valid URL.

After the data is received and converted to NSDictionary it is getForecast’s responsibility to be aware of the forecast data format and its conversion to match the forecast model. In other words, it needs to check if all of the values we require are present and have corresponding keys in the dictionary.

We will use them to create a new instance of ForecastItem and add it to the forecast model. If there are any errors from a previous operation or not all the data can be found, we signal the caller with an error.

Note that there is a case when the server can return a valid JSON response that contains an error message. At the end of the function you will find a code block, which performs a check if the response contains an error message.

Creating Error Messages

We will complete the WeatherForecastService class with the implementation of the getNSError:FromCode method (Listing 14-9). Inside we use a switch to determine the type of error and to create a NSError instance with a corresponding error message that will mean something to the user.

Listing 14-9. getNSError Method Implementation
//Transforms ErrorCode to NSError
func getNSError( fromCode errorCode:ErrorCode ) -> NSError {
    switch errorCode {
    case .noForecastData(let code):
        return NSError(domain:errorDomain, code:code, userInfo:
                 [NSLocalizedDescriptionKey:"There is no forecast data."])
    case .urlFormatError(let code):
        return NSError(domain:errorDomain, code:code, userInfo:
                 [NSLocalizedDescriptionKey:"URL format error."])
    case .serverError(let code, let message):
        return NSError(domain:errorDomain, code:code, userInfo:
                 [NSLocalizedDescriptionKey:"Server error: (message)"])
    case .jsonParsingError(let code, let message):
        return NSError(domain:errorDomain, code:code, userInfo:
                 [NSLocalizedDescriptionKey:"JSON parsing error: (message)"])
    }
}

Showing the Forecast in a Table View

Finally, we are ready to show the forecast to the user. We will display every forecast item on a separate row of the table view we added in the beginning of the chapter. Open ViewController.swiftfile and get ready to code.

As we saw in Chapter 6 , the table view needs to get its data from an object, which conforms to the UITableViewDataSource protocol. We will modify the ViewController class to work as a data source and conform to the protocol by first adding the protocol name to ViewController’s inheritance list:

class ViewController: UIViewController, UITableViewDataSource

Then we will add two of the protocol’s methods: tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAtIndexPath). For now let us just stub them out as shown in Listing 14-10.

Listing 14-10. Implement UITableViewDataSource Protocol’s Methods
class ViewController: UIViewController, UITableViewDataSource {

    //The rest of the code goes here

    func tableView(tableView: UITableView,
        numberOfRowsInSection section: Int) -> Int {
        return 0
    }


    func tableView(tableView: UITableView,
        cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        return UITableViewCell()
    }


}

Let us override UIViewController’s viewDidLoad method and set up ViewController as the data source for the table view in it (Listing 14-11).

Listing 14-11. Set Up the Data Source for the Table View
class ViewController: UIViewController, UITableViewDataSource {
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }


    //The rest of the code goes here
}

Next, we will add two properties to ViewController to work with data: an instance of ForecastModel to keep the data and an instance of WeatherForecastService to fetch it from the server (Listing 14-12).

Listing 14-12. Instantiate the WeatherForecastService and ForecastModel Classes
class ViewController: UIViewController, UITableViewDataSource {
    //The rest of the code goes here


    let weatherService = WeatherForecastService()
    let forecastModel = ForecastModel()


    //The rest of the code goes here
}

Let us now add implementation to the two methods we stubbed out earlier, so that the forecast items can appear in the table view. First we will have tableView(_:numberOfRowsInSection:) work out and return the number of items in the forecast model, which corresponds to the number of rows we need in the table (Listing 14-13).

Listing 14-13. Work Out the Number of Rows in the Table
func tableView(tableView: UITableView,
            numberOfRowsInSection section: Int) -> Int {
    //Return forecast items number
    return self.forecastModel.getItemCount()
}

The tableView(_:cellForRowAtIndexPath:) method is called when the table view needs to populate a given cell with data and this is what we will implement it to do (Listing 14-14).

Listing 14-14. Populate a Cell in the Table with Data
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {


    //Create new table cell for the row
    let cell = UITableViewCell()


    //Get a forecast item corresponding to the row number
    let forecastItem = self.forecastModel.getItem( atIndex: indexPath.row )


    //Set the cell's text label to the forecast item’s
    //temperature, date and time
    cell.textLabel?.text =
            "(forecastItem.temparature)° C | (forecastItem.dayAndTime)"


    if forecastItem.uiImageCache == nil {
        weatherService.downloadForecastImage( forecastItem ) { (error) in


            dispatch_async(dispatch_get_main_queue(), {[weak self] in
                if let error = error {
                    //If there is an error message print it
                    print(error)
                }
                else {
                    //Assign the cached image to the cell's image view
                    cell.imageView?.image = forecastItem.uiImageCache


                    //Refresh data in the row
                    self!.tableView.reloadRowsAtIndexPaths([indexPath],
                                                    withRowAnimation: .None)
                }
            })
        }
    }
    else {
        //Assign the cached image to the cell's image view
        cell.imageView?.image = forecastItem.uiImageCache
    }


    return cell
}

In the code in Listing 14-14 we first get a forecast item from the model that matches the row of the cell that has been requested. Then we set the cell’s textLabel property to display the temperature and the date. The interesting part is how we load a weather image in the cell.

The UITableViewCell class comes with a property called imageView, which we use to display a weather image. If the uiImageCache property of the forecast item is not empty, we can assign its value directly to the cell.imageView.image property. Otherwise we have to download the image first and do the assignment once the download is complete. Thus we call the downloadForecastImage method with a closure completion handler, in which the cell’s image view can be updated.

Note the dispatch_async(dispatch_get_main_queue(),{() -> Void}) call in the completion handler. It ensures that the image view update happens on the main thread, as this is the only thread that is allowed to access the user interface in iOS, while our completion handler is most likely to be called on the thread where the download happened.

Note

To keep things simple and the focus on network operation, we have taken the approach of creating a new cell every time we provide cell data in Listing 14-14. Apple strongly discourages this practice. Instead, UITableView offers a memory-efficient mechanism for cell reusing. See Chapter 6 for details.

The last bit of code in the app will be the implementation of the Search button action that queries the server and populates the model with forecast items. Do you remember the @IBAction searchForCity stub we created earlier? This is where we will add the implementation (Listing 14-15).

To populate the model we just need to call weatherService.getForecast and hand a model reference to it. It takes a completion handler, which we will implement as a closure. In it we ask the table view to reload its data if there were no errors while fetching the forecast. Again, we make sure that any UI update happens on the main thread.

Listing 14-15. Implementation of the searchForCity Action
@IBAction func searchForCity(sender: AnyObject) {
    weatherService.getForecast( forecastModel:forecastModel,
            forCity: cityTextField.text!){ ( error : NSError? ) -> Void in


        if let error = error {
            //If there is an error message print it
            print(error)
        }
        else {
            dispatch_async(dispatch_get_main_queue() , {[weak self] in
                //Refresh data in the table view
                self!.tableView.reloadData();
                })
        }
    }
}
Note

The format of the code in Listing 14-15 may already look familiar from the iCloud tutorial we did in Chapter 13. In case you are not doing the tutorials in order, however (why should you?!), here is a quick note on some of the syntax that may look unfamiliar. When a response has come from the server and there is no error, we update the user interface to load the new data by calling self!.tableView.reloadData(). This call is made in a closure—a block of code that can be passed around. You can see the closure using the self keyword to reference the current instance of our ViewController class. Closures are independent objects (i.e., although this closure can access ViewController via self, it is not itself part of the ViewController class). In order for it not to retain the reference that will count toward the ViewController instance’s reference count, we use the weak keyword. For more details on weak and strong references see Chapter 21.

And this is it. Run the app and type the name of the place you live in to see if the weather service can find it. If it does, you will get the weather forecast for the next five days (Figure 14-5).

A371202_1_En_14_Fig5_HTML.jpg
Figure 14-5. Our weather forecast app running in a simulator

Let us see what the weather in London, UK, will be like. Hard to believe! There are some sunny days ahead.

Summary

In this chapter you saw how to create a network session to a remote web service, download images, and work with JSON-formatted data. You built a weather forecast application that retrieves data from the OpenWeatherMap service using the HTTP API the service provides.

In the next chapter we will explore the power of push notifications and Apple’s advertisement channel as a way to monetize your apps.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset