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

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.

Boris Bügling

May 24, 2016
Tweet

More Decks by Boris Bügling

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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}" ] } }
  4. Mapping JSON responses to objects • The hardest problem in

    Swift development ! • Lots of libraries tackling the problem • My favourite: https://github.com/Anviking/Decodable
  5. 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" ) } }
  6. Decoding JSON do { let json = try NSJSONSerialization.JSONObjectWithData(data, options:

    []) let repo = try [Repository].decode(json) } catch { print(error) }
  7. 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) } [...] }
  8. ...which makes a pretty annoying user experience let name =

    entry.fields["name"] as! String Solution: code generation
  9. 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 %}) } }
  10. $ 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) } }
  11. 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)
  12. • Offers us to create a question • We can

    fill in the form we get • Submit the form
  13. Siren Link { "entities": [ { "links": [ { "rel":

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

    "POST", "fields": [ { "name": "question" }, { "name": "choices" } ], "type": "application/x-www-form-urlencoded" } } }
  15. But don't forget about the real API • Record fixtures

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

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

    stub(http(.PUT, "/kylef/Mockingjay"), json(body))
  18. 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
  19. 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
  20. 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) } }
  21. VVDocumenter-Xcode /** <#Description#> - parameter matching: <#matching description#> - parameter

    completion: <#completion description#> - returns: <#return value description#> */ https://github.com/onevcat/VVDocumenter-Xcode
  22. 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)
  23. 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.
  24. Also supports Carthage $ carthage build --no-skip-current --platform iOS $

    carthage archive Contentful => Share the result archive as part of your GitHub release
  25. 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.
  26. What have we learned? • Use Playgrounds and community-built tooling

    • Document extensively • Test thoroughly • Open source your work => Achieve that full quality index