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:

Using a Token
let client = MyDataHelpsClient()
let token = ParticipantAccessToken(token: tokenString)
let session = ParticipantSession(client: client, accessToken: token)
let client = MyDataHelpsClient() let token = ParticipantAccessToken(token: tokenString) let session = ParticipantSession(client: client, accessToken: token)

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.

Refreshing a Token
// 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.
// 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:

Error handling
do {
    nameLabel.text = try await session.getParticipantInfo().demographics.firstName
} catch {
    // All errors thrown by the SDK will be of type MyDataHelpsError:
    switch MyDataHelpsError(error) {
        ...
    }
}
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):

Async Behavior with SwiftUI
@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"
            ...
            }
        }
    }
}
@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:

Async Behavior 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"
                ...
                }
            }
        }
    }
}
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:

Queries
let session: ParticipantSession
let criteria = SurveyTaskQuery(statuses: Set([.incomplete]), sortOrder: .dateAscending)
let tasks = try await session.querySurveyTasks(criteria)
print("\(tasks.surveyTasks.count) tasks assigned.")
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:

Paging
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) ...
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):

Fetching a single value
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.")
}
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.

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.

Type-safe identifiers
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)
}
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.