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 String
s as the keys and AnyObject
s
as the values. 10:05 If we change the result type to an array of
JSONDictionary
s, 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 Episode
s, 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 String
s. 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
Episode
s. 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 nil
s 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.