Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jake Craige: Modern Swift Networking with Swish

Realm
August 25, 2016

Jake Craige: Modern Swift Networking with Swish

Abstract/excerpt: In the Apple ecosystem, everyone has their own way of implementing a network stack. Many end up with a large `WebService` class where everything gets dumped. These classes, like massive view controllers, expose broad interfaces that are hard to understand, unwieldy to use, and difficult to test.
But network stacks don't have to be like this! Let's talk about how you can use the Swish framework to build a protocol-oriented and testable networking stack in Swift that will make you happy.
Bio: Jake Craige is a Developer at thoughtbot, a consulting company of designers and developers that partner with you to build great web and mobile products. He's really excited about Swift and how its static typing and functional capabilities lead to safe and creative solutions to common problems.
Twitter: https://twitter.com/jakecraige

Realm

August 25, 2016
Tweet

More Decks by Realm

Other Decks in Technology

Transcript

  1. Setting up a Model with Argo struct Comment { let

    id: Int let text: String let user: String } extension Comment: Decodable { // Assume: { "id": 1, "commentText": "Hello world", "user": "ralph" } static func decode(json: JSON) -> Decoded<Comment> { return curry(Comment.init) <^> json <| "id" <*> json <| "commentText" <*> json <| "user" } }
  2. Building a GET Request struct CommentRequest: Request { typealias ResponseObject

    = Comment let id: Int func build() -> NSURLRequest { let url = NSURL( string: "https://www.example.com/comments/\(id)" )! return NSURLRequest(URL: url) } }
  3. Executing the GET Request let request = CommentRequest(id: 1) APIClient().performRequest(request)

    { result in switch result { // Result<Comment, SwishError> case let .Success(comment): print("Here's the comment: \(comment)") case let .Failure(error): print("Oh no, an error: \(error)") } } // => Comment(id: 1, text: "Hi", user: "ralph")
  4. Building a POST Request struct CreateCommentRequest: Request { typealias ResponseObject

    = Comment let text: String let user: String var jsonPayload: [String: AnyObject] { return ["text": text, "user": user] } func build() -> NSURLRequest { // ... } }
  5. Building a POST Request struct CreateCommentRequest: Request { // ...

    func build() -> NSURLRequest { let url = NSURL(string: "https://www.example.com/comments")! let request = NSMutableURLRequest(URL: url) request.HTTPMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.jsonPayload = jsonPayload return request } }
  6. Executing the POST Request let request = CreateCommentRequest(text: "Hola", user:

    "ralph") APIClient().performRequest(request) { result in switch result { // Result<Comment, SwishError> case let .Success(comment): print("Here's the comment: \(value)") case let .Failure(error): print("Oh no, an error: \(error)") } } // => Comment(id: 2, text: "Hola", user: "ralph")
  7. ! "

  8. Argo's Decodable protocol Decodable { associatedtype DecodedType = Self static

    func decode(_ json: JSON) -> Decoded<DecodedType> }
  9. Argo's Decodable protocol Decodable { associatedtype DecodedType = Self static

    func decode(_ json: JSON) -> Decoded<DecodedType> } extension Comment: Decodable { static func decode(json: JSON) -> Decoded<Comment> { return curry(Comment.init) <^> json <| "id" <*> json <| "commentText" <*> json <| "user" } }
  10. Swish's Request protocol Parser { associatedtype Representation static func parse(j:

    AnyObject) -> Result<Representation, SwishError> } protocol Request { associatedtype ResponseObject associatedtype ResponseParser: Parser = JSON func build() -> NSURLRequest func parse(j: ResponseParser.Representation) -> Result<ResponseObject, SwishError> }
  11. Swish's Request protocol Request { associatedtype ResponseObject associatedtype ResponseParser: Parser

    = JSON func build() -> NSURLRequest func parse(j: ResponseParser.Representation) -> Result<ResponseObject, SwishError> } struct CommentRequest: Request { typealias ResponseObject = Comment let id: Int func build() -> NSURLRequest { let url = NSURL(string: "https://www.example.com/comments/\(id)")! return NSURLRequest(URL: url) } }
  12. Swish Request.parse struct CommentRequest: Request { typealias ResponseObject = Comment

    let id: Int func build() -> NSURLRequest { let url = NSURL(string: "https://www.example.com/comments/\(id)")! return NSURLRequest(URL: url) } func parse(j: JSON) -> Result<Comment, SwishError> { return .fromDecoded(Comment.decode(j)) // Default return .fromDecoded(j <| "comment") // Root key } }
  13. Testing Retrieving a Comment itBehavesLike(.GETRequest, request: CommentRequest(id: 1)) it("points to

    /comments/:id") { let request = CommentRequest(id: 1) expect(request.build()).to(hitEndpoint("/comments/1")) }
  14. Testing Creating a Comment itBehavesLike(.POSTRequest, request: CreateCommentRequest(text: "", user: ""))

    it("points to /comments") { let request = CreateCommentRequest(text: "", user: "") expect(request.build()).to(hitEndpoint("/comments")) } it("has a payload with the text and user") { let request = CreateCommentRequest(text: "Hi!", user: "ralph") expect(request.build()).to(havePayload([ "text": "Hi!", "user": "ralph" ])) }
  15. Stubbing the Network it("completes the full request cycle") { let

    request = CommentRequest(id: 1) stub(request).with(.comment) var response: Comment? = .None APIClient().performRequest(request) { response = $0.value } expect(response) .toEventually(equal(Comment(id: 1, text: "Hallo", user: "ralph"))) }
  16. What else can you do with Swish? 1. Dependency injection

    of APIClient, protocol Client, for testing 2. Cancel in-flight requests 3. Execute requests on different queues. 4. Support arbitrary JSON parsers or response types. 5. Anything you want.