Welcome to part two in a series of articles on building your own iOS weather app in Swift!
The previous article in this series showed you how to:
- Get a key for OpenWeatherMap’s API for current weather data
- Make a manual API call using your browser
- Create a basic app to get the data from OpenWeatherMap
- Tweak the app
In this article, we’ll explain the code that was presented in the previous article and make the next major step in writing our weather app: extracting the data from the JSON returned by OpenWeatherMap.
A deeper look at the code that gets the weather data
In the last installment, I gave you enough code to connect to OpenWeatherMap and retrieve the weather data, but never explained how it works. Let’s fix that oversight.
Our simple weather app makes use of the NSURLSession
class along with some classes that go along with it. Together, classes in the NSURLSession
family of classes give us an API for downloading and uploading data from the internet, in a number of different ways.
The diagram below shows the classes that our weather app works with, either directly or indirectly:
Here’s a slightly more in-depth explanation of the classes in the diagram above:
NSURLSession class |
Instances of NSURLSession are containers — collections of other objects — that provide an API for sending data to and receiving data from a given URL. You use the NSURLSession API to create one or more sessions, which are objects that coordinate one or more tasks, which are objects that actually transfer the data. |
Shared session object | The shared session object is a pre-made singleton instance of NSURLSession . It’s not as configurable as other NSURLSession instances that you can instantiate yourself, but for simple requests, such as the kind we’re making in our bare-bones weather app, it’s good enough. You access the shared session with NSURLSession ‘s sharedSession class method. |
NSURLSessionTask class |
As mentioned in the explanation of NSURLSession above, tasks are the objects within an NSURLSession instance that actually transfer the data. NSURLSessionTask is the base class for tasks. |
NSURLSessionDataTask class |
You typically don’t instantiate an In addition to data tasks, there are also upload tasks, which are used to upload data to a server, and download tasks, which are used for downloading data from a server into a file (as opposed to memory, which is where data tasks download their data to). |
That’s the “big picture” view. Keep it in mind as we look at the code that does the downloading, contained in the getWeatherInfo
function in the WeatherGetter
class:
import Foundation class WeatherGetter { private let openWeatherMapBaseURL = "http://api.openweathermap.org/data/2.5/weather" private let openWeatherMapAPIKey = "06db44f389d2172e9b1096cdce7b051c" func getWeatherInfo(city: String) { // *** 1 *** let session = NSURLSession.sharedSession() // *** 2 *** let weatherRequestURL = NSURL(string: "\(openWeatherMapBaseURL)?APPID=\(openWeatherMapAPIKey)&q=\(city)")! // *** 3 *** let dataTask = session.dataTaskWithURL(weatherRequestURL) { (data: NSData?, response: NSURLResponse?, error: NSError?) in // *** 4 *** if let error = error { // Case 1: Error // We got some kind of error while trying to get data from the server. print("Error:\n\(error)") } // *** 5 *** else { // Case 2: Success // We got a response from the server! print("Raw data:\n\(data!)\n") let dataString = String(data: data!, encoding: NSUTF8StringEncoding) print("Human-readable data:\n\(dataString!)") } } // *** 6 *** dataTask.resume() } }
I’ve added number comments to the code, which correspond to the explanations below:
- We use
NSURLSession
‘s class methodsharedSession
to get a reference to the shared session object, and assign that reference to the local variablesession
. - We construct an URL using the URL for OpenWeatherMap’s current weather API, filling in the blanks with the name of the city that we want the weather for, and our OpenWeatherMap API key.
- We want to use a data task to request and retrieve the data from OpenWeatherMap. As mentioned earlier, tasks are created by using one of
NSURLSession
‘s task-creation methods. In this case, we’re usingdataTaskWithURL
, which we provide with 2 arguments:- The URL created in step 2, and
- A completion handler that executes once the task is done requesting and retrieving the data. The completion handler will receive three arguments, which we can work with:
data
: If the request was successfully made and the data was successfully received, this will contain that received data.response
: The response from the server.error
: If the request was not made successfully or the data was not received successfully, this will contain information about the error.
- If the error argument isn’t
nil
, we assign its value to a local variable also namederror
, and then display its contents. - If the error argument is
nil
, it means that no error occurred, and we display both the server response and the data received, in both raw and human-readable formats. - Up until this point, we’ve only defined the data task. This statement activates it.
Turning the weather data into a Swift dictionary
Right now, if the weather request was successfully made and the data was successfully received, we take that data, convert it into a human-readable string, and output it to the debug console…
// Case 2: Success // We got a response from the server! print("Raw data:\n\(data!)\n") let dataString = String(data: data!, encoding: NSUTF8StringEncoding) print("Human-readable data:\n\(dataString!)")
…and that human-readable string looks something like this:
{"coord":{"lon":-82.46,"lat":27.95},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"base":"cmc stations","main":{"temp":295.49,"pressure":1015,"humidity":64,"temp_min":294.15,"temp_max":296.15},"wind":{"speed":2.1,"deg":280},"clouds":{"all":1},"dt":1460163717,"sys":{"type":1,"id":728,"message":0.004,"country":"US","sunrise":1460200241,"sunset":1460245929},"id":4174757,"name":"Tampa","cod":200}
We could simply perform all sorts of string operations to extract the data we need, but why should we? The data is already in JSON format, which maps nicely to data structures in most programming languages, including Swift. There should be a simple way to take that incoming data and turn it into a nice Swift dictionary.
Enter the NSJSONSerialization
class, which can convert JSON into Foundation objects (such as arrays and dictionaries), and vice versa. It has a method called JSONObjectWithData
that takes two arguments:
data
: AnNSData
object containing JSON data, which we happen to have as one of the parameters of our completion handler, andoptions
: Options that specify how the JSON data should be read and how the corresponding Foundation objects should be created.
Let’s change the code so that we no longer turn the data into a string, but instead using NSJSONSerialization
‘s JSONObjectWithData
method to turn it into a dictionary:
// Case 2: Success // We got a response from the server! do { // Try to convert that data into a Swift dictionary let weather = try NSJSONSerialization.JSONObjectWithData( data!, options: .MutableContainers) as! [String: AnyObject] // If we made it to this point, we've successfully converted the // JSON-formatted weather data into a Swift dictionary. // Let's print its contents to the debug console. print("Date and time: \(weather["dt"]!)") print("City: \(weather["name"]!)") print("Longitude: \(weather["coord"]!["lon"]!!)") print("Latitude: \(weather["coord"]!["lat"]!!)") print("Weather ID: \(weather["weather"]![0]!["id"]!!)") print("Weather main: \(weather["weather"]![0]!["main"]!!)") print("Weather description: \(weather["weather"]![0]!["description"]!!)") print("Weather icon ID: \(weather["weather"]![0]!["icon"]!!)") print("Temperature: \(weather["main"]!["temp"]!!)") print("Humidity: \(weather["main"]!["humidity"]!!)") print("Pressure: \(weather["main"]!["pressure"]!!)") print("Cloud cover: \(weather["clouds"]!["all"]!!)") print("Wind direction: \(weather["wind"]!["deg"]!!) degrees") print("Wind speed: \(weather["wind"]!["speed"]!!)") print("Country: \(weather["sys"]!["country"]!!)") print("Sunrise: \(weather["sys"]!["sunrise"]!!)") print("Sunset: \(weather["sys"]!["sunset"]!!)") } catch let jsonError as NSError { // An error occurred while trying to convert the data into a Swift dictionary. print("JSON error description: \(jsonError.description)") }
If you run the app now, you’ll see that when put into dictionary form, it’s easy to extract weather data:
Date and time: 1462277829
City: Tampa
Longitude: -82.45999999999999
Latitude: 27.95
Weather ID: 800
Weather main: Clear
Weather description: clear sky
Weather icon ID: 02d
Temperature: 294.659
Humidity: 95
Pressure: 1025.47
Cloud cover: 8
Wind direction: 168.003 degrees
Wind speed: 2.11
Country: US
Sunrise: 1462272456
Sunset: 1462320359
Here’s what the complete WeatherGetter.swift should look like now:
import Foundation class WeatherGetter { private let openWeatherMapBaseURL = "http://api.openweathermap.org/data/2.5/weather" private let openWeatherMapAPIKey = "YOUR API KEY HERE" func getWeather(city: String) { // This is a pretty simple networking task, so the shared session will do. let session = NSURLSession.sharedSession() let weatherRequestURL = NSURL(string: "\(openWeatherMapBaseURL)?APPID=\(openWeatherMapAPIKey)&q=\(city)")! // The data task retrieves the data. let dataTask = session.dataTaskWithURL(weatherRequestURL) { (data: NSData?, response: NSURLResponse?, error: NSError?) in if let error = error { // Case 1: Error // We got some kind of error while trying to get data from the server. print("Error:\n\(error)") } else { // Case 2: Success // We got a response from the server! do { // Try to convert that data into a Swift dictionary let weather = try NSJSONSerialization.JSONObjectWithData( data!, options: .MutableContainers) as! [String: AnyObject] // If we made it to this point, we've successfully converted the // JSON-formatted weather data into a Swift dictionary. // Let's print its contents to the debug console. print("Date and time: \(weather["dt"]!)") print("City: \(weather["name"]!)") print("Longitude: \(weather["coord"]!["lon"]!!)") print("Latitude: \(weather["coord"]!["lat"]!!)") print("Weather ID: \(weather["weather"]![0]!["id"]!!)") print("Weather main: \(weather["weather"]![0]!["main"]!!)") print("Weather description: \(weather["weather"]![0]!["description"]!!)") print("Weather icon ID: \(weather["weather"]![0]!["icon"]!!)") print("Temperature: \(weather["main"]!["temp"]!!)") print("Humidity: \(weather["main"]!["humidity"]!!)") print("Pressure: \(weather["main"]!["pressure"]!!)") print("Cloud cover: \(weather["clouds"]!["all"]!!)") print("Wind direction: \(weather["wind"]!["deg"]!!) degrees") print("Wind speed: \(weather["wind"]!["speed"]!!)") print("Country: \(weather["sys"]!["country"]!!)") print("Sunrise: \(weather["sys"]!["sunrise"]!!)") print("Sunset: \(weather["sys"]!["sunset"]!!)") } catch let jsonError as NSError { // An error occurred while trying to convert the data into a Swift dictionary. print("JSON error description: \(jsonError.description)") } } } // The data task is set up...launch it! dataTask.resume() } }
At this point, the WeatherGetter
class doesn’t just get the weather data from OpenWeatherMap; it also puts that data into a form that we can process: a Dictionary
. We’re still displaying the information in the debug console — we’re still not showing it to anyone who’s not running the app with Xcode.
In the next installment in this series, we’ll take the weather data, now in dictionary form, and make it visible to the user. We’ll also make it possible for the user to enter a city to get the weather for, rather than hard-wire it into the app.
You can download the project files for this aricle (41KB zipped) here.
12 replies on “How to build an iOS weather app in Swift, part 2: A little more explanation, and turning OpenWeatherMap’s JSON into a Swift dictionary”
I am not getting any api data back in playgrounds.
Adrienne: Let me look into what’s not working for you. Could you please send me your playground? Email it to me at joey@joeydevilla.com.
[…] the first and second articles in this series, we spent just about all our time working on the WeatherGetter […]
[…] case you missed the earlier installments in this series, here are part one, part two, and part […]
[…] In the second article, we went a little deeper into NSURLSession and the related classes that made iOS networking possible. We also took the weather data that OpenWeatherMap returned and converted it from a string containing JSON-formatted weather data into a more easily processed Swift dictionary. […]
ON this line of code I’m getting this error.
`print(“Weather ID: \(weather[“weather”]![0]![“id”]!!)”)`
Type ‘Any’ has no subscript members.
Hi Joey,
I am following your tutorial but it’s in Swift 3. I am using a new http request as it on Stackoverflow http://stackoverflow.com/questions/38292793/http-requests-in-swift-3. I managed to retrieved data back but it doesn’t display in UI. I tried build all views programmically but it still not see anything. I did use my API key with a sample project and it isn’t working either. Can you please help me? I can send my project to you.
Best,
Thanks for the nice tutorial! Struggled with this, but here is working code for Swift 3:
//
// WeatherGetter.swift
// WeatherApp
//
// Created by Willy Bergsnov on 15.02.2017.
// Copyright © 2017 Mother Ocean. All rights reserved.
//
import Foundation
class WeatherGetter {
private let openWeatherMapBaseURL = “http://api.openweathermap.org/data/2.5/weather”
private let openWeatherMapAPIKey = “fce9415e81588e8c8498361ef487dbb8”
func getWeather(city: String) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let weatherRequestURL = URL(string: “\(openWeatherMapBaseURL)?APPID=\(openWeatherMapAPIKey)&q=\(city)”)!
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL, completionHandler: {
(data, response, error) in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print(“Error:\n\(error)”)
}
else {
// Case 2: Success
// We got a response from the server!
do {
// Try to convert that data into a Swift dictionary
let weather = try JSONSerialization.jsonObject(
with: data!,
options: .mutableContainers) as! Dictionary
// If we made it to this point, we’ve successfully converted the
// JSON-formatted weather data into a Swift dictionary.
// Let’s print its contents to the debug console.
print(“Date and time: \(weather[“dt”]!)”)
print(“City: \(weather[“name”]!)”)
print(“Longitude: \(weather[“coord”]![“lon”]!!)”)
print(“Latitude: \(weather[“coord”]![“lat”]!!)”)
print(“Weather ID: \((weather[“weather”]![0]! as! [String:AnyObject])[“id”]!)”)
print(“Weather main: \((weather[“weather”]![0]! as! [String:AnyObject])[“main”]!)”)
print(“Weather description: \((weather[“weather”]![0]! as! [String:AnyObject])[“description”]!)”)
print(“Weather icon ID: \((weather[“weather”]![0]! as! [String:AnyObject])[“icon”]!)”)
print(“Temperature: \(weather[“main”]![“temp”]!!)”)
print(“Humidity: \(weather[“main”]![“humidity”]!!)”)
print(“Pressure: \(weather[“main”]![“pressure”]!!)”)
print(“Cloud cover: \(weather[“clouds”]![“all”]!!)”)
print(“Wind direction: \(weather[“wind”]![“deg”]!!) degrees”)
print(“Wind speed: \(weather[“wind”]![“speed”]!!)”)
print(“Country: \(weather[“sys”]![“country”]!!)”)
print(“Sunrise: \(weather[“sys”]![“sunrise”]!!)”)
print(“Sunset: \(weather[“sys”]![“sunset”]!!)”)
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print(“JSON error description: \(jsonError.description)”)
}
}
})
// The data task is set up…launch it!
dataTask.resume()
}
}
[…] Part 2: A little more explanation, and turning OpenWeatherMap’s JSON into a Swift dictionary […]
Thank you Joey, these posts have been very educational!
Thank you Wily, your Swift version 3 code was very helpful in figuring out how to access the “weather” items.
Hey Joey! I can’t thank you enough. Your tutorial saved me hours of figuring out. The article is neat and well written.
Wish you all the very best for your future endeavors :)
Even the revised code is throwing an error now:
Generic parameter ‘Key’ could not be inferred in cast to ‘Dictionary’
:)