Skip to content

Instantly share code, notes, and snippets.

@sye8
Last active December 11, 2020 14:28
Show Gist options
  • Save sye8/5472c425439e134ecd4afeee0957e38b to your computer and use it in GitHub Desktop.
Save sye8/5472c425439e134ecd4afeee0957e38b to your computer and use it in GitHub Desktop.
Research Kit + Apple Watch Tutorial (Swift 3) - Part 1: Obtaining Consent and Generating PDF

Research Kit + Apple Watch Tutorial

This is a series of tutorials that will walkthrough:

Part 1: Obtaining Consent and Generating PDF

ResearchKit is an open source framework introduced by Apple that allows researchers and developers to create powerful apps for medical research. Easily create visual consent flows, real-time dynamic active tasks, and surveys using a variety of customizable modules that you can build upon and share with the community. And since ResearchKit works seamlessly with HealthKit, researchers can access even more relevant data for their studies — like daily step counts, calorie use, and heart rate.

--- ResearchKit.org

As Apple described, Research Kit is a handy tool to create medical research apps, which would most probably involve collecting user data. Before collecting any data, as a researcher, you would need to obtain consent from the user by explaining to them what will happen to the data you collected, and kindly ask the users to sign the consent.

This part of the tutorial will walk through how to "easily create visual consent flows", ask for Health data authorization, create a passcode to lock the app and generate a pdf version of the consent document.

Note that this tutorial assumes that you have some prior experiences in Swift

Note that this tutorial is written in Swift 3

So, let's begin

1. Let's tell a story...

Go ahead and open up your Xcode, create a new Xcode project, and select Single View App from iOS. Give your project a nice name and store it at a place you promise you won't forget.

After creating a new project, delete ViewController.swift and open up Main.storyboard.

Now, for the research, we would only need to obtain the users' consent once as they join the study. If they have consented, we would like to show them the tasks screen directly.

So, in Main.storyboard, add 2 more view controllers to the screen.

storyboard0

The layout of the storyboard is a matter of religion, not programming

And on one of the view controllers you just added, add a Join Study button.

joinstudy

Also, you may want to add some constaints so Xcode doesn't complain

From this point on, we will call the view controller that you just added a button to Consent View Controller and the blank one Tasks View Controller, as it will be used for buttons for tasks in the later parts of the series.

Good. Now, we need some way for the user to reach the button from the Initial View Controller (the one with an arrow pointing into it from the void). To do that, we will create a custom segue from to Consent View Controller.

Segue \ˈsɛɡweɪ\

A segue refers to a transition from one scene to another in a storyboard

--- App Development with Swift

While holding the control key, click and drag the yellow icon from the Initial View Controller to Consent View Controller. You will see a small window like the one shown below.

manualsegue0

Select Custom. You will now see an arrow pointing from Initial View Controller to Consent View Controller like below.

manualsegue1

Repeat the same for Tasks View Controller. The reason to choose custom segues here is that we want the next view controller to be presented directly without any animation. We will program those soon, but before we do so, those segues need to be named for us to refer to them in our code.

Select the segue by clicking on the arrow, and give them Identifiers like in the screenshots below.

manualsegue2

manualsegue3

I call them toConsent and toTasks respectively because, well, they lead to Consent View Controller and Tasks View Controller respectively

Create a new group called Intro, and in the group, create IntroViewController.swift and IntroSegue.swift and those will contain code that decides what to show to the user and how to do so.

In IntroSegue.swift, add the following code:

import UIKit

class IntroSegue: UIStoryboardSegue{
    
    override func perform() {
        let controllerToReplace = source.childViewControllers.first
        let destinationControllerView = destination.view
        
        destinationControllerView?.translatesAutoresizingMaskIntoConstraints = true
        destinationControllerView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        destinationControllerView?.frame = source.view.bounds
        
        controllerToReplace?.willMove(toParentViewController: nil)
        source.addChildViewController(destination)
        
        source.view.addSubview(destinationControllerView!)
        controllerToReplace?.view.removeFromSuperview()
        
        destination.didMove(toParentViewController: source)
        controllerToReplace?.removeFromParentViewController()
    }
    
}

This code will instruct our custom segues to replace the old view controller with the new one, without any animation (did I mention that before?)

Now, go back to Main.storyboard and set the Class of the two custom segues to IntroSegue, shown below:

manualsegue4

Then, in IntroViewController.swift, add the following code:

import UIKit

class IntroViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        toConsent()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    func toConsent(){
        performSegue(withIdentifier: "toConsent", sender: self)
    }
    
    func toTasks(){
        performSegue(withIdentifier: "toTasks", sender: self)
    }
}

At this stage, we will directly head off to ConsentViewController (as seen in viewDidLoad())

Important: in performSegue(withIdentifier: ,sender:) use the identifier you gave to the segues)

And to apply the code to Intro View Controller, set its Class to IntroViewController.swift, as shown below:

storyboard1

Build and run your app (Either in simulator or on an actual phone), you will be shown Join Study button on app launch:

app0

Yay!

"But", you say, "The button doesn't work." Well, we will take care of that in section 2

2. The Consent Task

Hmm...seems like I forgot something...

This is a ResearchKit tutorial...ah, right! We need to import ResearchKit

Since ResearchKit is an open source project, you would have to clone it from github first. To do that, you can either go directly here, Download ZIP and unzip it.

Or you can open Terminal and type:

git clone https://github.com/ResearchKit/ResearchKit.git

What do you mean git command not found? Oh you don't have git? Then you can install Homebrew and use it to install git:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install git

Then, go into the ResearchKit folder and drag and drop the ResearchKit.xcodeproj into your project:

importresearchkit0

Yup, I named my project ResearchKitConsent

Then, open your project setting (by clicking on the xcodeproject icon with your project name), scroll to the bottom of General, and in embedded binaries section, click +, and add ResearchKit.frameworkiOS:

importresearchkit1

After adding, the embedded binaries section should look like this:

importresearchkit2

Good. Now, create a new group called Consent, create ConsentViewController.swift, and add the following code:

import ResearchKit

class ConsentViewController: UIViewController{
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
}

To make the button actually work, we will set the Class of Consent View Controller to ConsentViewController. (It is a mouthful to read, but matching names makes everyone's life easier).

consentvc0

Then, open Assistant Editor "assistanteditor" (It should show ConsentViewController.swift. If not, select the file manually), and while holding control key, click the button and drag into your code. Select connection type as Action and give it an appropriate name:

consentvc1

I call it joinButtonTapped because, well, it does something when I tap Join Study

But before we add anything to the function stub, we will create our consent document. Create a file called ConsentDocument.swift under group Consent. Add the following code:

import ResearchKit

public var ConsentDocument: ORKConsentDocument{
    
    let consentDocument = ORKConsentDocument()
    consentDocument.title = "Research Study Consent Form" //Or other title you prefer
    
    let sectionTypes: [ORKConsentSectionType] = [
        .overview,
        .dataGathering,
        .privacy,
        .dataUse,
        .timeCommitment,
        .studySurvey,
        .studyTasks,
        .withdrawing
    ]
    
    //Section contents
    
    //Add sections
    
    //Signature
    
    return consentDocument
}

This part of the code tells your app what sections your consent document will contain

Then replace //Section contents with a String[] of the same size, with each element of the list initialized to a string containing corresponding text for each section. For example:

let text = [
        "Section 1: Welcome. This study is about...",
        "Section 2: Data Gathering. This study will collect data from your Apple Watch...",
        "Section 3: Privacy. We value your privacy...",
        "Section 4: Data Use. The data collected will be used for...",
        "Section 5: Time Commitment. This study will take you roughly...",
        "Section 6: Study Survey. For this study, you will need to fill out a survey...",
        "Section 7: Study Tasks. You will be requested to do these tasks...",
        "Section 8: Withdrawing. To withdraw from the study..."
]

You can always use lorem ipsum as placeholder

To add the sections to the document, replace //Add sections with the code below:

 for sectionType in sectionTypes {
        let section = ORKConsentSection(type: sectionType)
        
        let localizedText = NSLocalizedString(text[sectionTypes.index(of: sectionType)!], comment: "")
        let localizedSummary = localizedText.components(separatedBy: ".")[0] + "."
        
        section.summary = localizedSummary
        section.content = localizedText
        
        if consentDocument.sections == nil {
            consentDocument.sections = [section]
        } else {
            consentDocument.sections!.append(section)
        }
}

Good old for loop

In order to render the consent document into a pdf, ResearchKit requires the signature field to have a title. (Otherwise you will get a SIGABRT when attempting to generate a PDF. Not sure why this is not included in most of the ResearchKit tutorials). So, replace //Signature with:

consentDocument.addSignature(ORKConsentSignature(forPersonWithTitle: "Participant", dateFormatString: nil, identifier: "ConsentDocumentParticipantSignature"))

We will call our participant "Participant". Perfect!

Next, we will use our ConsentDocument to create a Consent Task. Create ConsentTask.swift under Consent group.

Our ConsentTask will be an ORKOrderedTask:

The ORKOrderedTask class implements all the methods in the ORKTask protocol and represents a task that assumes a fixed order for its steps.

In the ResearchKit framework, any simple sequential task, such as a survey or an active task, can be represented as an ordered task.

--- ResearchKit Documentation

And thus our task will consist of a list of sequential tasks:

import ResearchKit

public var ConsentTask: ORKOrderedTask{
    
    var steps = [ORKStep]()

    //Visualization
    //This step is where Apple will help us present the consent document with animations
    
    //Request Health Data
    //We will need the user to grant us access to health data for collection
    
    //Review & Sign
    //The whole document will be shown to the user and the user will draw their signature on the touch screen
    
    //Passcode/TouchID Protection
    //Because it is personal data
    
    //Completion
    //A thank you message
    
    return ORKOrderedTask(identifier: "ConsentTask", steps: steps)
}

As Apple mentioned, ResearchKit provides easy visualization of the consent document. So, we will replace

//This step is where Apple will help us present the consent document with animations

with

let consentDocument = ConsentDocument
let visualConsentStep = ORKVisualConsentStep(identifier: "VisualConsentStep", document: consentDocument)
steps += [visualConsentStep]

replace

//The whole document will be shown to the user and the user will draw their signature on the touch screen

with

let signature = consentDocument.signatures!.first!
let reviewConsentStep = ORKConsentReviewStep(identifier: "ConsentReviewStep", signature: signature, in: consentDocument)
reviewConsentStep.text = "Review the consent form."
reviewConsentStep.reasonForConsent = "Consent to join study"
steps += [reviewConsentStep]

and replace

//A thank you message

with

let completionStep = ORKCompletionStep(identifier: "CompletionStep")
completionStep.title = "Welcome aboard."
completionStep.text = "Thank you for joining this study."
steps += [completionStep]

At this stage, we have a functional ConsentTask and let's try it out! Remember joinButtonTapped(_ sender: UIButton) from ConsentViewController? Add the code below:

@IBAction func joinButtonTapped(_ sender: UIButton) {
    let taskViewController = ORKTaskViewController(task: ConsentTask, taskRun: nil)
    taskViewController.delegate = self
    present(taskViewController, animated: true, completion: nil)
}

Let's build and....

...wait, there is an error at taskViewController.delegate = self

Ah, right, our ConsentViewController is not an ORKTaskViewControllerDelegate yet. Our ConsentViewController will need to extend ORKTaskViewControllerDelegate in order to tell taskViewController what to do when we finish our ConsentTask.

So, add the following code to ConsentViewController.swift:

extension ConsentViewController: ORKTaskViewControllerDelegate{
    func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
        dismiss(animated: true, completion: nil) //Dismisses the view controller when we finish our consent task 
    }
}

Now, build, run, and click the Join Study button:

consent0

Yay! Also, nice animation! Thanks Apple!

3. Can I have your Health data please?

Remember in ConsentTask.swift we haven't replace //We will need the user to grant us access to health data for collection right?

For this step in the task, we will first enable HealthKit for our app. Go to Capabilities of project settings and enable HealthKit:

healthkitcapability

And we will add some description strings to Info.plist:

healthkitdescription

Apple will not let us access health data unless we explain to users explicitly what we will do with it

Then, we will have to create our own ORKActiveStep in order to show the Health authorization screen.

Note that usually ORKActiveStep will be used to collect data in real time. Here we will use it because ORKInstructionStep uses a Get Started button, which may confuse the user

Create HealthDataAuthStep.swift under Consent group and add the following code (suppose we are collecting heart rate data):

import HealthKit

import ResearchKit

class HealthDataAuthStep: ORKActiveStep {
    
    let healthKitStore = HKHealthStore()
    
    let healthDataItemsToRead: Set<HKObjectType> = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!]
    let healthDataItemsToWrite: Set<HKSampleType> = []
    
    override init(identifier: String) {
        super.init(identifier: identifier)
        
        title = NSLocalizedString("Health Data", comment: "")
        text = NSLocalizedString("On the next screen, you will be prompted to grant access to read your heart rate data for this study.\n\nIf you have declined authorization before and wish to grant access, head to\n\n Settings -> Privacy -> Health\n\nto authorize.", comment: "")
    }
    
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func getHealthAuthorization(_ completion: @escaping (_ success: Bool, _ error: NSError?) -> Void) {
        HKHealthStore().requestAuthorization(toShare: healthDataItemsToWrite, read: healthDataItemsToRead){ (success, error) -> Void in
            completion(success, error as NSError?)
        }
    }
}

There are also other data types avaliable from HealthKit

And because we just coded a custom ORKActiveTask, we would also need an ORKActiveStepViewController to go with it. Create HealthDataAuthStepViewController.swift under Consent group and add the following code:

import HealthKit

import ResearchKit

class HealthDataAuthStepViewController: ORKActiveStepViewController {
    
    var healthDataStep: HealthDataAuthStep? {
        return step as? HealthDataAuthStep
    }
    
    override func goForward() {
        healthDataStep?.getHealthAuthorization(){succeeded, _ in
            guard succeeded || (TARGET_OS_SIMULATOR != 0) else { return }
            OperationQueue.main.addOperation {
                super.goForward()
            }
        }
    }
    
}

In ConsentViewController.swift add the following method to the extension so our app will use the view controller we just created:

func taskViewController(_ taskViewController: ORKTaskViewController, viewControllerFor step: ORKStep) -> ORKStepViewController? {
    if step is HealthDataAuthStep {
        let healthStepViewController = HealthDataAuthStepViewController(step: step)
        return healthStepViewController
    }
    return nil
}

And we will finally replace //We will need the user to grant us access to health data for collection with:

let healthDataStep = HealthDataAuthStep(identifier: "Health")
steps += [healthDataStep]

Now you have a glipse of what need to happen so we can use "easy consent flow"

Build and run the app. The consent task will now contain a step that asks for Health data authorization.

Note that this window only appears once. If the user declines access to health data here, they will need to go to Settings -> Privacy to grant access

healthauth0

healthauth1

4. User's eyes only

The other step that we have not replaced //Because it is personal data with proper passcode protection step. So, here we go:

let passcodeStep = ORKPasscodeStep(identifier: "Passcode")
passcodeStep.text = "Now you will create a passcode to identify yourself to the app and protect access to information you've entered."
steps += [passcodeStep]

You probably realized if this is included as a seperate section in this tutorial, it won't be that easy. You are right.

For the passcode step to work, we would first need to tell our app what to do. Open up AppDelegate.swift and add the following extension to make it a ORKPasscodeDelegate:

extension AppDelegate: ORKPasscodeDelegate {
    func passcodeViewControllerDidFinish(withSuccess viewController: UIViewController) {
        containerViewController?.contentHidden = false
        viewController.dismiss(animated: true, completion: nil)
    }
    
    func passcodeViewControllerDidFailAuthentication(_ viewController: UIViewController) {
    }
}

We would also need to lock the app when launching or returning to foreground if there is a passcode stored. So, add the following method to AppDelegate:

func lockApp() {
    /*
     * Only lock the app if there is a stored passcode and a passcode
     * controller isn't already being shown.
     */
    guard ORKPasscodeViewController.isPasscodeStoredInKeychain() && !(containerViewController?.presentedViewController is ORKPasscodeViewController) else { return }
        
    window?.makeKeyAndVisible()
        
    let passcodeViewController = ORKPasscodeViewController.passcodeAuthenticationViewController(withText: "Welcome back ", delegate: self)
    containerViewController?.present(passcodeViewController, animated: false, completion: nil)
}

Since we will be hiding the content, go to IntroViewController and add:

var contentHidden = false {
    didSet {
        guard contentHidden != oldValue && isViewLoaded else { return }
        childViewControllers.first?.view.isHidden = contentHidden
    }
}

And the app will need to know when to lock the app. import ResearchKit and add the following to AppDelegate.swift:

var containerViewController: IntroViewController? {
    return window?.rootViewController as? IntroViewController
}

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    let standardDefaults = UserDefaults.standard
    if standardDefaults.object(forKey: "TutorialFirstRun") == nil {
        ORKPasscodeViewController.removePasscodeFromKeychain()
        standardDefaults.setValue("TutorialFirstRun", forKey: "TutorialFirstRun")
    }
        
    // Appearance customization
    let pageControlAppearance = UIPageControl.appearance()
    pageControlAppearance.pageIndicatorTintColor = UIColor.lightGray
    pageControlAppearance.currentPageIndicatorTintColor = UIColor.black
        
    return true
}

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
    lockApp()
    return true
}

func applicationDidEnterBackground(_ application: UIApplication) {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    if ORKPasscodeViewController.isPasscodeStoredInKeychain() {
        // Hide content so it doesn't appear in the app switcher.
        containerViewController?.contentHidden = true
    }
}

func applicationWillEnterForeground(_ application: UIApplication) {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    lockApp()
}

Build and run the app. You will see a passcode step after you draw your signature:

passcode0

When the app is launched or brought to foreground the next time, it will ask for the passcode:

passcode1

4. Has the user consented?

The fact that there is a passcode store also presents us a way of knowing if the user has consented or not.

Open IntroViewController and change viewDidLoad() to lead the user directly to Tasks View Controller on launch if they have consented:

import ResearchKit

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    if ORKPasscodeViewController.isPasscodeStoredInKeychain(){
        toTasks()
    }else{
        toConsent()
    }
}

We would also need to lead user to Tasks View Controller when they complete the consent task.

Add the following code to IntroViewController:

@IBAction func unwindToTasks(_ segue: UIStoryboardSegue){
    toTasks()
}

Then, open up Main.storyboard, select Consent View Controller and, while holding control key, click and drag the yellow view controller icon to the exit icon. Select unwindToTasks::

unwindtotasks0

Give the unwind segue identifier unwindToTasks:

unwindtotasks1

And in ConsentViewController: ORKTaskViewControllerDelegate, modify

taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?)

so the unwind segue is performed when consent task is complete:

func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
    switch reason{
        case .completed:
            performSegue(withIdentifier: "unwindToTasks", sender: nil)
        case .discarded, .failed, .saved:
            dismiss(animated: true, completion: nil)
    }       
}

The passcode store also presents us an easy way for the user to leave our study. Create a new group called Tasks and in the group, create TasksViewController.swift. Initialize the file with the standard UIViewController stub:

import UIKit

import ResearchKit

class TasksViewController: UIViewController{
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

Assign TasksViewController as Tasks View Controller's custom class in Main.storyboard, and add a button Leave Study:

tasksvc0

Then, while holding control key, click and drag Leave Study button to Consent View Controller and select Show segue:

tasksvc1

We shall call it returnToConsent:

tasksvc2

Next, give the button some action. Open Assistant Editor "assistanteditor", and add an Action connection to TasksViewController. We shall name it leaveButtonTapped:

tasksvc3

In leaveButtonTapped(_ sender: UIButton), add the following code which deletes the passcode store and present Consent View Controller:

ORKPasscodeViewController.removePasscodeFromKeychain()
performSegue(withIdentifier: "returnToConsent", sender: nil)

This way, the user will leave the study, and if they wish to join again, they will be presented the consent document and requested to create a passcode.

Build, run and try out the cycle. Yay!

5. What if I want to print the consent document?

Luckily, ResearchKit not only has the tool to visualize the consent process, but also the tool to save the signed consent document as a PDF.

Modify

taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?)

in ConsentViewController: ORKTaskViewControllerDelegate to:

func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
    switch reason{
        case .completed:
            let signatureResult: ORKConsentSignatureResult = taskViewController.result.stepResult(forStepIdentifier: "ConsentReviewStep")?.firstResult as! ORKConsentSignatureResult
            let consentDocument = ConsentDocument.copy() as! ORKConsentDocument
            signatureResult.apply(to: consentDocument)
            consentDocument.makePDF{ (data, error) -> Void in
                var docURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last
                docURL = docURL?.appendingPathComponent("consent.pdf")
                try? data?.write(to: docURL!, options: .atomicWrite)
            }
            performSegue(withIdentifier: "unwindToTasks", sender: nil)
        case .discarded, .failed, .saved:
            dismiss(animated: true, completion: nil)
    }
}

TL:DR: Apply the signature to the document and save as a PDF

Now, add a button Consent Document to Tasks View Controller and create an action connection in Tasks View Controller:

tasksvc4

Then add the following code to the button handler:

let taskViewController = ORKTaskViewController(task: consentPDFViewerTask(), taskRun: nil)
taskViewController.delegate = self
present(taskViewController, animated: true, completion: nil)

At this point, Xcode may throw you some errors. This is because we haven't create the PDF task nor its delegate yet.

Add the following code to TasksViewController to create a PDF task that will open our consent document:

func consentPDFViewerTask() -> ORKOrderedTask{
    var docURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last
    docURL = docURL?.appendingPathComponent("consent.pdf")
    let PDFViewerStep = ORKPDFViewerStep.init(identifier: "ConsentPDFViewer", pdfURL: docURL)
    PDFViewerStep.title = "Consent"
    return ORKOrderedTask(identifier: String("ConsentPDF"), steps: [PDFViewerStep])
}

and the following extension to tell the app what to do when the user closes the PDF viewer:

extension TasksViewController: ORKTaskViewControllerDelegate{
    func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
        taskViewController.dismiss(animated: true, completion: nil)
    }
}

Build and run. Complete the consent, and tap Consent Document:

pdf0 pdf1

Voilà!

And we are done with consent. Yay!

Until I see you in Part 2, farewell!


The completed tutorial project is avaliable here

Referenced Projects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment