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.