How to use
Basic usage flow of Axon SDK
The Axon Singleton
Most of the time you will interact with the MDRAxon
singleton; or its pretty counter part [Medable axon]
. The first thing to do is to initialize it. Make sure you have internet connection.
1. Axon Initialization
// Init Axon
[Medable axon];
// Init Axon
Medable.axon()
2. Downloading required assets in advance
The next thing to do, is to download all the assets required for your study to work properly. There's a manager to download all of them at once and it fires NSNotifications when it starts/ends/etc.
// Download Step images
[[[Medable axon] assetDownloader] start];
// Listen to these notifications to be informed about progress
extern NSString *kDidStartDownloadingStepImagesNotification;
extern NSString *kDidDownloadAFileNotification;
extern NSString *kDidReceiveAFaultNotification;
extern NSString *kDidEndDownloadingStepImagesNotification;
// Consult documentation in MDRStepImagesDownloader's header file.
// Download Study Assets
Medable.axon().assetDownloader().start()
// Listen to these notifications to be informed about progress
Notification.Name.didStartDownloadingStepImages
Notification.Name.didDownloadAFile
Notification.Name.didReceiveAFault
Notification.Name.didEndDownloadingStepImages
// Consult documentation in MDRStepImagesDownloader's header file.
Here is an example of how to handle these notifications:
// First, you have to listen
- (ret)someMethod
{
NSNotificationCenter* nCenter = [NSNotificationCenter defaultCenter];
[nCenter addObserver:self selector:@selector(handleAssetDownloaderNotification:) name:kDidStartDownloadingStepImagesNotification object:nil];
[nCenter addObserver:self selector:@selector(handleAssetDownloaderNotification:) name:kDidDownloadAFileNotification object:nil];
[nCenter addObserver:self selector:@selector(handleAssetDownloaderNotification:) name:kDidReceiveAFaultNotification object:nil];
[nCenter addObserver:self selector:@selector(handleAssetDownloaderNotification:) name:kDidEndDownloadingStepImagesNotification object:nil];
}
// First, you have to listen
func someMethod() {
let nCenter = NotificationCenter.default
nCenter.addObserver(self, selector: #selector(handleAssetDownloadNotification(notification:)), name: NSNotification.Name.didStartDownloadingStepImages, object: nil)
nCenter.addObserver(self, selector: #selector(handleAssetDownloadNotification(notification:)), name: NSNotification.Name.didDownloadAFile, object: nil)
nCenter.addObserver(self, selector: #selector(handleAssetDownloadNotification(notification:)), name: NSNotification.Name.didReceiveAFault, object: nil)
nCenter.addObserver(self, selector: #selector(handleAssetDownloadNotification(notification:)), name: NSNotification.Name.didEndDownloadingStepImages, object: nil)
}
// Then, handle
- (void)handleAssetDownloaderNotification:(NSNotification*)notif
{
NSString* notifName = notif.name;
if ([notifName isEqualToString:kDidStartDownloadingStepImagesNotification])
{
// Assets download started
}
else if ([notifName isEqualToString:kDidDownloadAFileNotification])
{
// Download finished for an asset
NSString* fileName = notif.userInfo[kFileKey];
}
else if ([notifName isEqualToString:kDidReceiveAFaultNotification])
{
// Fault
MDFault* fault = notif.object;
}
else if ([notifName isEqualToString:kDidEndDownloadingStepImagesNotification])
{
// Assets Download finished
[self refreshTaskList];
}
}
// Then, handle
@objc func handleAssetDownloadNotification(notification: Notification) {
let notificationName = notification.name
if notificationName == Notification.Name.didStartDownloadingStepImages {
// Assets download started
}
else if notificationName == NSNotification.Name.didDownloadAFile {
// Download finished for an asset
let fileName = notification.userInfo?[kFileKey]
}
else if notificationName == NSNotification.Name.didReceiveAFault {
// Fault
let fault = notification.object as! MDFault
}
else if notificationName == NSNotification.Name.didEndGettingUserTasks {
// Finished filtering tasks
guard let taskManagerResults = Medable.axon().userTaskManager().results() else {
return
}
for (groupId, groupFilterData) in taskManagerResults {
guard let tasks = groupFilterData.availableTasks else { continue }
print("Group: " + groupId)
for task in tasks {
print("Task: " + task.name)
}
}
}
}
If you are interested in listening to all notifications, there is a convenient method to add an observer:
[MDRAssetDownloader addAllNotificationsObserver:self selector:@selector(handleAssetDownloaderNotification:)];
MDRAssetDownloader.addAllNotificationsObserver(self, selector: #selector(handleAssetDownloaderNotification(notification:)))
Required assets
- Only after you received a kDidEndDownloadingStepImagesNotification, it is safe to proceed to the next steps.
- This process is required at least once before login AND once after login, since the required assets might differ.
3. Getting surveys: The User Tasks Manager
Now we are ready to get the user's current available tasks. There is a task manager who checks the group the user is part of, the task scheduling/dependencies/start and end dates/past user responses/etc, and returns the currently available tasks.
// Get the user's "current tasks"
[[[Medable axon] userTaskManager] fetchUserTasks];
// Listen to these notifications to be informed about progress
extern NSString *kDidStartGettingUserTasksNotification;
extern NSString *kDidEndGettingUserTasksNotification;
extern NSString *kDidReceiveAFaultGettingUserTasksNotification;
// Consult documentation in MDRUserTaskManager's header file.
// Get the user's "current tasks"
Medable.axon().userTaskManager().fetchUserTasks()
// Listen to these notifications to be informed about progress
Notification.Name.didStartGettingUserTasks
Notification.Name.didEndGettingUserTasks
Notification.Name.didReceiveAFaultGettingUserTasks
// Consult documentation in MDRUserTaskManager's header file.
Here is an example of how to handle these notifications:
// First, you have to listen
- (ret)someMethod
{
NSNotificationCenter* nCenter = [NSNotificationCenter defaultCenter];
[nCenter addObserver:self selector:@selector(handleTaskManagerNotification:) name:kDidStartGettingUserTasksNotification object:nil];
[nCenter addObserver:self selector:@selector(handleTaskManagerNotification:) name:kDidEndGettingUserTasksNotification object:nil];
[nCenter addObserver:self selector:@selector(handleTaskManagerNotification:) name:kDidReceiveAFaultGettingUserTasksNotification object:nil];
}
// First, you have to listen
func someMethod() {
let nCenter = NotificationCenter.default
nCenter.addObserver(self, selector: #selector(handleTaskManagerNotification(notification:)), name: Notification.Name.didStartGettingUserTasks, object: nil)
nCenter.addObserver(self, selector: #selector(handleTaskManagerNotification(notification:)), name: Notification.Name.didEndGettingUserTasks, object: nil)
nCenter.addObserver(self, selector: #selector(handleTaskManagerNotification(notification:)), name: Notification.Name.didReceiveAFaultGettingUserTasks, object: nil)
}
// Then, handle
- (void)handleTaskManagerNotification:(NSNotification*)notif
{
NSString* notifName = notif.name;
if ([notifName isEqualToString:kDidStartGettingUserTasksNotification])
{
// Started fetching - "Loading Study..."
}
else if ([notifName isEqualToString:kDidEndGettingUserTasksNotification])
{
// Get the tasks objects
NSArray<MDRTask*>* tasks = notif.object;
// Update UI with tasks - make sure to run this in the main thread...
[[NSOperationQueue mainQueue] addOperationWithBlock:^
{
// The study instance is held by the MDRAxon singleton
NSLog(@"Study: %@", [Medable axon].userStudy);
// Currently available tasks
NSLog(@"Tasks: %@", tasks);
}];
}
else if ([notifName isEqualToString:kDidReceiveAFaultGettingUserTasksNotification])
{
// Get the fault and handle it
MDFault* fault = (MDFault*)notif.object;
NSLog(@"Fault: %@", fault);
}
}
// Then, handle
@objc func handleTaskManagerNotification(notification: Notification) {
let notificationName = notification.name
if notificationName == Notification.Name.didStartGettingUserTasks {
// Started fetching - "Loading Study..."
}
else if notificationName == NSNotification.Name.didEndGettingUserTasks {
// Finished filtering tasks
guard let taskManagerResults = Medable.axon().userTaskManager().results() else {
return
}
for (groupId, groupFilterData) in taskManagerResults {
guard let tasks = groupFilterData.availableTasks else { continue }
print("Group: " + groupId)
for task in tasks {
print("Task: " + task.name)
}
}
}
else if notificationName == NSNotification.Name.didReceiveAFaultGettingUserTasks {
// Get the fault and handle it
let fault = notification.object as! MDFault
}
}
If you are interested in listening to all of the notifications, there is a convenient method to add an observer:
[MDRUserTaskManager addAllNotificationsObserver:self selector:@selector(handleTaskManagerNotification:)];
MDRUserTaskManager.addAllNotificationsObserver(self, selector: #selector(handleTaskManagerNotification(notification:)))
Tasks Manager and anonymous/non-anonymous access
- Something important to mention is that
fetchUserTasks
works differently depending on if there is a logged in user or not at the moment of calling it.- If
fetchUserTasks
is called when there is a logged in user, the task manager returns the current available tasks for that user. - Conversely, if
fetchUserTasks
is called when there is no logged in user, the task manager tries to fetch the public tasks --read about public tasks in theEnrollment section. Success of this public tasks fetching depends on if the study is open or limited enrollment and if an invitation token has been provided as described in the Invitation token section.
- If
4. Taking surveys: ResearchKit
Once we get the currently available tasks from the Tasks Manager, we need to convert those to ResearchKit objects and pass them to ResearchKit. A method like this one, in your view controller class, would do the job:
- (void)startTask:(MDRTask*)task
{
if (!task) return;
// Store your last task. You're going to need this later (for responses).
self.lastTask = task;
// Convert Medable Task object (MDRTask) to a ResearchKit task object (id<ORKTask>).
id<ORKTask> rkTask = [task researchKitTask];
ORKTaskViewController* taskViewController = [[ORKTaskViewController alloc] initWithTask:rkTask taskRunUUID:nil];
taskViewController.title = task.name;
taskViewController.delegate = self; // ORKTaskViewControllerDelegate
taskViewController.showsProgressInNavigationBar = YES; // only works for ordered tasks (tasks without branching).
[self presentViewController:taskViewController animated:YES completion:nil];
}
func startTask(task: MDRTask) {
// Store your last task. You're going to need this later (for responses).
self.lastTask = task
// Convert Medable Task object (MDRTask) to a ResearchKit task object (id<ORKTask>).
guard let rkTask = task.researchKitTask() else { return }
DispatchQueue.main.async {
let taskViewController = ORKTaskViewController(task: rkTask, taskRun: nil)
taskViewController.title = task.name
taskViewController.delegate = self // ORKTaskViewControllerDelegate
taskViewController.showsProgressInNavigationBar = true // only works for ordered tasks (tasks without branching).
navController?.pushViewController(taskViewController, animated: true)
}
}
5. Uploading survey responses: The Response Uploader
After a participant finishes a task, you can upload the responses to the cloud. You can do it right from the ORKTaskViewControllerDelegate's method, like this:
#pragma mark - ORKTaskViewControllerDelegate
- (void)taskViewController:(ORKTaskViewController *)taskViewController
didFinishWithReason:(ORKTaskViewControllerFinishReason)reason
error:(nullable NSError *)error
{
switch (reason)
{
case ORKTaskViewControllerFinishReasonCompleted:
{
ORKTaskResult* taskResult = taskViewController.result;
// Last task
MDRTask* lastTask = self.lastTask;
// Send responses
[[[Medable axon] responseUploader] sendResponseToTask:lastTask result:taskResult];
}
break;
...
}
[self dismissViewControllerAnimated:YES completion:nil];
}
// Listen to these notifications to get noticed about the state and progress of the uploading of responses.
extern NSString *kCreatedTaskResponseNotification;
extern NSString *kStartedToSendResponseToTaskNotification;
extern NSString *kProgressToTaskResponseNotification;
extern NSString *kCompletedResponseToTaskNotification;
#pragma mark - ORKTaskViewControllerDelegate
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
switch reason {
...
case .completed:
let taskResult = taskViewController.result
guard let lastTask = self.lastTask else { return }
// Send Responses
Medable.axon().responseUploader().sendResponse(for: lastTask, result: taskResult)
...
}
navController?.dismiss(animated: true, completion: nil)
}
// Listen to these notifications to get noticed about the state and progress of the uploading of responses.
Notification.Name.createdTaskResponse
Notification.Name.startedToSendResponseToTask
Notification.Name.progressToTaskResponse
Notification.Name.completedResponseToTask
Here is an example of how to handle these notifications:
// First, you have to listen
- (ret)someMethod
{
NSNotificationCenter* nCenter = [NSNotificationCenter defaultCenter];
[nCenter addObserver:self selector:@selector(handleResponseUploaderNotification:) name:kCreatedTaskResponseNotification object:nil];
[nCenter addObserver:self selector:@selector(handleResponseUploaderNotification:) name:kStartedToSendResponseToTaskNotification object:nil];
[nCenter addObserver:self selector:@selector(handleResponseUploaderNotification:) name:kProgressToTaskResponseNotification object:nil];
[nCenter addObserver:self selector:@selector(handleResponseUploaderNotification:) name:kCompletedResponseToTaskNotification object:nil];
}
// First, you have to listen
func someMethod() {
let nCenter = NotificationCenter.default
nCenter.addObserver(self, selector: #selector(handleResponseUploaderNotification(notification:)), name: Notification.Name.createdTaskResponse, object: nil)
nCenter.addObserver(self, selector: #selector(handleResponseUploaderNotification(notification:)), name: Notification.Name.startedToSendResponseToTask, object: nil)
nCenter.addObserver(self, selector: #selector(handleResponseUploaderNotification(notification:)), name: Notification.Name.progressToTaskResponse, object: nil)
nCenter.addObserver(self, selector: #selector(handleResponseUploaderNotification(notification:)), name: Notification.Name.completedResponseToTask, object: nil)
}
// Then, handle
- (void)handleResponseUploaderNotification:(NSNotification*)notif
{
NSString* notifName = notif.name;
if ([notifName isEqualToString:kCreatedTaskResponseNotification])
{
// Created a task response object - these are related to step responses, and need to be created first.
NSString* taskResponseId = notif.userInfo[kTaskResponse];
}
else if ([notifName isEqualToString:kStartedToSendResponseToTaskNotification])
{
// Started to send the data - responses for that with Id:
NSString* taskId = notif.userInfo[kTask];
}
else if ([notifName isEqualToString:kProgressToTaskResponseNotification])
{
// Progress changed - sent response for taskId / step. NOT USED FOR NOW...
NSString* taskId = notif.userInfo[kTask];
MDRStep* step = notif.userInfo[kStep];
NSNumber* progress = notif.userInfo[kProgress]; // 0.0f to 1.0f
MDFault* fault = notif.userInfo[kFaultKey];
if (fault)
{
// Handle fault
}
}
else if ([notifName isEqualToString:kGeneratedConsentPDFNotification])
{
// Generated Consent PDF
NSData* consentPDFData = notif.object;
}
else if ([notifName isEqualToString:kCompletedResponseToTaskNotification])
{
// Response upload completed
NSString* taskId = notif.userInfo[kTask];
MDFault* fault = notif.userInfo[kFaultKey];
if (fault)
{
// Handle fault
}
}
}
// Then, handle
@objc func handleResponseUploaderNotification(notification: Notification) {
let notificationName = notification.name
if notificationName == Notification.Name.createdTaskResponse {
// Created a task response object - these are related to step responses, and need to be created first.
let taskResponseId = notification.userInfo?[kTaskResponse]
}
else if notificationName == Notification.Name.startedToSendResponseToTask {
// Started to send the data - responses for that with Id:
let taskId = notification.userInfo?[kTask]
}
else if notificationName == Notification.Name.progressToTaskResponse {
// Progress changed - sent response for taskId / step. NOT USED FOR NOW...
//NSDictionary *info = @{ kTask: pendingResponse.taskId, kProgress: @(1.0f) };
let taskId = notification.userInfo?[kTask] as? MDRTask
let step = notification.userInfo?[kStep] as? MDRStep
let progress = notification.userInfo?[kProgress] as? NSNumber // 0.0f to 1.0f
if let fault = notification.userInfo?[kFaultKey] as? MDFault {
// Handle fault
}
}
else if notificationName == Notification.Name.generatedConsentPDF {
// Generated Consent PDF
let consentPDFData = notification.object as? Data
}
else if notificationName == Notification.Name.completedResponseToTask {
// Response upload completed
let taskId = notification.userInfo?[kTask] as? MDRTask
if let fault = notification.userInfo?[kFaultKey] as? MDFault {
// Handle fault
}
}
}
If you are interested in listening to all these notifications, there is a convenient method to add an observer:
[MDRResponseUploader addAllNotificationsObserver:self selector:@selector(handleResponseUploaderNotification:)];
MDRResponseUploader.addAllNotificationsObserver(self, selector: #selector(handleResponseUploaderNotification(notification:)))
Updated 4 months ago