
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 seem that 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.
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/.
Note
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.
The API keys used in this book are fake, please replace them with the corresponding API key given to you by the service provider.
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 as shown in the following screenshot:

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.
Note
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 errorNSLocalizedFailureReasonErrorKey
: This explains what caused the errorNSLocalizedRecoverySuggestionErrorKey
: 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.
Note
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.