Getting the data

So how, and from where, do we get the data we need from the web? There are, in fact, very many sources that will provide us with data for free, and not just weather data. Searching through Google open data just now yielded 728 million results, which should be enough to keep most curious developers amused for some time.

Welcome to openweathermap.org

From the large number of open weather data sources, we will select http://openweathermap.org, which provides a large quantity of data for free to registered users. Go to the URL above and perform the following steps:

  1. Create a new account, which is free, requiring only an email address.
  2. Copy the API Key that will be generated once you have created an account.

You're done! Now you have an API key that you will include when you request data from the server, confirming that you are entitled to access that data. The key will look something like this: 78c75588dfe58276a694af9c660edxxx

With less form-filling than it takes to create a Google account you now have access to extensive weather data, covering the entire planet. What an amazing thing software development is.

Using http://openweathermap.org/ city ID's in our data requests, we can specify the cities whose data we wish to display to the user. The code in this book uses the ID's of a few cities dear to my heart, but you can change those to any cities you wish by referring to the city ID's listed here:

http://openweathermap.org/help/city_list.txt

Introducing JSON

Now, there are a number of formats in which you can download data from http://openweathermap.org/, and we will be using JSON, which has become one of the most popular formats due to its ease of use and lightweight structure. JSON stands for JavaScript Object Notation, but you don't need any JavaScript knowledge to use it, it just turned out to be a really convenient format.

JSON data is text. You can read it. You can print it to the console. And it is really easy to use. It is also very easy to convert into Swift Dictionary objects using NSJSONSerialization methods that are available in WatchKit. And once you have done that, it is just a matter of parsing those dictionaries.

JSON data structure 101

JSON data contains arrays and dictionaries, much the same as you know from iOS, each of which contains further arrays and dictionaries, or strings, numbers and Boolean values, collectively referred to as objects.

One of the first things you need to do when you have obtained JSON data and converted it to a Dictionary, is print that dictionary using print(yourDictionary). You can then see, in the console, all the data that is available, and the structure in which it is stored. With a little familiarity with such data, you will soon be able to quickly inspect the dictionaries and arrays, in order to find the information you need.

Making the data more readable

With deeply nested arrays of dictionaries that contain dictionaries of arrays and yet more dictionaries, we run the danger of getting lost within the data. We will tidy up, and make safer, the code we will be writing, by creating a couple of typealias, which will also get Swift's type-checking magic working for us, ensuring that the data objects we pass around are really the objects we think they are.

Create a new Swift file, and call it SharedConstants.swift. Again, select both the phone and the WatchKit Extension targets. Now add the following code below the import statement:

typealias jsonDict  = [String: AnyObject]
typealias jsonArray = [AnyObject]

All we are doing here is adding two typealias declarations; jsonArray is an array of AnyObject, since we have no way of knowing in advance what exactly it will contain, and the jsonDict is a Dictionary of String keys and AnyObject values, which can also contain values of whatever type JSON throws at us.

Remember, although JSON has a limited number of types, it is still easy to make mistakes when handling JSON data—Swift considers a dictionary of [String: String] key-value pairs an entirely different creature to a dictionary of, say, [String: [String] key-value pairs. Using typealias gets type-checking working in our favor.

Introducing NSURLSession

Okay, so we have had a brief look at the data that will arrive onto the watch (which we will look at in more detail very soon), but how do we actually GET the data? How does the watch even connect to the Internet?

The answer to the latter question is, fortunately: very easily. Without us having to write a single line of code, the Apple Watch will happily decide whether it makes sense to use the iPhone's network connection, using what Apple calls Tetherless WiFi, or whether to connect directly to known networks to which the iPhone has connected in the past.

Be glad you don't have to write that code. Be very glad.

Apple's NSURLSession provides us with a huge amount of functionality (most of which we don't need here) around downloading and uploading data to and from the web, as well as caching that data.

Disabling App Transport Security

In iOS 9, Apple introduced App Transport Security (ATS), which helps and encourages developers to use secure Internet Protocols whenever possible. Unfortunately, at the time of writing, http://openweathermap.org/ does not offer secure https URL's, so we'll need to get around ATS, otherwise the web requests will be refused by the operating system.

To do this, perform the following steps:

  1. In the project navigator, locate the Info.plist file in the Weather Watch WatchKit Extension. Careful to select the right one (each target has one).
  2. Right-click on it and from the contextual menu that opens, select Open As | Source Code. This is a much easier way to enter code than the default Property List view.
  3. Add the following highlighted code under the first <dict> tag:
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>NSAppTransportSecurity</key>
        <dict>
            <key>NSAllowsArbitraryLoads</key>
            <true/>
        </dict>
        <key>CFBundleDevelopmentRegion</key>
        <string>en</string>

Although it is possible to disable ATS on a per-domain basis, our app only uses the one server, and for the sake of simplicity we simply allow all non-secure http traffic, by adding this entry to the watch app target's property list.

Using NSURLSession

So let's write some code that will download the data, using whatever connection the watchOS deems appropriate:

Create a new Swift file, call it SessionManager, and tick both the watch app and the phone app targets, then add the following code below the import statement:

