ExistAPI 0.0.16

ExistAPI 0.0.16

Maintained by Belle B. Cooper.



ExistAPI 0.0.16

ExistAPI

A framework for working with the Exist API on iOS.

ExistAPI wraps the Exist API in promises (via PromiseKit) to make it easier to work with API responses imperatively and chain API calls together. It also deserialises all API responses into strongly typed, Decodable models.

Installation

via Cocoapods:

pod 'ExistAPI', '~> 0.0.13'

Quick examples

Get a token from the Exist API to authorise your user (details in the Exist API docs here). Then create an API instance:

let token = getTokenFromAuthProcess()
let existAPI = ExistAPI(token: token)

Get the user's attributes for the past week:

existAPI.attributes()
	.done { attributes, response in
	// handle attribute models here
	}.catch { error in
	// deal with errors
	}

Get attributes with some params:

existAPI.attributes(names: ["steps", "mood"], limit: 12)
	.done { attributes, response in
	// handle data here
	}.catch { error in
	// handle error here
	}

Acquire an attribute:

existAPI.acquire(names: ["steps"])
	.done { attributeResponse, urlResponse in
	// deal with data here
	}.catch { error in
	// handle error
	}

Update data for some attributes:

let steps = IntAttributeUpdate(name: "steps", date: Date(), value: 3158)
let distance = FloatAttributeData(name: "steps_distance", date: Date(), value: 1.2)
existAPI.update(attributes: [steps, distance])
	.done { attributeResponse, urlResponse in
	// if some attributes failed but some succeeded, check failures here
	}.catch { error in
	// handle error
	}

Usage

ExistAPI uses PromiseKit to handle asynchronous networking tasks. If you haven't used promises before, the PromiseKit docs are very good, and will help you understand the basic usage. Essentially, promises let you act on the result of an asynchronous task as if it were synchronous, making your code easier to write, read, and maintain.

Each of the public functions available in the ExistAPI class returns a Promise. You can chain these together, but the most simple use is to use a .done closure for handling the result and a .catch closure for handling errors.

Please see the examples below for more ideas on how to use ExistAPI's promises.

Requirements

  • Swift 4.0
  • iOS 12
  • An Exist account
  • Cocoapods

Getting started

First, you'll need to create an Exist developer client. This blog post has detailed instructions on how to create a developer client.

To quickly get started, you can simply copy and paste your personal auth token from your developer client details page. This will let you start testing your app with the Exist API immediately, and save building the authorisation flow for your users until later.

Important notes

  • When using Dates with ExistAPI, always use the local device time.

Creating an ExistAPI instance

Create an instance of ExistAPI with your token, and optionally set a specific timeout period for network requests:

let existAPI = ExistAPI(token: yourToken, timeout: 40)

GET requests

func attributes(names: [String]?, limit: Int?, minDate: Date?, maxDate: Date?) -> Promise<(attributes: [Attribute], response: URLResponse)>

Returns an array of Attribute models:

struct Attribute: AttributeValues {
    let name: String
    let label: String
    let group: AttributeGroup
    let priority: Int
    let service: String
    let valueType: ValueType
    let valueTypeDescription: String
    private let values: [AttributeData]
}

public protocol AttributeValues {
    func getIntValues() throws -> [IntValue]
    func getStringValues() throws -> [StringValue]
    func getFloatValues() throws -> [FloatValue]
}

Each attribute uses the AttributeValues protocol to return its values as Ints, Strings, or Floats. It's up to you to use the correct corresponding function depending on the value type of the attribute you're working with. All available attributes and their types can be found here in the Exist API docs.

The value type models are as follows:

public protocol ValueObject {
    associatedtype ValueType
    var value: ValueType? { get }
    var date: Date { get }
}

public struct IntValue: ValueObject {
    public typealias ValueType = Int
    public var value: Int?
    public var date: Date
}

public struct StringValue: ValueObject {
    public typealias ValueType = String
    public var value: String?
    public var date: Date
}

public struct FloatValue: ValueObject {
    public typealias ValueType = Float
    public var value: Float?
    public var date: Date
}
func insights(limit: Int?, pageIndex: Int?, minDate: Date?, maxDate: Date?) -> Promise<(insights: InsightResponse, response: URLResponse)>

Returns an InsightResponse:

struct InsightResponse {
    let count: Int
    var next: String?
    var previous: String?
    let results: [Insight]
}

struct Insight: Codable {
    let created: Date
    let targetDate: Date?
    let type: InsightType
    let html: String
    let text: String
}
func averages(for attribute: String?, limit: Int?, pageIndex: Int?, minDate: Date?, maxDate: Date?) -> Promise<(averages: [Average], response: URLResponse)>

Returns an array of Average models:

class Average: Codable {
    let attribute: String
    let date: Date
    let overall: Float
    let monday: Float
    let tuesday: Float
    let wednesday: Float
    let thursday: Float
    let friday: Float
    let saturday: Float
    let sunday: Float
}
func correlations(for attribute: String?, limit: Int?, pageIndex: Int?, minDate: Date?, maxDate: Date?, latest: Bool?) -> Promise<(correlations: [Correlation], response: URLResponse)>

Returns an array of Correlation models:

class Correlation: Codable {
    let date: Date
    let period: Int
    let attribute: String
    let attribute2: String
    let value: Float
    let p: Float
    let percentage: Float
    let stars: Int
    let secondPerson: String
    let secondPersonElements: [String]
    let strengthDescription: String
    let starsDescription: String
    let description: String?
    let occurrence: String?
    let rating: CorrelationRating?
}
func user() -> Promise<(user: User, response: URLResponse)>

Returns a User model:

class User: Codable {
    let id: Int
    let username: String
    let firstName: String
    let lastName: String
    let bio: String
    let url: String
    let avatar: String
    let timezone: String
    let imperialUnits: Bool
    let imperialDistance: Bool
    let imperialWeight: Bool
    let imperialEnergy: Bool
    let imperialLiquid: Bool
    let imperialTemperature: Bool
    let trial: Bool
    let delinquent: Bool
}

POST requests

Attributes must be owned by a service before they can be updated. To own an attribute, use the acquire function. Updating an attribute not owned by your client will fail. Read more on attribute ownership in the Exist API docs.

public func acquire(names: [String]) -> Promise<(attributeResponse: AttributeResponse, response: URLResponse)>

Returns an AttributeResponse:

public struct AttributeResponse: Codable {
    var success: [SuccessfulAttribute]?
    var failed: [FailedAttribute]?
}

public struct SuccessfulAttribute: Codable {
    var name: String
    var active: Bool?
}

public struct FailedAttribute: Codable {
    var name: String
    var errorCode: String
    var error: String
}

To stop owning an attribute, use the release function:

public func release(names: [String]) -> Promise<(attributeResponse: AttributeResponse, response: URLResponse)>

Before using the update function, make sure you've read the Exist API docs on acquiring and updating attributes.

public func update<T: AttributeUpdate>(attributes: [T]) -> Promise<(attributeResponse: AttributeUpdateResponse, response: URLResponse)>

Takes an array of objects conforming to the AttributeUpdate protocol:

public protocol AttributeUpdate: Codable {
    associatedtype Value: Codable
    var name: String { get }
    var date: Date { get }
    var value: Value { get }
    func dictionaryRepresentation() throws -> [String: Any]?
}

There are three concrete implementations of AttributeUpdate:

public struct StringAttributeUpdate: AttributeUpdate {
    public typealias Value = String
}

public struct FloatAttributeUpdate: AttributeUpdate {
    public typealias Value = Float
}

public struct IntAttributeUpdate: AttributeUpdate {
    public typealias Value = Int
}

The update function returns an AttributeUpdateResponse:

public struct AttributeUpdateResponse: Codable {
    var success: [SuccessfullyUpdatedAttribute]?
    var failed: [FailedToUpdateAttribute]?
}

public struct SuccessfullyUpdatedAttribute: Codable {
    var name: String
    var date: Date
    var value: String?
}

public struct FailedToUpdateAttribute: Codable {
    var name: String?
    var date: Date?
    var value: String?
    var errorCode: String
    var error: String
}

Chaining

Because ExistAPI uses PromiseKit, you can chain multiple calls together. Here are some examples:

Since we need to first acquire attributes in a user's Exist account before we're able to update those attributes, we can chain these two steps together the first time we want to update an attribute with data:

existAPI.acquire(names: ["weight"]
	.then { attributeResponse, urlResponse in
     	guard let success = attributeResponse.success,
        	success.contains("weight") else { return }
     	let update = FloatAttributeUpdate(name: "weight", date: Date(), value: 65.8)
		existAPI.update(attributes: updates)
    		.done { attributeUpdateResponse, urlResponse in
        		// handle success
    		}.catch { error in
        		// handle error
    		}

PromiseKit lets us use when to only act on a bunch of promises when they're all completed, and to handle errors in this chain of promises just once. Using when lets us compile a bunch of calls to the Exist API and act on all the returned data at once:

let insightsPromise = existAPI.insights(days: 30)
let averagesPromise = existAPI.averages(limit: 30)
let correlationsPromise = existAPI.correlations

when(fulfilled: [insightsPromise, averagesPromise, correlationsPromise])
	.done { insights, averages, correlations in
	// handle data
	}.catch { error in
	// handle errors just once for all these promises
	}

Building the app

Clone the source and build using Xcode 10.

Running the tests

Create a new file inside ExistAPITests called TestConstants.swift and add a string constant called TEST_TOKEN with your own API token, like this:

let TEST_TOKEN = "your_token_string_here"

Without this, the tests will fail. You can get your access token by creating a developer client in your Exist account.

TODO

  • GET requests
  • POST requests
  • Create a convenience func for accessing only today's attributes
  • Support appending custom tags
  • Add tests for including queries in requests