Skip to content

Instantly share code, notes, and snippets.

@ivan-magda
Last active July 10, 2016 02:26
Show Gist options
  • Save ivan-magda/73a0dfa641be2b591e733db57b217ae2 to your computer and use it in GitHub Desktop.
Save ivan-magda/73a0dfa641be2b591e733db57b217ae2 to your computer and use it in GitHub Desktop.
Explore an alternative approach to building the networking layer of an app from "Swift Talk #1 episode". In this playground we are searching for photos by a specific tags with Flickr API.
//: Explore an alternative approach to building the networking layer of an app from [Swift Talk #1 episode](https://talk.objc.io/episodes/S01E01-networking).
//: They make use of Swift's generics and structs to end up with simple, testable code.
//:
//: In this playground we are searching for photos by a specific tags with [Flickr API](https://www.flickr.com/services/api/flickr.photos.search.html).
import UIKit
import XCPlayground
//: `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<T> {
let url: NSURL
let parse: NSData -> T?
}
extension Resource {
init(url: NSURL, parseJSON: AnyObject -> T?) {
self.url = url
self.parse = { data in
let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
return json.flatMap(parseJSON)
}
}
}
//: `Photo` struct, which is representing a Flickr Photo. This struct has three properties: the id of the photo, title and a medium url string. Also a private enum `Key` for convenience JSON parsing.
struct Photo {
private enum Key: String {
case Id = "id"
case Title = "title"
case MediumURL = "url_m"
}
let id: String
let title: String
let mediumURL: String
}
//: Parsing JSON
typealias JSONDictionary = [String: AnyObject]
extension Photo {
init?(json: JSONDictionary) {
guard let id = json[Key.Id.rawValue] as? String,
title = json[Key.Title.rawValue] as? String,
mediumURL = json[Key.MediumURL.rawValue] as? String else { return nil }
self.id = id
self.title = title
self.mediumURL = mediumURL
}
}
//: Convenience `FlickrSearch` class for searching photos by tags. This class has one property - `tags`. Using this tags we could generate endpoint URL with specific tags and also `Resource` that will contains all information.
class FlickrSearch {
let tags: [String]
init(tags: [String]) {
self.tags = tags
}
func tagsResource() -> Resource<[Photo]> {
return Resource<[Photo]>(url: resourceURL()) { json in
let photos = (json as? NSDictionary)?.valueForKeyPath("photos.photo") as? [JSONDictionary]
return photos?.failingFlatMap(Photo.init)
}
}
private func resourceURL() -> NSURL {
let tagsString = tags.joinWithSeparator(",")
let url = NSURL(string: "https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=f8d31c5ab65c3806dfbaa13b4a3aaff1&tags=\(tagsString)&safe_search=1&extras=url_m&format=json&nojsoncallback=1")!
return url
}
}
//: `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
}
}
//: The Webservice Class
//:
//: 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. The second parameter is a completion handler, which takes an T? because the request might fail or something else could go wrong. In the load method, we use NSURLSession.sharedSession() to make the call. We create a data task with the URL, which we get from the resource. 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. In the data task's completion handler, we get the data as the first parameter. We'll ignore the other two parameters. Finally, to start the data task, we have to call resume:
final class WebService {
func load<T>(resource: Resource<T>, completion: T? -> ()) {
NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
let result = data.flatMap(resource.parse)
completion(result)
}.resume()
}
}
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
//: Search for photos by tags.
//: Create a FlickrSearch and Webservice instances and call load method with the photosResource. In the completion handler, we'll print the result:
let flickrSearch = FlickrSearch(tags: ["Russia", "Moscow", "Saint-Petersburg"])
WebService().load(flickrSearch.tagsResource()) { result in
guard let photos = result else { return }
photos.enumerate().forEach { print("\($0). id: \($1.id), title: \($1.title)") }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment