How I Usually Do: Networking - Swift 3

During the time I was an iOS developer I picked up some patterns for doing networking. There is a lot of decisions to make when implementing networking, but this general solution is my favorite. I'll go through an example API I implemented.

Networking Class

class Api {
    enum Error: Swift.Error {
        case noData
        case noParse
    }

    private let session: URLSession = URLSession(configuration: .default)
    var queue: DispatchQueue = .main
    
    let baseURL: URL = URL(string: "https://example.com")!
}

For each API we create a class - no point of making it a struct, it doesn't have state anyway.

We create a nested enum for errors. Some errors are very common, like the JSON couldn't be parsed. We create separate errors for data validation here, e.g. case requiredFieldFooMissing. If the specific error is not shown to the user, it is at least useful for debugging.

We have a queue field that can be changed from outside. This is the queue for all the callbacks.

I don't like using explicit unwrapping, but until there are not sensible URL literals or URL support in xcassets this is what you have to do for the baseURL

Simple Endpoint

func simpleRequest(param: String,
                   success: @escaping ((foo: String, bar: String?, baz: String?)) -> Void,
	               failure: @escaping (Swift.Error) -> Void) {

We use a tuple with named arguments for the success callback. This is so at the call site we can use $0.foo, $0.bar, $0.baz instead of $0, $1, $2 as it is more readable. We return Swift.Error so it can either be our Api.Error or just a networking or serialization error from Foundation.

var urlRequest = URLRequest(url: baseURL.appendingPathComponent("/simple")	
session.dataTask(with: urlRequest) { (data, _, error) in
    if let error = error {
        self.queue.async {
            failure(error)
        }
        return
    }

We forward the networking error to the failure.

All failure and success callbacks are returned on the queue. So we don't have to make the additional GCD boilerplate in view controllers.

guard let data = data else {
    self.queue.async {
        failure(Error.noData)
    }
    return
}

var json: Any?
do {
    json = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
    self.queue.async {
        failure(error)
    }
    return
}

We use standard JSONSerialization to parse the JSON. In Swift 4 we will be able to just use Codable here, but Swift 4 is still beta.

        guard let jsonDictionary = json as? [String: Any],
              let foo = jsonDictionary["foo"] as? String,
            else {
            self.queue.async {
                failure(Error.noParse)
            }
            return
        }

        let bar = jsonDictionary["bar"] as? String
        let baz = jsonDictionary["baz"] as? String

        self.queue.async {
            success((foo, bar, baz))
        }
    }.resume()
}

We perform a guard let to check if the top level JSON object is correct, and to extract all required fields. Optional fields are parsed afterwards so they don't trigger a failure when missing.

We use as? Type to perform type validation, as the API may have changed to a number or a more complex type from a string and problems can arise. This was a place where it was very easy to crash using Objective-C, but Swift makes it much safer.

If you want prettier logs or better error reporting, you can split the guard let and have different Error values returned.

Authorization

This networking assumes, that you have an endpoint, where you send your login and password and get a token in return. Then all the authorized requests use the token in the header.

private static let keychain = Keychain()

You want to store the token in the keychain for security reasons. I usually use the KeychainAccess library from CocoaPods.

func login(user: String,
           password: String
	       success: @escaping () -> Void,
	       failure: @escaping (Swift.Error) -> Void) {

We don't return the token to the view controller. The class stores it itself in the keychain and uses it for subsequent requests. The token is abstracted away.

    var urlRequest = URLRequest(url: baseURL.appendingPathComponent("/login")	
    session.dataTask(with: urlRequest) { (data, _, error) in

        [... same as above ...]
        	
        guard let jsonDictionary = json as? [AnyHashable: Any],
              let token = jsonDictionary["token"] as? String,
            else {
            self.queue.async {
                failure(Error.noParse)
            }
            return
        }
        
        keychain["token"] = token

        self.queue.async {
            success()
        }
    }.resume()
}

We store the token in the keychain on success and call the success() callback. After that the view controller can assume we're logged in.

Authorized Request

if let token = keychain["token"] {
    urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}

We just add this code for URL requests that need authorization.

Logout

For logout functionality we just clear the key in the keychain.

keychain["token"] = nil

Parsing complex types

Sometimes we have a more complex structure for the response, like a feed of posts. Parsing it all out in the method is messy. So we do it using structs and optional initializers.

{
    "posts": [
        { 
            "title": "String"
            "subtitle": "Optional String"
            "body": "String"
            "thumbnail": "Optional Image URL"
            "author": "String"
            "replies": [
                { "author": "String", body: "String" },
                ...
            ]
         },
         ...
    ]
}

Let's assume our complex endpoint has a list of posts, and all posts have a list of replies.

struct Post {
   let title: String
   let subtitle: String?
   let body: String
   let thumbnail: URL?
   let author: String
   let replies: [Reply]
}

struct Reply {
    let author: String
    let body: String
}

We create structs for the types we parse. If fields are optional, we use Optional and we don't use it otherwise. Post has an array of Reply. URLs are parsed out as URL values and validated during parsing, so our view controllers don't have to double check.

struct Reply {
    [...]
    
    init?(dictionary: [String: Any]) {
	    guard let author = dictionary["author"] as? String,
            let body = dictionary["body"] as? String
        else {
            return nil
        }
        
        self.author = author
        self.body = body
    }
}

We start with the innermost type first. We parse the required fields, if they are not present or don't have the correct type, we return nil using the fact that we create an optional initializer.

struct Post {
    [...]
    
    init?(dictionary: [String: Any]) {
	    guard let author = dictionary["author"] as? String,
            let body = dictionary["body" as? String
            let title = dictionary["title" as? String
        else {
            return nil
        }
        
        self.author = author
        self.body = body
        self.title = title

		  self.subtitle = dictionary["subtitle"] as? String            
		  
		  let thumbnailString = dictionary["thumbnail"] as? String
		  self.thumbnail = URL(string; thumbnailString)

We first parse the required fields in a guard let as in Reply. We parse the optional fields after, so they won't trigger a nil. We also make sure the thumbnail is well formed by using the optional URL(string:) initializer.

		  let rawReplies = dictionary["replies"] as? [[String: Any]]
		  self.replies = rawReplies?.flatMap { Reply(dictionary: $0 } ?? []
    }
}

For the array of nested Reply values we first make sure that the key has a correct value - array of dictionaries. Then we apply the Run(dictionary:) optional initializer to each of the dictionaries.

We use flatMap which filters out all the dictionaries that triggered the nil case, so we are left with only well formed replies.

In case the key was missing or was not well-typed, we just use [] as this is a good default value.

It's important to note, that if we do pagination by counting objects we already downloaded, we can drop some not well formed objects here and break our pagination mechanic. For this cases it is best to return a field with the original count with the reponse to the view controller.

Conclusion

This solution tries to use Swift safety features, which weren't available in Objective-C. It was very easy to assume a wrong type and get something else at runtime and crash the whole app. Swift with optionals makes it much safer to work with.

We use optional initializers for complex types to split the parsing logic from the networking class, so it is easier to test and nicely separated.

We abstract away the token management, GCD management and all the network internals, so view controllers just get async endpoints with simple return values.

This solution tries to be as simple as possible with safety and comfort in mind.