Slide 1

Slide 1 text

Building a Better API Client in Swift Twilio Signal, May 2016 Boris Bügling - @NeoNacho

Slide 2

Slide 2 text

CocoaPods

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Why do we want to build better API clients?

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Mobile apps need to be more reliable than the web.

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Let's build better API clients to help building better apps

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

What does better mean?

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

Agenda • Development • Testing • Documentation • Deployment

Slide 17

Slide 17 text

Development

Slide 18

Slide 18 text

How do we talk about our APIs?

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Documentation

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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}" ] } }

Slide 23

Slide 23 text

Written by my colleague Mario

Slide 24

Slide 24 text

Mapping JSON responses to objects • The hardest problem in Swift development ! • Lots of libraries tackling the problem • My favourite: https://github.com/Anviking/Decodable

Slide 25

Slide 25 text

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" ) } }

Slide 26

Slide 26 text

Decoding JSON do { let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) let repo = try [Repository].decode(json) } catch { print(error) }

Slide 27

Slide 27 text

Demo $ pod playgrounds Decodable

Slide 28

Slide 28 text

Dynamic APIs and type safety

Slide 29

Slide 29 text

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) } [...] }

Slide 30

Slide 30 text

...which makes a pretty annoying user experience let name = entry.fields["name"] as! String Solution: code generation

Slide 31

Slide 31 text

https://github.com/kylef/stencil

Slide 32

Slide 32 text

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 %}) } }

Slide 33

Slide 33 text

$ 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) } }

Slide 34

Slide 34 text

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)

Slide 35

Slide 35 text

Anticipating change is one of the central themes of REST.

Slide 36

Slide 36 text

Example: creating a poll

Slide 37

Slide 37 text

Slide 38

Slide 38 text

• Offers us to create a question • We can fill in the form we get • Submit the form

Slide 39

Slide 39 text

We can do the same in our apps with Hypermedia

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Siren Link { "entities": [ { "links": [ { "rel": ["self"], "href": "questions/1/choices/1" } ], "rel": ["choices"], "properties": { "choice": "Swift", "votes": 22 } } ] }

Slide 42

Slide 42 text

Siren Action { "actions": { "create": { "href": "/questions", "method": "POST", "fields": [ { "name": "question" }, { "name": "choices" } ], "type": "application/x-www-form-urlencoded" } } }

Slide 43

Slide 43 text

We can now change implementation details on the fly

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Change HTTP methods e.g. PUT to POST

Slide 46

Slide 46 text

Change the content- type

Slide 47

Slide 47 text

Change fields used in forms

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

Demo $ pod playgrounds Hyperdrive

Slide 50

Slide 50 text

Testing

Slide 51

Slide 51 text

Stub or mock HTTP requests • Mockingjay • CCLRequestReplay • OHHTTPStubs • VCRURLConnection • ...

Slide 52

Slide 52 text

But don't forget about the real API • Record fixtures from live servers • Leave the option to run against production • ...and do that periodically

Slide 53

Slide 53 text

Use stubbing to test error conditions let error = NSError() stub(http(.PUT, "/kylef/Mockingjay"), failure(error))

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Use Xcode Code Coverage

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Documentation

Slide 59

Slide 59 text

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 -> Void) -> NSURLSessionDataTask? { var parameters = matching parameters["initial"] = true return sync(parameters, completion: completion) } }

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

$ bundle exec jazzy --podspec Contentful.podspec

Slide 62

Slide 62 text

VVDocumenter-Xcode /** <#Description#> - parameter matching: <#matching description#> - parameter completion: <#completion description#> - returns: <#return value description#> */ https://github.com/onevcat/VVDocumenter-Xcode

Slide 63

Slide 63 text

Playgrounds

Slide 64

Slide 64 text

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)

Slide 65

Slide 65 text

Rendered markup

Slide 66

Slide 66 text

Deployment

Slide 67

Slide 67 text

"Download the SDK from our webpage."

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

use_frameworks! target 'My App' do pod 'Contentful' end

Slide 70

Slide 70 text

Binary frameworks Please ship dynamic frameworks!

Slide 71

Slide 71 text

Consider open source

Slide 72

Slide 72 text

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.

Slide 73

Slide 73 text

Also supports Carthage $ carthage build --no-skip-current --platform iOS $ carthage archive Contentful => Share the result archive as part of your GitHub release

Slide 74

Slide 74 text

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.

Slide 75

Slide 75 text

Don't forget Objective-C !

Slide 76

Slide 76 text

What have we learned? • Use Playgrounds and community-built tooling • Document extensively • Test thoroughly • Open source your work => Achieve that full quality index

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

• ! https://www.contentful.com • " #ContentSocks at us • # @NeoNacho [email protected] http://buegling.com/talks