Save 37% off PRO during our Black Friday Sale! »

Building better API clients in Swift

Building better API clients in Swift

What are the best practices for building API clients in Swift? In this talk we will learn:

- how to make API clients adapt to change more gracefully using Hypermedia
- how to make APIs more accessible with interactive playgrounds and comprehensive documentation
- how to use code generation to bring the advantages of type safety to clients for more dynamic APIs
- how to set our tooling to ensure quality goals are met with every release.

Live coding will be used throughout.

9d2ea021919ff81e02d48530aae191bd?s=128

Boris Bügling

May 24, 2016
Tweet

Transcript

  1. Building a Better API Client in Swift Twilio Signal, May

    2016 Boris Bügling - @NeoNacho
  2. CocoaPods

  3. None
  4. None
  5. Why do we want to build better API clients?

  6. None
  7. None
  8. Mobile apps need to be more reliable than the web.

  9. None
  10. Let's build better API clients to help building better apps

  11. None
  12. What does better mean?

  13. None
  14. None
  15. None
  16. Agenda • Development • Testing • Documentation • Deployment

  17. Development

  18. How do we talk about our APIs?

  19. API Blueprint ## Choice [/questions/{question_id}/choices/{choice_id}] + Parameters + question_id: 1

    (required, number) - ID of the Question in form of an integer + choice_id: 1 (required, number) - ID of the Choice in form of an integer ### Vote on a Choice [POST] This action allows you to vote on a question's choice. + Response 201 + Headers Location: /questions/1
  20. Documentation

  21. Testing with dredd node_modules/.bin/dredd out/cpa.apib https://preview.contentful.com info: Beginning Dredd testing...

    pass: GET /spaces/cfexampleapi?access_token=XXX duration: 846ms pass: GET /spaces/cfexampleapi/content_types?access_token=XXX duration: 625ms
  22. API client coverage * swift (25/31, missing: 6) { "missing":

    { "GET": [ "/spaces/{space_id}/entries?content_type={content_type}&{attribute}%5Bexists%5D={value}", "/spaces/{space_id}/entries?skip={value}", "/spaces/{space_id}/entries?include={value}", "/spaces/{space_id}/entries/{entry_id}?locale={locale}", "/spaces/{space_id}/sync?initial=true&type={type}", "/spaces/{space_id}/sync?initial=true&type=Entry&content_type={content_type_id}" ] } }
  23. Written by my colleague Mario

  24. Mapping JSON responses to objects • The hardest problem in

    Swift development ! • Lots of libraries tackling the problem • My favourite: https://github.com/Anviking/Decodable
  25. Decoding JSON struct Repository { let name: String let stargazersCount:

    Int } extension Repository: Decodable { static func decode(j: AnyObject) throws -> Repository { return try Repository( name: j => "nested" => "name", stargazersCount: j => "stargazers_count" ) } }
  26. Decoding JSON do { let json = try NSJSONSerialization.JSONObjectWithData(data, options:

    []) let repo = try [Repository].decode(json) } catch { print(error) }
  27. Demo $ pod playgrounds Decodable

  28. Dynamic APIs and type safety

  29. Maps essentially to a Dictionary /// An Entry represents a

    typed collection of data in Contentful public struct Entry : Resource, LocalizedResource { /// System fields public let sys: [String:AnyObject] /// Content fields public var fields: [String:Any] { return Contentful.fields(localizedFields, forLocale: locale, defaultLocale: defaultLocale) } [...] }
  30. ...which makes a pretty annoying user experience let name =

    entry.fields["name"] as! String Solution: code generation
  31. https://github.com/kylef/stencil

  32. Template // This is a generated file. import CoreLocation struct

    {{ className }} {{% for field in fields %} let {{ field.name }}: {{ field.type }}?{% endfor %} } import Contentful extension {{ className }} { static func fromEntry(entry: Entry) throws -> {{ className }} { return {{ className }}({% for field in fields %} {{ field.name }}: entry.fields["{{ field.name }}"] as? {{ field.type }},{% endfor %}) } }
  33. $ contentful-generator cfexampleapi b4c0n73n7fu1 --output out // This is a

    generated file. import CoreLocation struct Dog { let name: String? let description: String? let image: Asset? } import Contentful extension Dog { static func fromEntry(entry: Entry) throws -> Dog { return Dog( name: entry.fields["name"] as? String, description: entry.fields["description"] as? String, image: entry.fields["image"] as? Asset) } }
  34. Offer a first-party offline persistence solution let store = CoreDataStore(context:

    self.managedObjectContext) let sync = ContentfulSynchronizer(client: client, persistenceStore: store) sync.mapAssets(to: Asset.self) sync.mapSpaces(to: SyncInfo.self) sync.map(contentTypeId: "1kUEViTN4EmGiEaaeC6ouY", to: Author.self) sync.map(contentTypeId: "5KMiN6YPvi42icqAUQMCQe", to: Category.self) sync.map(contentTypeId: "2wKn6yEnZewu2SCCkus4as", to: Post.self)
  35. Anticipating change is one of the central themes of REST.

  36. Example: creating a poll

  37. <html> <body> <h1>Poll</h1> <ul> <li><a href="/questions/new" rel="create">Make a new question</a></li>

    </ul> </body> </html>
  38. • Offers us to create a question • We can

    fill in the form we get • Submit the form
  39. We can do the same in our apps with Hypermedia

  40. Formats • HAL (application/hal+json) • Siren (application/vnd.siren+json)

  41. Siren Link { "entities": [ { "links": [ { "rel":

    ["self"], "href": "questions/1/choices/1" } ], "rel": ["choices"], "properties": { "choice": "Swift", "votes": 22 } } ] }
  42. Siren Action { "actions": { "create": { "href": "/questions", "method":

    "POST", "fields": [ { "name": "question" }, { "name": "choices" } ], "type": "application/x-www-form-urlencoded" } } }
  43. We can now change implementation details on the fly

  44. Change URIs of resources e.g. /polls/{id} to /questions/{id}

  45. Change HTTP methods e.g. PUT to POST

  46. Change the content- type

  47. Change fields used in forms

  48. None
  49. Demo $ pod playgrounds Hyperdrive

  50. Testing

  51. Stub or mock HTTP requests • Mockingjay • CCLRequestReplay •

    OHHTTPStubs • VCRURLConnection • ...
  52. But don't forget about the real API • Record fixtures

    from live servers • Leave the option to run against production • ...and do that periodically
  53. Use stubbing to test error conditions let error = NSError()

    stub(http(.PUT, "/kylef/Mockingjay"), failure(error))
  54. Or faulty responses let body = [ "description": nil ]

    stub(http(.PUT, "/kylef/Mockingjay"), json(body))
  55. Use Xcode Code Coverage

  56. Slather $ bundle exec slather coverage -s Contentful.xcodeproj Slathering... Sources/Asset.swift:

    17 of 27 lines (62.96%) Sources/Client.swift: 126 of 164 lines (76.83%) Sources/Configuration.swift: 14 of 15 lines (93.33%) Sources/DecodableData.swift: 6 of 6 lines (100.00%) Sources/Decoding.swift: 220 of 240 lines (91.67%) Sources/Entry.swift: 19 of 19 lines (100.00%) Sources/Resource.swift: 12 of 13 lines (92.31%) Sources/SignalUtils.swift: 39 of 48 lines (81.25%) Sources/SyncSpace.swift: 49 of 82 lines (59.76%) Test Coverage: 81.76% Slathered
  57. CI language: objective-c osx_image: xcode7.1 script: - xcodebuild -workspace Contentful.xcworkspace

    \ -scheme Contentful test -sdk iphonesimulator - bundle exec slather coverage --coveralls Contentful.xcodeproj - pod lib lint Contentful.podspec
  58. Documentation

  59. Swift supports inline documentation extension Client { /** Perform an

    initial synchronization of the Space this client is constrained to. - parameter matching: Additional options for the synchronization - parameter completion: A handler being called on completion of the request - returns: The data task being used, enables cancellation of requests */ public func initialSync(matching: [String:AnyObject] = [String:AnyObject](), completion: Result<SyncSpace> -> Void) -> NSURLSessionDataTask? { var parameters = matching parameters["initial"] = true return sync(parameters, completion: completion) } }
  60. None
  61. $ bundle exec jazzy --podspec Contentful.podspec

  62. VVDocumenter-Xcode /** <#Description#> - parameter matching: <#matching description#> - parameter

    completion: <#completion description#> - returns: <#return value description#> */ https://github.com/onevcat/VVDocumenter-Xcode
  63. Playgrounds

  64. Inline Markdown /*: ## Make the first request Create a

    client object using those credentials, this will be used to make most API requests. */ let client = Client(spaceIdentifier: SPACE, accessToken: TOKEN) /*: To request an entry with the specified ID: */ client.fetchEntry("5PeGS2SoZGSa4GuiQsigQu").1 .error { print($0) } .next { /*: All resources in Contentful have a variety of read-only, system-managed properties, stored in their “sys” property. This includes things like when the resource was last updated and how many revisions have been published. */ print($0.sys)
  65. Rendered markup

  66. Deployment

  67. "Download the SDK from our webpage."

  68. None
  69. use_frameworks! target 'My App' do pod 'Contentful' end

  70. Binary frameworks Please ship dynamic frameworks!

  71. Consider open source

  72. Pod template $ pod lib create Contentful Cloning `https://github.com/CocoaPods/pod-template.git` into

    `Contentful`. Configuring Contentful template. [...] What language do you want to use?? [ Swift / ObjC ] > swift [...] Running pod install on your new library.
  73. Also supports Carthage $ carthage build --no-skip-current --platform iOS $

    carthage archive Contentful => Share the result archive as part of your GitHub release
  74. Keep in mind that Swift has no stable ABI While

    your app’s runtime compatibility is ensured, the Swift language itself will continue to evolve, and the binary interface will also change. To be safe, all components of your app should be built with the same version of Xcode and the Swift compiler to ensure that they work together.
  75. Don't forget Objective-C !

  76. What have we learned? • Use Playgrounds and community-built tooling

    • Document extensively • Test thoroughly • Open source your work => Achieve that full quality index
  77. None
  78. • ! https://www.contentful.com • " #ContentSocks at us • #

    @NeoNacho boris@contentful.com http://buegling.com/talks