iOS Programming Guide
Get the most out of the MyDataHelpsKit iOS SDK by familiarizing yourself with these common programming tasks and design patterns.
Authentication
To guard against unauthorized use, most features of the MyDataHelpsKit SDK require a participant access token.
Obtaining a Participant Access Token
You are responsible for implementing an appropriate authentication layer for obtaining a participant’s access token. Your iOS app is responsible for presenting any UI for authentication, managing the access token, and configuring MyDataHelpsKit to use it.
See Participant Access Tokens for details on how to manage participant access tokens.
Configuring MyDataHelpsKit With a Token
The ParticipantSession
class is the primary interface for interacting with MyDataHelps, and is initialized using a ParticipantAccessToken
. A single instance can be shared throughout your app, is thread-safe, and can be retained for as long as the participant’s access token is valid. For example:
let client = MyDataHelpsClient()
let token = ParticipantAccessToken(token: tokenString)
let session = ParticipantSession(client: client, accessToken: token)
Warning
Never use your service token in your client app. Use a participant token instead.
Refreshing the Token
The iOS SDK doesn’t have an event for token expiration. Your app will need to keep track of the expires_in
time returned by the original token request and request a new token when necessary.
Create a new ParticipantSession
when you obtain or renew a participant access token or when a different participant logs in.
// You can re-use the existing 'client' object.
let token = ParticipantAccessToken(token: tokenString)
let newSession = ParticipantSession(client: client, accessToken: token)
// Use the new session object for all SDK operations moving forward.
Asynchronous Behavior and Error Handling
Communication with the MyDataHelps platform is typically asynchronous. All asynchronous requests in ParticipantSession
are implemented as Swift async
functions. These functions suspend the calling thread until the request is complete, and then asynchronously deliver a result object (if applicable). Any server requests or response parsing are performed in a background thread with no guarantee about the specific thread the result is returned on.
The caller is responsible for controlling the thread in which the async continuation occurs; the calling Task, function, or class should be marked as @MainActor
or use DispatchQueue.main.async { }
if it updates the app’s UI, as shown in the examples below.
Error Handling
All errors thrown by MyDataHelpsKit are of type MyDataHelpsError
, an enum with specific cases for known error conditions. Instead of writing two catch
blocks (one catch let error as MyDataHelpsError
and one fallback catch
for unknown error types), it’s safe to use the provided initializer to cast the unspecified Error
type in a single catch
block to an actual MyDataHelpsError value:
do {
nameLabel.text = try await session.getParticipantInfo().demographics.firstName
} catch {
// All errors thrown by the SDK will be of type MyDataHelpsError:
switch MyDataHelpsError(error) {
...
}
}
MyDataHelpsError
values do not have localized descriptions; interpret the specific error enum case to determine and localize user-facing error messages in a manner appropriate to your app.
If your participant access token is invalid or expired, the SDK will throw MyDataHelpsError.unauthorizedRequest(HTTPResponseError)
. Refresh your participant access token and create a new ParticipantSession with the updated token. See authentication above for details about access tokens.
The MyDataHelps API has a rate limiting feature to preserve stability for all customers. If you send too many server requests to the MyDataHelps platform, the SDK will throw MyDataHelpsError.tooManyRequests(APIRateLimit, HTTPResponseError)
. See APIRateLimit
documentation for more information.
Examples
Using MyDataHelpsKit’s async APIs and errors with SwiftUI (see the MyDataHelpsKit example app for additional SwiftUI examples):
@MainActor class ParticipantInfoViewModel: ObservableObject {
let session: ParticipantSession
@Published var name: String?
@Published var errorMessage: MyDataHelpsError?
func fetch() async {
do {
// ParticipantInfoViewModel is marked as @MainActor, guaranteeing the
// name (or any error message) will be set on the main thread.
name = try await session.getParticipantInfo().demographics.firstName
} catch {
// This safely casts the thrown error to a MyDataHelpsError
// without an extra 'catch let...as...' block.
switch MyDataHelpsError(error) {
case .unauthorizedRequest: reauthenticate()
case .timedOut: errorMessage = "Timed out"
...
}
}
}
}
Using MyDataHelpsKit’s async APIs and errors with UIKit:
class ParticipantInfoViewController: UIViewController {
let session: ParticipantSession
@IBOutlet var nameLabel: UILabel!
@IBOutlet var errorLabel: UILabel!
func viewDidLoad() {
Task { @MainActor in
do {
let name = try await self.session.getParticipantInfo().demographics.firstName
self.nameLabel.text = name
} catch {
// This safely casts the thrown error to a MyDataHelpsError
// without an extra 'catch let...as...' block.
switch MyDataHelpsError(error) {
case .unauthorizedRequest: self.reauthenticate()
case .timedOut: self.errorLabel.text = "Timed out"
...
}
}
}
}
}
Query APIs
The SDK uses a “query” concept for APIs that retrieve data from the MyDataHelps platform. These queries all follow the same design patterns for ease of use. Queries are performed using corresponding query functions in ParticipantSession
. These functions are asynchronous and throw MyDataHelpsError
values upon failure. For example, to fetch a list of survey tasks assigned to the participant:
let session: ParticipantSession
let criteria = SurveyTaskQuery(statuses: Set([.incomplete]), sortOrder: .dateAscending)
let tasks = try await session.querySurveyTasks(criteria)
print("\(tasks.surveyTasks.count) tasks assigned.")
Your app uses Query
structs, such as SurveyTaskQuery
in the above example, to specify filtering and sorting criteria for the data to fetch. Queries have optional parameters; set non-nil/non-default values only for the parameters you want to use for filtering. Consult the documentation for each query struct and its properties for details about available options.
Paged Results
Query functions typically return collections of data, grouped into pages. In the example above, querySurveyTasks
produces a SurveyTaskResultPage
. Each paged result has an array of items and a nextPageID
which identifies the following page of results (if any). Most queries have a limit
parameter that controls the page size.
If the entire set of results fit within a single page, the array of items within the Page object contains all of the results, and nextPageID
is nil to indicate there are no additional pages to fetch. If there are zero results, a Page object is still returned, with an empty array of items and a nil nextPageID
.
When there are multiple pages of results, the first page object has an array of items with count == limit
. Its nextPageID
is an opaque identifier which your app can use to fetch the next page of results. Use the page(after:)
function of the original query object to produce a new query for next page to fetch. Repeat this process as needed in you app’s UI to fetch additional pages of results:
let session: ParticipantSession
let criteria = SurveyTaskQuery(statuses: Set([.incomplete]), sortOrder: .dateAscending, limit: 5)
let page1 = try await session.querySurveyTasks(criteria)
print("First page: \(page1.surveyTasks)")
guard let page2Query = criteria.page(after: page1) else {
return // There was only one page of results available.
}
let page2 = try await session.querySurveyTasks(page2Query)
print("Second page: \(page2.surveyTasks)")
guard let page3Query = criteria.page(after: page2) ...
If your app is fetching a single value, specify the appropriate identifier(s) in the Query object and expect a paged result with a single item (if found):
let criteria = SurveyTaskQuery(surveyID: surveyID)
if let task = try await session.querySurveyTasks(criteria).surveyTasks.first {
print("Task \(task.id) is assigned for survey \(surveyID).")
} else {
print("No task assigned for that survey.")
}
Best Practices and Sample Code
The MyDataHelpsKit example app has a variety of practical examples of using the SDK’s query functions. It demonstrates best practices for automatic loading of additional pages, infinite scrolling, search, and handling empty results and errors. PagedListView
encapsulates all paging logic into a single full-screen list view, while smaller view components are used throughout the app to embed paged results within other views.
Tip
To ensure a smooth UX and avoid rate-limiting failures, take care with paging and search logic. Only fetch additional pages of data when your app needs to display more data to the user (such as scrolling to the bottom of a list view). Implement debouncing on search input to avoid executing queries with every keystroke.
Type-safe Identifiers
Most model types in this SDK have at least one identifier value, typically an opaque auto-generated string. It is an error
to use the identifier from a model of type A when fetching, querying, or creating models of type B. MyDataHelpsKit uses ScopedIdentifier
types prevent such mistakes by making it a compiler error to use a mismatched identifier.
For example, the following code mistakenly uses a surveyID
to delete a survey result,
instead of the surveyResultID
. If allowed, this would fail to delete any survey result, or possibly delete
the wrong survey result. ScopedIdentifiers prevent such a mistake from compiling,
because surveyID and surveyResultID are incompatible types.
func deleteSurveyResult(containing answer: SurveyAnswer) async throws {
// Identifies an invalid result ID, and will not compile:
// try await session.deleteSurveyResult(answer.surveyID)
// Will compile and work correctly:
try await session.deleteSurveyResult(answer.surveyResultID)
}
func deleteAccount(_ account: ExternalAccount) async throws {
// External accounts and their associated providers have the same
// raw value for their IDs, but it's incorrect to use a provider
// ID when deleting an external account. This will not compile.
// try await session.deleteExternalAccount(account.provider.id)
// Will compile and work correctly:
try await session.deleteExternalAccount(account.id)
}
In most cases, your app does not need to explicitly create instances of ScopedIdentifiers: typically these identifiers are auto-generated by the MyDataHelps platform and returned to your app in query results.
Enum-like Types
Many enumeration-like values in MyDataHelpsKit, such as DeviceDataNamespace
or SurveyTaskStatus
, are implemented as RawRepresentable structs with static fields for known values, rather than strict enum
types. This facilitates forward compatibility for versions of your app that aren’t using the latest SDK release: queries will not immediately fail due to decoding errors if a new enumeration value is encountered.
Typically, you can treat these types exactly like strict Swift enum
types. Standard practice with switch
statements will help the compiler enforce treatment of all known values at compile time, etc. If your queries or UI cannot safely handle unknown values, filter for known values by using query parameters or by post-processing results returned by the SDK.