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]?) -> ()) {
}
func loadMedia(episode: Episode, completion: (Media?) -> ()) {
}
}
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")!
}
}
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.