let baseUrl = "http://api.openweathermap.org/data/2.5/group?units=metric" //1
let APIKey = "78c75588dfe58276a694af9c660edxxx" //2
let faveCity = 2643741 //3
let cities = [faveCity, 2193733, 1273294, 5128581, 2950159, 3435910] //4

These are the strings we will need to construct the URL of the data request. The comments in the code are as follows:

  1. This is the base URL with which all requests to http://openweathermap.org/ begin. We will add more information to this depending on which data we want to request.
  2. Put your API key (which you got after creating the http://openweathermap.org/ account) here, not forgetting the quote marks to turn it into a Swift String.
  3. Here we specify a favorite city—you can use this one (which is the City of London) or choose another from the list linked to above. This is the city whose data will be shown on the Glance screen.
  4. We create an array of city codes, using our faveCity code as the first.

Now we'll create the actual SessionManager class that will be available from elsewhere in the app. Add the following code to SessionManager.swift:

class WeatherSessionManager {

    static let sharedInstance = WeatherSessionManager() //5
    private init() {}

    var lastRequestDate: NSDate? //6
}

The comments in the code are as follows:

  1. As we have done previously, we provide a static sharedInstance property so that the same instance is used throughout the app, and prevent the creation of a separate instance by making the init method private.
  2. The lastRequestDate property will store the most recent date (and time) of a data request to the server. Any new request made will compare its own date with this one to establish whether or not to use the data already saved (or cached) by NSURLSession, or whether to fetch fresh data.

We'll now write the method that will create the full request URL string from the baseURL, APIKey, and cities strings. Add the following method to the SessionManager class:

    func urlForCities(cityCodes: [Int]) -> String {
        var urlStr = baseUrl + "&APPID=" + APIKey + "&id=" //7
        for cityCode in cityCodes { //8
            urlStr += "(cityCode),"
        }
        return urlStr //9
    }

You will likely have many methods like this one in a larger, more complex app, so it is worth making doubly sure you understand what is going on. The comments in the code are as follows:

  1. We concatenate the necessary strings using Swift String convenient + operator (if you're coming from Objective C, you will appreciate just how convenient it is). Note the request data is always preceded by "&", the name of the property (or field name) and a "=" character
  2. We enumerate through the array of city codes, adding each to the urlString variable, and adding a comma (the comma after the last entry in the array is simply ignored).
  3. Now that the full URL string is constructed, we return it to the calling function.

Now we get to the magic stuff. We will create and configure NSURLSession, and then call its session.dataTaskWithURL(url, completionHandler) method. This takes two arguments. The first is a straightforward NSURL, and the second is a function in the form of a closure.

Passing a function as an argument to another function may be new to you, and if it is, you're going to love it, I promise you. Being able to pass a function directly to another function means not having to maintain a direct relationship (such as a delegate) between two objects or not needing to bother with setting up notifications between them. Passing functions means, in effect:

doSomething(withSomeArg: ArgType, whenFinishedCallThisFunction: FunctionType)

dataTaskWithURL stipulates that the function it is to call, once it is finished, must have the type (NSData?, NSURLResponse?, NSError?) -> Void, and so we'll create a typealias for this first, which will make the code look less intimidating. Add the following declaration to SessionManager:

typealias DataTaskCompletionHandler =
    (NSData?, NSURLResponse?, NSError?) -> Void

Now we can write a fetchWeatherData method. This will in turn call session.dataTaskWithURL method from NSURL class, but where do we get the DataTaskCompletionHandler?

That will be passed by the calling method to fetchWeatherData as its single argument.
Add the following method to the SessionManager class:

    func fetchWeatherData(completionHandler: DataTaskCompletionHandler) {

}

If we had not created the type alias, this method signature would have looked like this:

    func fetchWeatherData(completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) {

}

This is a little harder to read, though it is a pattern you'll quickly get used to.

Now add the following code to the fetchWeatherData method:

    func fetchWeatherData(completionHandler: DataTaskCompletionHandler) {
        let sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration() //10
        if let date = lastRequestDate
            where NSDate().timeIntervalSinceDate(date)  < 10.0 {
            sessionConfig.requestCachePolicy = .ReturnCacheDataElseLoad
        } else {
            sessionConfig.requestCachePolicy = .UseProtocolCachePolicy
        }
        lastRequestDate = NSDate() //11
        let session = NSURLSession(configuration: sessionConfig) //12
        let apiCall = urlForCities(cities) //13
        if let url = NSURL(string: apiCall) {
            let task = session.dataTaskWithURL(url, completionHandler: completionHandler) //14
            task.resume() //15
        }
    }

The comments in the code are as follows:

  1. Before we create NSURLSession, we need to prepare NSURLSessionConfiguration, and set its RequestCachePolicy according to how long it has been since the last request. The ten second value we have used here is to make testing it more convenient, but it will need changing to something more realistic later!
  2. We then update the lastRequestDate property with the date of this request, using a call to NSDate(), which returns the current date.
  3. We can now initiate the NSURLSession with the sessionConfig that we just created.
  4. Finally, we use the urlForCities method to construct the API call, and pass that as the argument to the NSURL init method, which creates the NSURL object that we need to pass to dataTaskWithURL.
  5. We also pass on the DataTaskCompletionHandler function that was passed in the call to fetchWeatherData.
  6. Nothing happens until we call resume method from NSURL class (there is no start method).
..................Content has been hidden....................

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