This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We make use of Swift's generics and structs to build a simple network layer with great testability.

00:01 Let's talk about the networking layer of the Swift talk app. 00:06 We think it's an interesting example to look at because we designed it differently than in previous Objective-C projects. 00:16 Typically, we would have created some kind of a Webservice class with individual methods that perform calls to particular endpoints. These methods return the data that we get back from these endpoints via a callback. 00:32 For example, we could have a loadEpisodes method, which makes the network call, parses the result, instantiates some Episode objects, and returns an array with the episodes. 00:44 We could also have a similar loadMedia method, which goes through the same steps to load the media for a particular episode:

final class Webservice {
    func loadEpisodes(completion: ([Episode]?) -> ()) {
        // TODO
    }
    
    func loadMedia(episode: Episode, completion: (Media?) -> ()) {
        // TODO
    }
}

00:50 In Objective-C, the advantage of this pattern is that the result in the callback has the correct type. 00:58 For example, we would get back an array of episodes and not just something of type id simply because it's a method that loads just any data from the network. 01:07 The disadvantage of this pattern is that each method performs a complex task behind the scenes: it makes a network call, parses the data, instantiates some model objects, and finally returns them via the callback. There are a lot of places where it can go wrong, and because of this, it's hard to test. 01:29 These methods are also asynchronous, which makes them even harder to test. Also, we would need to have a network stack set up or mocked, which makes the tests complicated. 01:39 In Swift, there are other patterns we can use to make this simpler.

The Resource Struct

01:51 We create a Resource struct, which is generic over the result type. This struct has two properties: the URL of the endpoint, and a parse function. The parse function tries to convert some data into the result:

struct Resource<A> {
    let url: NSURL
    let parse: NSData -> A?
}

02:12 The parse function's return type is optional because the parsing might fail. Instead of making it optional, we could also use a Result type or make it throws in order to pass on more detailed information about what went wrong. 02:27 Additionally, if we wanted to deal only with JSON, the parse function could take an AnyObject instead of NSData. 02:37 However, using AnyObject would prevent us from using our Resource for anything but JSON — for example, images.

02:59 Let's create the episodesResource. It's just a simple resource that returns NSData:

let episodesResource = Resource<NSData>(url: url, parse: { data in
    return data
})

03:33 In the end, this resource should have a result type of [Episode]. We'll refactor the parse function in several steps to get from a result of NSData to a result of [Episode].

The Webservice Class

03:58 To load a resource from the network, we create a Webservice class with just one method: load. This method is generic and takes the resource as its first parameter. 04:32 The second parameter is a completion handler, which takes an A? because the request might fail or something else could go wrong. 04:48 In the load method, we use NSURLSession.sharedSession() to make the call. 04:54 We create a data task with the URL, which we get from the resource. 05:07 The resource bundles all the information we need to make a request. Currently, it only contains the URL, but there could be more properties in the future. 05:13 In the data task's completion handler, we get the data as the first parameter, but we'll ignore the other two parameters. 05:26 Finally, to start the data task, we have to call resume:

final class Webservice {
    func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
        NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
            if let data = data {
                completion(resource.parse(data))
            } else {
                completion(nil)
            }
        }.resume()
    }
}

05:38 To call the completion handler, we have to transform the data into the resource's result type by applying the parse function. 05:53 Since the data is optional, we use optional binding. If the data is nil, we call the completion handler with nil. 06:10 If the data isn't nil, we call the completion handler with the result of the parse function.

06:22 Because we're working in a playground, we have to make it execute indefinitely; otherwise, it'll stop as soon as the main queue is done:

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

07:00 Let's create a Webservice instance and call its load method with the episodesResource. In the completion handler, we'll print the result:

Webservice().load(episodesResource) { result in
    print(result)
}

07:18 In the console, we see that we get back some raw binary data. 07:31 Before we continue, we'll refactor the load method — we don't like the double call to completion. 07:51 We can try using a guard let. 08:02 However, then we still have two calls to completion, and we also have to add an extra return statement:

final class Webservice {
    func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
        NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
            guard let data = data else {
                completion(nil)
                return
            }
            completion(resource.parse(data))
        }.resume()
    }
}

08:07 Another approach is to use flatMap. 08:20 First, we can try map. However, map gives us an A??, instead of the A? we're looking for. 08:42 Using flatMap will remove the double optional:

final class Webservice {
    func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
        NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
            let result = data.flatMap(resource.parse)
            completion(result)
        }.resume()
    }
}

Parsing JSON

08:58 As the next step, we'll change the episodesResource in order to parse the NSData into a JSON object. 09:08 For this, we'll use the built-in JSON parsing. 09:23 Since JSON parsing is a throwing operation, we call the parsing method with try?:

let episodesResource = Resource<AnyObject>(url: url, parse: { data in
    let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
    return json
})

09:40 In the sidebar, we see that the binary data gets parsed. It's an array of dictionaries, so we could make the result type more specific. 09:52 A JSON dictionary contains Strings as the keys and AnyObjects as the values. 10:05 If we change the result type to an array of JSONDictionarys, we need to add a cast as well:

typealias JSONDictionary = [String: AnyObject]

let episodesResource = Resource<[JSONDictionary]>(url: url, parse: { data in
    let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
    return json as? [JSONDictionary]
})

10:23 The next step is to return an array of Episodes, so we need to turn each JSON dictionary into an Episode. 10:37 We can do this in an initializer on Episode that takes a dictionary. 10:53 Before we write this initializer though, we'll first add some properties on Episode: id and title, which are both Strings. In the real project, there are many more properties:

struct Episode {
    let id: String
    let title: String
    // ...
}

11:13 We can now write a failable initializer in an extension. By writing it in an extension, we keep the default memberwise initializer. 11:55 Within this initializer, we first need to check if the dictionary contains all the data we need. 12:02 We use a guard statement for that, and then we check if the dictionary contains an id and if it's a String. Extracting the title works the same way. 12:24 If the guard fails, we immediately return nil. 12:32 If it succeeds, we can assign the id and the title:

extension Episode {
    init?(dictionary: JSONDictionary) {
        guard let id = dictionary["id"] as? String,
            title = dictionary["title"] as? String else { return nil }
        self.id = id
        self.title = title
    }
}

12:48 Now we can refactor the episodesResource to return an array of Episodes. 13:17 First, we check if we have JSON dictionaries. Otherwise, we immediately return nil. 13:39 To convert the dictionaries to episodes, we can map over them and use the failable Episode.init as our transform function. 13:55 However, the initializer returns an optional, so the result of the map is [Episode?]. But we don't want the nils in there; the result's type should be [Episode]. 14:12 Again, we can fix that by using flatMap.

14:18 In our project, we used a different version of flatMap. flatMap will silently ignore the dictionaries that couldn't be parsed, and we want to fail completely in case any of the dictionaries are invalid. Not ignoring the invalid dictionaries is a domain-specific decision:

extension SequenceType {
    public func failingFlatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]? {
        var result: [T] = []
        for element in self {
            guard let transformed = try transform(element) else { return nil }
            result.append(transformed)
        }
        return result
    }
}

14:52 We can refactor our parse function to remove the double return statements. 15:01 First, we could try using guard again, but this doesn't remove the two return statements. 15:18 However, guard allows us to get rid of one level of nesting, and the early exit is clearer:

let episodesResource = Resource<[Episode]>(url: url, parse: { data in
    let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
    guard let dictionaries = json as? [JSONDictionary] else { return nil }
    return dictionaries.flatMap(Episode.init)
})

15:28 We can try to get rid of the double return by using optional chaining on dictionaries:

let episodesResource = Resource<[Episode]>(url: url, parse: { data in
    let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
    let dictionaries = json as? [JSONDictionary]
    return dictionaries?.flatMap(Episode.init)
})

15:44 This starts to get hard to understand. We have an optional dictionaries, and we use optional chaining to call flatMap, which has a failable initializer as its argument. In this case, we would probably go for the guard version, as it's clearer. 16:02 However, you could make an argument for either solution.

JSON Resources

16:07 Once we create more resources, it's necessary to duplicate the JSON parsing in each resource. 16:19 To remove the duplication, we could create a different kind of resource. However, we can also extend the existing resource with another initializer. 16:34 This initializer also takes a URL, but the type of the parse function is AnyObject -> A? instead of NSData -> A?. 17:09 We wrap this parse function in another function of type NSData -> A? and move the JSON parsing from our episodesResource into this wrapper. 17:33 Because the parsed JSON is an optional, we can use flatMap to call parseJSON:

extension Resource {
    init(url: NSURL, parseJSON: AnyObject -> A?) {
        self.url = url
        self.parse = { data in
            let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
            return json.flatMap(parseJSON)
        }
    }
}

18:00 Now we can change our episodesResource to use the new initializer:

let episodesResource = Resource<[Episode]>(url: url, parseJSON: { json in
    guard let dictionaries = json as? [JSONDictionary] else { return nil }
    return dictionaries.flatMap(Episode.init)
})

Naming the Resources

18:17 Another thing we don't like is that this episodesResource is in the global namespace. We're also not fond of its name. 18:30 We can move the episodesResource into an extension on Episode as a type property. 18:50 We could rename it to allEpisodesResource, a descriptive and verbose name. 19:03 However, we don't really like that. Looking at the type, it's already clear that it belongs to Episode. From the type, it's also clear that it's a resource, so why don't we just call it all? 19:20 At the call site, it'll be clear:

Webservice().load(Episode.all) { result in
    print(result)
}

19:40 Looking at the call site really convinced us that this is a good idea. 19:45 However, at first we thought it was a dangerous name, as you might confuse this with a collection. 19:53 We don't think that's a problem though, because it would immediately fail if you try to use it as a collection.

20:09 In the extension on Episode, we can also add other resources that depend on the episode's properties — for example, a media resource, which fetches the media for a particular episode. 20:47 In the media resource, we can use string interpolation to construct a URL:

extension Episode {
    var media: Resource<Media> {
        let url = NSURL(string: "http://localhost:8000/episodes/\(id).json")!
        // TODO Return the resource ...
    }
}

21:18 If we need more parameters that aren't available in the Episode struct, we can change the resource property to a method and pass the parameters in directly.

21:27 What we like about this approach to networking is that almost all the code is synchronous. It's simple, it's easy to test, and we don't need to set up a networking stack or something to test it. The only asynchronous code we have is the Webservice.load method. 21:53 This architecture is a good example of something that comes naturally out of Swift; Swift's generics and structs make it easy to design it like this. The same design wouldn't have had the same advantages in Objective-C, and it would have felt out of place.

22:21 Let's add POST support in a future episode.

Resources

  • Playground

    Written in Swift 2.2

  • Episode Video

    Become a subscriber to download episode videos.

Related Blogposts

In Collection

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes