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.

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