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
Date
s withExistAPI
, 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 Int
s, String
s, or Float
s. 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