If you start a new App project in Swift, you will likely sooner or later have to ask yourself: Which networking library should I use? Back in Objective-C, the common choice was AFNetworking. However, I bet you’d like a more ‘Swift’ option for your new project.
So you check out AFNetworking’s Swift successor: Alamofire. Or any of a whole stack of new Swift networking libraries. And it is indeed quite ‘Swift-y’ and nice, however since it is such a new library, it is inevitably missing a few features that you need, and it’ll take a while for you to customise it to talk to your backend, which (I’m willing to bet!) has a couple of non-standard quirks. Eg it always needs a special HTTP header, or it always responds with JSON however it gets the content-type header wrong.
I suggest you simply use NSURLSession directly, with your own custom wrapper! In this post, I’ll show you best-practices to do so. It’ll probably take no longer than it would take to learn how to use Alamofire, and you’ll be able to tweak it to your hearts content, to work around whatever quirks your backend team throws your way (we’ve all been there!).
Before you get bored and give up on reading this post: if you’d like to see all this together, I’ve uploaded a sample project here: github.com/chrishulbert/Wattle.
It’s deliberately not a cocoapod - I expect you to either read it as inspiration to write your own, or copy it in as a starting point and customise it to your needs. It’s named Wattle in homage to Alamofire being named after a state flower - the wattle is the national flower of Australia.
The first step in using NSURLSession is to create a configuration object. NSURLSessionConfiguration has three standard ones: Default, Ephemeral, and Background. These are class functions on the NSURLSessionConfiguration class.
Since it is always good practice to imitate Apple’s frameworks in order to make things idiomatic, we’ll add another class function for our own configuration. Simply create an extension on NSURLSessionConfiguration which creates a new configuration object based on Default, customise it a bit, and return it:
extension NSURLSessionConfiguration {
/// Just like defaultSessionConfiguration, returns a
/// newly created session configuration object, customised
/// from the default to your requirements.
class func mySessionConfiguration() -> NSURLSessionConfiguration {
let config = defaultSessionConfiguration()
// Eg we think 60s is too long a timeout time.
config.timeoutIntervalForRequest = 20
// Some headers that are common to all reqeuests.
// Eg my backend needs to be explicitly asked for JSON.
config.HTTPAdditionalHeaders = ["MyResponseType": "JSON"]
// Eg we want to use pipelining.
config.HTTPShouldUsePipelining = true
return config
}
}
The next thing you’ll need is a session delegate that implements NSURLSessionDelegate. I recommend calling it ‘XYZURLSessionDelegate’ where XYZ are your company initials. It won’t need to be a singleton, as its instance is retained by the NSURLSession. It needs to inherit NSObject, although this may not be necessary in later Swift versions.
Here’s an example of a delegate that allows self-signed certs for your dev/test servers, which I’ve almost always needed:
class MyURLSessionDelegate: NSObject, NSURLSessionDelegate {
func URLSession(session: NSURLSession,
didReceiveChallenge challenge: NSURLAuthenticationChallenge,
completionHandler: (NSURLSessionAuthChallengeDisposition,
NSURLCredential!) -> Void) {
// For example, you may want to override this to accept
// some self-signed certs here.
if challenge.protectionSpace.authenticationMethod ==
NSURLAuthenticationMethodServerTrust &&
Constants.selfSignedHosts.contains(
challenge.protectionSpace.host) {
// Allow the self-signed cert.
let credential = NSURLCredential(forTrust:
challenge.protectionSpace.serverTrust)
completionHandler(.UseCredential, credential)
} else {
// You *have* to call completionHandler, so call
// it to do the default action.
completionHandler(.PerformDefaultHandling, nil)
}
}
struct Constants {
// A list of hosts you allow self-signed certificates on.
// You'd likely have your dev/test servers here.
// Please don't put your production server here!
static let selfSignedHosts: Set<String> =
["dev.example.com", "test.example.com"]
}
}
NSURLSession already has a singleton session that you can use: sharedSession
. Following Apple’s example, let’s make an extension on NSURLSession for our own session singleton. Swift won’t let us use the ‘static let’ trick in an extension (maybe future versions will) so we need to use the nested struct trick. Finally, since the delegate is retained by the session according to Apple’s docs, we can simply pass in a newly instantiated MyURLSessionDelegate:
extension NSURLSession {
/// Just like sharedSession, returns a shared singleton
/// session object.
class var mySharedSession: NSURLSession {
// The session is stored in a nested struct because
// you can't do a 'static let' singleton in a
// class extension.
struct Instance {
// The singleton URL session, configured
// to use our custom config and delegate.
static let session = NSURLSession(
configuration: NSURLSessionConfiguration.
mySessionConfiguration(),
// Delegate is retained by the session.
delegate: MyURLSessionDelegate(),
delegateQueue: NSOperationQueue.mainQueue())
}
return Instance.session
}
}
Next you’ll need something to help you construct NSURLRequests. NSURLRequest already has two static helpers for this: requestWithURL
and requestWithURL:cachePolicy:timeoutInterval:
. But they don’t hit the spot for what you’ll probably need, so let’s add another. Again, following Apple’s example, I recommend adding it as a NSURLRequest extension. The following supports querystring and JSON-encoded body parameters, and per-request headers. You may want to modify it if, for example, your backend requires form or XML encoding:
extension NSURLRequest {
/// Helper for making a URL request. This is to be used internally
/// by the string extension, not by the rest of your app.
/// It JSON encodes parameters if any are provided.
/// Adds any headers specific to only this request too if
/// provided. Any headers you use all the time should be in
/// NSURLSessionConfiguration.wattleSessionConfiguration.
/// You may want to extend this if your requests need any
/// further customising, eg timeouts etc.
class func requestWithURL(
URL: NSURL,
method: String,
queryParameters: [String: String]?,
bodyParameters: NSDictionary?,
headers: [String: String]?) -> NSURLRequest {
// If there's a querystring, append it to the URL.
let actualURL: NSURL
if let queryParameters = queryParameters {
let components = NSURLComponents(URL: URL,
resolvingAgainstBaseURL: true)!
components.queryItems = map(queryParameters) {
(key, value) in
NSURLQueryItem(name: key, value: value)
}
actualURL = components.URL!
} else {
actualURL = URL
}
// Make the request for the given method.
let request = NSMutableURLRequest(URL: actualURL)
request.HTTPMethod = method
// Add any body JSON params (for POSTs).
if let bodyParameters = bodyParameters {
request.setValue("application/json",
forHTTPHeaderField: "Content-Type")
request.HTTPBody =
NSJSONSerialization.dataWithJSONObject(
bodyParameters,
options: nil, error: nil)
}
// Add any extra headers if given.
if let headers = headers {
for (field, value) in headers {
request.addValue(value,
forHTTPHeaderField: field)
}
}
return request
}
}
Note that the helper above won’t be directly used by other parts of your app, but is used by further helpers below.
I recommend using a Swift struct to manage your responses, as you can now add helper methods which come in extremely handy. It means you can add extra fields and helpers without needing any changes to your calling code, which is a big time saver in my experience.
/// This wraps up all the response from a URL request together.
struct WTLResponse {
// Actual fields.
let data: NSData!
let response: NSURLResponse!
var error: NSError?
// Helpers.
var HTTPResponse: NSHTTPURLResponse! {
return response as? NSHTTPURLResponse
}
var responseJSON: AnyObject? {
if let data = data {
return NSJSONSerialization.JSONObjectWithData(
data, options: nil, error: nil)
} else {
return nil
}
}
var responseString: String? {
if let data = data,
string = NSString(data: data, encoding: NSUTF8StringEncoding) {
return String(string)
} else {
return nil
}
}
}
And finally, here is the String extension that ties it all together and is used as the main entry point for the rest of your app. I’ll concede that you may find it a bit too ‘cute’ to use a string extension for this, and so you may wish to use static methods on a class named ‘MyNetworking’ or such. Up to you. Also, you may want to add PUT and DELETE if your backend needs them. But here’s what I think is nice:
extension String {
typealias NetworkingCompletion = WTLResponse -> Void
/// Simply does an HTTP GET/POST/PUT/DELETE using the receiver as the
/// endpoint eg 'users'. This endpoint is appended to the baseURL which
/// is specified in Constants below. These should be your main entry
/// point into Wattle from the rest of your app.
func get(parameters: [String: String]? = nil,
completion: NetworkingCompletion) {
requestWithMethod("GET",
queryParameters: parameters,
completion: completion)
}
/// Note that post's parameters are different, as they go in the body
/// instead of the querystring.
func post(parameters: NSDictionary? = nil,
completion: NetworkingCompletion) {
requestWithMethod("POST",
bodyParameters: parameters,
completion: completion)
}
/// Used to contain the common code for GET and POST and DELETE and PUT.
private func requestWithMethod(method: String,
queryParameters: [String: String]? = nil,
bodyParameters: NSDictionary? = nil,
completion: NetworkingCompletion) {
// Create the request, with the JSON payload or querystring if necessary.
let request = NSURLRequest.requestWithURL(
NSURL(string: self, relativeToURL: Constants.baseURL)!,
method: method,
queryParameters: queryParameters,
bodyParameters: bodyParameters,
headers: nil)
let task = NSURLSession.sharedWattleSession.dataTaskWithRequest(request) {
data, response, sessionError in
// Check for a non-200 response, as NSURLSession doesn't consider
// that as an error.
var error = sessionError
if let httpResponse = response as? NSHTTPURLResponse {
if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
let description = "HTTP response: \(httpResponse.statusCode)"
error = NSError(domain: "Custom",
code: 0,
userInfo: [NSLocalizedDescriptionKey: description])
}
}
// Wrap up the response.
let wrappedResponse = WTLResponse(
data: data,
response: response,
error: error)
completion(wrappedResponse)
}
task.resume()
}
// MARK: - Constants
struct Constants {
/// This is the base URL for your requests. You'll of course want
/// to make this point at your servers.
static let baseURL = NSURL(string: "https://api.github.com/")!
}
}
So, now it’s all in place, how do you use this from the rest of your app? Quite simply:
// Simplest possible example.
"emojis".get { response in
println(response.responseJSON)
}
// Some parsing (I recommend you should put parsing as
// an extension on your model classes).
"users".get { response in
if let users = response.responseJSON as? [NSDictionary] {
let names = users.map { $0["login"]! }
println(names)
} else {
println("Error: \(response.error)")
}
}
// A querystring param.
"meta".get(parameters: ["since": "2015-01-02"]) { response in
println(response.responseJSON)
}
Hope someone finds this helpful :)
Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.
(Comp Sci, Hons - UTS)
Software Developer (Freelancer / Contractor) in Australia.
I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!
Get in touch:
[email protected]
github.com/chrishulbert
linkedin