You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
909 lines
43 KiB
Markdown
909 lines
43 KiB
Markdown
# Introducción al Uso de ResearchKit: Innovación en Ensayos Clínicos con Tecnología Wearable
|
|
|
|
ResearchKit ha demostrado ser una herramienta útil en la recolección de datos para ensayos clínicos, permitiendo gestionar actividades como el consentimiento informado, el manejo de historias clínicas, y la realización de tareas específicas. Esto resalta la importancia de aprender a utilizar los recursos que ResearchKit ofrece para facilitar el desarrollo de software en el ámbito médico.
|
|
|
|
### Creación de proyecto
|
|
|
|
El primer paso consiste en crear un proyecto nuevo en Xcode, asegurando que esté actualizado a la última versión. Al abrir Xcode, se desplegará un menú en el que debe seleccionarse la opción "Create New Project...".
|
|
|
|
![](imagenes/createP.png)
|
|
|
|
Luego, al hacer clic, se abrirá una ventana con opciones de proyecto. En este caso, debe seleccionarse "App de iOS" ya que la aplicación estará destinada a iPhone y iPad. A continuación, se asignará un nombre, se definirá el tipo de interfaz, el lenguaje a utilizar, entre otros datos, además de especificar la ubicación del proyecto.
|
|
|
|
![](imagenes/appios.png)
|
|
![](imagenes/nameApp.png)
|
|
![](imagenes/ubiApp.png)
|
|
|
|
Una vez completada la creación del proyecto, se mostrará el entorno de trabajo de Xcode, que permite visualizar las áreas involucradas en el desarrollo de una app para iOS.
|
|
|
|
![](imagenes/entornoXcode.png)
|
|
|
|
Con el proyecto listo, el siguiente paso es descargar el framework de ResearchKit, la parte central de este repositorio. Para ello, se descargará el repositorio en formato ZIP desde [GitHub](https://github.com/ResearchKit/ResearchKit), seleccionando el botón **Code** y luego **Download ZIP**.
|
|
|
|
### Integración de Frameworks
|
|
|
|
![](imagenes/descargaZIP.png)
|
|
|
|
Después de descargar el repositorio en ZIP, se puede acceder al framework. Para importarlo, basta con arrastrar el archivo *ResearchKit.xcodeproj* al navegador de proyectos, ubicándolo justo debajo del proyecto general.
|
|
|
|
![](imagenes/ArrasRK.png)
|
|
![](imagenes/ArrasRK2.png)
|
|
|
|
Para activar los frameworks de ResearchKit, se debe dirigir al navegador de proyectos, seleccionar el proyecto principal y, en el único target disponible, desplazarse hasta el menú *Frameworks, Libraries, and Embedden Content*. En este menú, seleccionando el botón "+", se eligen los frameworks de ResearchKit.
|
|
|
|
![](imagenes/frameRK1.png)
|
|
![](imagenes/frameRK2.png)
|
|
|
|
Estos tres frameworks permiten que la aplicación funcione con las actividades, formularios y tareas que ofrece esta herramienta de Apple.
|
|
|
|
### Estructura de la aplicación
|
|
|
|
Para comenzar la creación de la aplicación, en el archivo *Main.storyboard* es necesario crear dos controladores de vista adicionales: uno para el inicio, otro para el consentimiento y un tercero para las actividades.
|
|
|
|
Para agregar estos controladores de vista, se abre el archivo *Main* en el navegador de proyectos. En la esquina superior derecha aparecerá un signo "+", en el cual se hace clic y se escribe **View Controller**. Luego, se arrastra dos veces hacia la ventana de visualización para crear los controladores de vista adicionales.
|
|
|
|
![](imagenes/vc1.png)
|
|
![](imagenes/vc2.png)
|
|
![](imagenes/vc3.png)
|
|
![](imagenes/vc4.png)
|
|
![](imagenes/vc5.png)
|
|
|
|
En uno de los controladores de vista, se necesita añadir un botón para "unirse al estudio", ya que antes de comenzar a recolectar datos es esencial contar con el consentimiento de los usuarios. Para añadir el botón, se hace clic en el "+" de la esquina superior, y se arrastra el elemento "button" hacia el controlador de vista deseado.
|
|
|
|
![](imagenes/bt1.png)
|
|
![](imagenes/bt2.png)
|
|
|
|
A continuación, el botón puede posicionarse, y su texto, tamaño, colores y otros elementos se modifican desde el inspector, ubicado en la esquina superior derecha.
|
|
|
|
![](imagenes/bt3.png)
|
|
|
|
Para mantener la posición del botón en diferentes dispositivos, se añaden *constraints*. Esto se logra haciendo clic en el icono de un cuadro con cotas, ubicado en la parte inferior, para acceder y configurar las restricciones de acuerdo con las necesidades, como se muestra en la imagen siguiente:
|
|
|
|
![](imagenes/constrains.png)
|
|
|
|
Es necesario asignar una clase a cada controlador de vista: **IntroViewController**, **ConsentViewController** y **TaskViewController**. El controlador principal, donde se encuentra la flecha de navegación, es el *IntroViewController*; el que contiene el botón para unirse al estudio es el *ConsentViewController*, y el controlador en blanco es el *TaskViewController*. Los identificadores se asignan seleccionando cada controlador de vista, luego haciendo clic en el menú del inspector en el icono de identificación y modificando la clase con el nombre correspondiente.
|
|
|
|
![](imagenes/ident.png)
|
|
|
|
Cuando cada controlador ha sido asignado a su clase correspondiente, el menú de controladores de vista se verá de la siguiente manera:
|
|
|
|
![](imagenes/identAll.png)
|
|
|
|
De esta manera, se tiene la base estructurada para el desarrollo de la aplicación.
|
|
|
|
Para la unión de los diferentes controladores de vista se hará uso de segues o transiciones entre vistas de la aplicación, por ello, desde **IntroViewController** se arrastra con click derecho la *Vista* a los 2 ViewController restantes y al soltar, se selecciona el segue manual *Custom*.
|
|
|
|
![](imagenes/segue1.png)
|
|
![](imagenes/segue2.png)
|
|
|
|
Se realiza para los 2 ViewControllers y quedaría de la siguiente manera:
|
|
|
|
![](imagenes/seguef.png)
|
|
|
|
Los segues de igual manera deben tener un identificador, por ello, al selecionar un segue, se abrirá el Inspector y los identificadores a poner será *toConsent* y *toTasks* para cada uno de los segues creados anteriormente.
|
|
|
|
![](imagenes/identSegue.png)
|
|
|
|
Para cada controlador de vistas se necesita crear un archico de tipo **Cocoa Touch Class** con el mismo nombre que la clase, para crear un archivo de este tipo, en la pestaña *File* hay una opción de *New* y se selecciona la opción *File from Template...*.
|
|
|
|
![](imagenes/newfile.png)
|
|
|
|
Se desplegará un menú y se seleccionará la opción Cocoa Touch Class, asegurandose que esté en iOS.
|
|
|
|
![](imagenes/type.png)
|
|
|
|
Pedirá una clase, se pone las creadas en los controladores de vista con subclase UIViewController.
|
|
|
|
![](imagenes/type2.png)
|
|
|
|
y se guardará en la misma ruta del archivo, como viene predeterminado.
|
|
|
|
![](imagenes/savefile.png)
|
|
|
|
al termino de los 3 archivos, deberá lucir de la siguiente forma:
|
|
|
|
![](imagenes/allVC.png)
|
|
|
|
### Direccionamiento de los segues
|
|
|
|
Además de los ViewController se creará un archivo de igual tipo Cocoa Touch Class, sin embargo, este llevará por nombre **IntroSegue** y la subclase será un **UIStoryboardSegue**, y al igual que los demás lo guardamos en la carpeta del proyecto.
|
|
|
|
![](imagenes/Intro.png)
|
|
|
|
Dentro de este archivo, colocaremos el siguiente código, el cuál implementa un segue que reemplaza un controlador hijo por otro de manera completamente personalizada, eliminando el controlador antiguo de la jerarquía y añadiendo el nuevo. Esto es útil si quieres hacer transiciones en las que no se empuje o presente una nueva vista de manera estándar, sino que se reemplace un controlador específico dentro de una vista ya existente.
|
|
|
|
```
|
|
import UIKit
|
|
|
|
class IntroSegue: UIStoryboardSegue {
|
|
|
|
override func perform() {
|
|
let controllerToReplace = source.children.first
|
|
let destinationControllerView = destination.view
|
|
|
|
destinationControllerView?.translatesAutoresizingMaskIntoConstraints = true
|
|
destinationControllerView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
destinationControllerView?.frame = source.view.bounds
|
|
|
|
controllerToReplace?.willMove(toParent: nil)
|
|
source.addChild(destination)
|
|
|
|
source.view.addSubview(destinationControllerView!)
|
|
controllerToReplace?.view.removeFromSuperview()
|
|
|
|
destination.didMove(toParent: source)
|
|
controllerToReplace?.removeFromParent()
|
|
}
|
|
}
|
|
```
|
|
y dentro del inspector, en los 2 segues que están en el Main.storyboard, se asigna la clase como *IntroSegue*
|
|
|
|
![](imagenes/segues1.png)
|
|
![](imagenes/segues2.png)
|
|
|
|
y para asegurarnos que se realicen las transiciones automáticas hacia los diferentes ViewController a través de los segues ya definidos, en el archivo de IntroViewController se realizará el codigo siguiente:
|
|
|
|
```
|
|
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)
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
Es momento de correr el programa, para verificar que todo este funcionando bien y esté libre de errores, debería abrir la pantalla del consentimiento.
|
|
|
|
|
|
![](imagenes/join.png)
|
|
|
|
### Uso del botón **Unirse al estudio**
|
|
|
|
Para darle funcionalidad al botón anteriormente creado, se dirigirá al archivo Main.storyboard y se arrastrará con click derecho del mouse, del botón hasta el archivo ConsentViewController, en la parte inferior; ya que ahí es donde se podráque es lo que se quiere realizar al presionar el botón.
|
|
|
|
|
|
![](imagenes/button1.png)
|
|
|
|
para identificarlo se le pondra de nombre **joinButtonTapped** y el tipo será un **UIButton**
|
|
|
|
![](imagenes/button2.png)
|
|
|
|
El código que se agregará es el siguiente:
|
|
|
|
```
|
|
@IBAction func joinButtonTapped(_ sender: UIButton) {
|
|
}
|
|
|
|
```
|
|
|
|
Para crear el documento de consentimiento, es necesario crear un archivo de tipo Swift File, con el nombre **ConsentDocument**, donde se podrán poner todo lo relacionado y que se requiera para que se visualice el consentimiento y el usuario pueda firmarlo.
|
|
|
|
![](imagenes/document1.png)
|
|
![](imagenes/document2.png)
|
|
|
|
el archivo creado debe estar vacío, unicamente con una linea de código que dice
|
|
|
|
```
|
|
import Foundation
|
|
```
|
|
|
|
en el archivo creado, se colocará un código base, donde indique que partes tendrá el consentimiento:
|
|
|
|
```
|
|
import Foundation
|
|
import ResearchKit
|
|
|
|
public var ConsentDocument: ORKConsentDocument {
|
|
|
|
let consentDocument = ORKConsentDocument()
|
|
consentDocument.title = "Introducción a ResearchKit" // O título de tu preferencia
|
|
|
|
// Tipos de secciones y sus contenidos
|
|
let sectionTypes: [ORKConsentSectionType] = [
|
|
.overview,
|
|
.dataGathering,
|
|
.privacy,
|
|
.dataUse,
|
|
.timeCommitment,
|
|
.studySurvey,
|
|
.studyTasks,
|
|
.withdrawing
|
|
]
|
|
|
|
let text = [
|
|
"Sección 1: Bienvenido. Este estudio trata sobre...",
|
|
"Sección 2: Recopilación de datos. Este estudio recopilará datos...",
|
|
"Sección 3: Privacidad. Valoramos su privacidad...",
|
|
"Sección 4: Uso de datos. Los datos recopilados se utilizarán para...",
|
|
"Sección 5: Compromiso de tiempo. Este estudio le llevará aproximadamente...",
|
|
"Sección 6: Encuesta del estudio. Para este estudio, deberá completar una encuesta...",
|
|
"Sección 7: Tareas del estudio. Se le solicitará que realice estas tareas...",
|
|
"Sección 8: Retiro. Para retirarse del estudio..."
|
|
]
|
|
|
|
// Crear secciones y añadirlas al documento de consentimiento
|
|
var sections: [ORKConsentSection] = []
|
|
|
|
for (index, sectionType) in sectionTypes.enumerated() {
|
|
let section = ORKConsentSection(type: sectionType)
|
|
let localizedText = NSLocalizedString(text[index], comment: "")
|
|
let localizedSummary = localizedText.components(separatedBy: ".")[0] + "."
|
|
|
|
section.summary = localizedSummary
|
|
section.content = localizedText
|
|
sections.append(section)
|
|
}
|
|
|
|
consentDocument.sections = sections
|
|
|
|
// Agregar la firma de consentimiento
|
|
let signature = ORKConsentSignature(forPersonWithTitle: "Participante", dateFormatString: nil, identifier: "ConsentDocumentParticipantSignature")
|
|
consentDocument.addSignature(signature)
|
|
|
|
return consentDocument
|
|
}
|
|
```
|
|
|
|
Para que el documento del consentimiento pueda ser activado, se requiere un tarea de consentimiento, por lo cual, se debe crear un archivo tipo Cocoa Touch Class, con la clase ORKOrderedTask, que lleve por nombre ConsentTask. Al que de igual manera se desarrollará un código base para que se vaya llenando de acuerdo a las necesidades, y como en el ConsentDocument se pusieron ya algunas secciones, se pondrán las misas para que haya una congruencia.
|
|
|
|
```
|
|
import UIKit
|
|
import ResearchKit
|
|
|
|
public var ConsentTask: ORKOrderedTask {
|
|
|
|
var steps = [ORKStep]()
|
|
|
|
// Visualización de secciones
|
|
|
|
let consentDocument = ConsentDocument
|
|
|
|
// Sección 1: Bienvenido
|
|
let section1 = ORKInstructionStep(identifier: "consentSection1InstructionStep")
|
|
section1.title = "Bienvenido"
|
|
section1.iconImage = UIImage(systemName: "hand.wave")
|
|
section1.detailText = "Sección 1: Bienvenido. Este estudio trata sobre..."
|
|
steps += [section1]
|
|
|
|
// Sección 2: Recopilación de datos
|
|
let section2 = ORKInstructionStep(identifier: "consentSection2InstructionStep")
|
|
section2.title = "Recopilación de datos"
|
|
section2.iconImage = UIImage(systemName: "doc.text")
|
|
section2.detailText = "Sección 2: Recopilación de datos. Este estudio recopilará datos..."
|
|
steps += [section2]
|
|
|
|
// Sección 3: Privacidad
|
|
let section3 = ORKInstructionStep(identifier: "consentSection3InstructionStep")
|
|
section3.title = "Privacidad"
|
|
section3.iconImage = UIImage(systemName: "lock.shield")
|
|
section3.detailText = "Sección 3: Privacidad. Valoramos su privacidad..."
|
|
steps += [section3]
|
|
|
|
// Sección 4: Uso de datos
|
|
let section4 = ORKInstructionStep(identifier: "consentSection4InstructionStep")
|
|
section4.title = "Uso de datos"
|
|
section4.iconImage = UIImage(systemName: "chart.bar")
|
|
section4.detailText = "Sección 4: Uso de datos. Los datos recopilados se utilizarán para..."
|
|
steps += [section4]
|
|
|
|
// Sección 5: Compromiso de tiempo
|
|
let section5 = ORKInstructionStep(identifier: "consentSection5InstructionStep")
|
|
section5.title = "Compromiso de tiempo"
|
|
section5.iconImage = UIImage(systemName: "clock")
|
|
section5.detailText = "Sección 5: Compromiso de tiempo. Este estudio le llevará aproximadamente..."
|
|
steps += [section5]
|
|
|
|
// Sección 6: Encuesta del estudio
|
|
let section6 = ORKInstructionStep(identifier: "consentSection6InstructionStep")
|
|
section6.title = "Encuesta del estudio"
|
|
section6.iconImage = UIImage(systemName: "list.bullet.rectangle")
|
|
section6.detailText = "Sección 6: Encuesta del estudio. Para este estudio, deberá completar una encuesta..."
|
|
steps += [section6]
|
|
|
|
// Sección 7: Tareas del estudio
|
|
let section7 = ORKInstructionStep(identifier: "consentSection7InstructionStep")
|
|
section7.title = "Tareas del estudio"
|
|
section7.iconImage = UIImage(systemName: "pencil.and.outline")
|
|
section7.detailText = "Sección 7: Tareas del estudio. Se le solicitará que realice estas tareas..."
|
|
steps += [section7]
|
|
|
|
// Sección 8: Retiro
|
|
let section8 = ORKInstructionStep(identifier: "consentSection8InstructionStep")
|
|
section8.title = "Retiro"
|
|
section8.iconImage = UIImage(systemName: "arrow.backward.circle")
|
|
section8.detailText = "Sección 8: Retiro. Para retirarse del estudio..."
|
|
steps += [section8]
|
|
|
|
// Revisar y firmar
|
|
let signature = consentDocument.signatures!.first!
|
|
let reviewConsentStep = ORKConsentReviewStep(identifier: "ConsentReviewStep", signature: signature, in: consentDocument)
|
|
reviewConsentStep.text = "Revise el formulario de consentimiento."
|
|
reviewConsentStep.reasonForConsent = "Consentimiento para unirse al estudio"
|
|
steps += [reviewConsentStep]
|
|
|
|
// Passcode/TouchID Protection
|
|
let passcodeStep = ORKPasscodeStep(identifier: "Passcode")
|
|
passcodeStep.iconImage = UIImage(systemName: "lock.circle")
|
|
passcodeStep.text = "Ahora creará un código de acceso para identificarse en la aplicación y proteger la información ingresada."
|
|
steps += [passcodeStep]
|
|
|
|
// Completion
|
|
let completionStep = ORKCompletionStep(identifier: "CompletionStep")
|
|
completionStep.iconImage = UIImage(systemName: "checkmark.seal")
|
|
completionStep.title = "Bienvenido a bordo"
|
|
completionStep.text = "Gracias por unirse a este estudio."
|
|
steps += [completionStep]
|
|
|
|
return ORKOrderedTask(identifier: "ConsentTask", steps: steps)
|
|
}
|
|
|
|
```
|
|
Para que al presionar el botón se acceda al consentimiento, es necesario crear la acción en el ConsentViewController, en el segue que se arrastró desde el botón, por lo cuál el código quedaría de la isguiente manera:
|
|
|
|
```
|
|
@IBAction func joinButtonTapped(_ sender: UIButton) {
|
|
let taskViewController = ORKTaskViewController(task: ConsentTask, taskRun: nil)
|
|
taskViewController.delegate = self
|
|
taskViewController.modalPresentationStyle = .fullScreen
|
|
present(taskViewController, animated: true, completion: nil)
|
|
}
|
|
```
|
|
|
|
también se necesita crear un metodo el cual se asegura de que la vista de la tarea se cierre automáticamente cuando el usuario termine o abandone la tarea, independientemente del motivo (reason) y se coloca debado de la acción del botón.
|
|
|
|
```
|
|
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskFinishReason, error: Error?) {
|
|
dismiss(animated: true, completion: nil) //Dismisses the view controller when we finish our consent task
|
|
|
|
}
|
|
```
|
|
|
|
Al finalizar, se puede correr la aplicación y observar las diferentes secciones que ya se programaron:
|
|
|
|
|
|
![](imagenes/cel1.png)
|
|
![](imagenes/cel2.png)
|
|
![](imagenes/cel3.png)
|
|
![](imagenes/cel4.png)
|
|
![](imagenes/cel5.png)
|
|
![](imagenes/cel6.png)
|
|
|
|
Debido a que los desarroladores de Apple, utilizan el idioma para hacer sus aplicaciones y el soporte, algunas de las secciones en la aplicación, podrían estar en idioma inglés.
|
|
|
|
Al tener terminado el consentimiento, se tendrá que enviar a una nueva pantalla donde se encuentran los formularios, tareas y demás actividades que ResearchKit permite hacer, ya que al momento de terminar solo se regresa a la pantalla principal.
|
|
|
|
Para hacer este cambio de cuando el consentimiento este completo, se crea una condicion *if* en el **IntroViewController** que indique que si se completó el consentimiento, proceda a la pantalla de **TasksViewController**, o e su defecto que haga efecto el segue, toTasks.
|
|
|
|
```
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
// Do any additional setup after loading the view, typically from a nib.
|
|
if ORKPasscodeViewController.isPasscodeStoredInKeychain(){
|
|
toTasks()
|
|
}else{
|
|
toConsent()
|
|
}
|
|
}
|
|
```
|
|
|
|
Sin olvidarse de importar ResearchKitUI
|
|
|
|
Además, se tiene que llevar al usuario al controlador de vista(TasksViewController) de tareas cuando se completen las tareas del consentimiento. Por ello, se agrega el sigueinte código al **IntroViewController**:
|
|
|
|
```
|
|
@IBAction func unwindToTasks(_ segue: UIStoryboardSegue){
|
|
toTasks()
|
|
}
|
|
|
|
```
|
|
|
|
y desde el main.storyboard se debe arrastrar con click derecho desde la pantalla de ConsentViewController del boton principal, (ConsentViewController) hasta el de exit y seleccionar la unica opcion que hay que es unwindTotask.
|
|
|
|
|
|
![](imagenes/unwind1.png)
|
|
![](imagenes/unwind2.png)
|
|
|
|
y agregaremos un identificador al segue con el nombre unwindToTasks.
|
|
|
|
![](imagenes/unwind3.png)
|
|
|
|
Se dirigirá al ConsentViewController en la funcion taskViewController que se encuentra en la parte inferior y la modificará para que haga la correcta transición del nuevo segue:
|
|
|
|
```
|
|
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskFinishReason, error: Error?) {
|
|
|
|
switch reason {
|
|
case .completed:
|
|
performSegue(withIdentifier: "unwindToTasks", sender: nil)
|
|
case .discarded:
|
|
dismiss(animated: true, completion: nil)
|
|
case .failed:
|
|
dismiss(animated: true, completion: nil)
|
|
case .saved:
|
|
dismiss(animated: true, completion: nil)
|
|
case .earlyTermination:
|
|
dismiss(animated: true, completion: nil)
|
|
|
|
@unknown default:
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
}
|
|
```
|
|
|
|
En el TasksViewController del Main.storyboard se requiere crear un nuevo botón para el abandono del estudio y así poder regresar a la pantalla de inicio.
|
|
|
|
![](imagenes/bAbandonar.png)
|
|
|
|
y para hacer el regreso a la pantalla de Consent al presionar el botón de abandonar, se crea, un segue de tipo show hasta el ConsentViewController. Y a este segue, se le pondrá un identificador que lleve por nombre returnToConsent:
|
|
|
|
![](imagenes/returnC.png)
|
|
![](imagenes/returnC1.png)
|
|
![](imagenes/returnC2.png)
|
|
|
|
Para hacer que funcione este segue al presionar el botón de abandonar el estudio, en el TasksViewController, es necesario crear un ButtonAction de tipo UIButton
|
|
con el nombre leaveButtonTapped.
|
|
|
|
![](imagenes/Bleave.png)
|
|
|
|
en el código generado se pondra el siguiente código, el cual elimina el almacén de contraseñas y presenta el ConsentViewController:
|
|
|
|
```
|
|
ORKPasscodeViewController.removePasscodeFromKeychain()
|
|
performSegue(withIdentifier: "returnToConsent", sender: nil)
|
|
|
|
```
|
|
De esta manera, el usuario abandonará el estudio, y si desea unirse de nuevo, se le presentará el documento de consentimiento y se le pedirá que cree un código de acceso.
|
|
|
|
Afortunadamente, ResearchKit no solo tiene la herramienta para visualizar el proceso de consentimiento, sino también la herramienta para guardar el documento de consentimiento firmado como PDF.
|
|
|
|
Para hacer la visualización del consentimiento se tiene que modificar desde el **ConsentViewController** la función *func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskFinishReason, error: Error?) {*
|
|
|
|
con el sigueinte código:
|
|
|
|
```
|
|
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskFinishReason, error: Error?) {
|
|
|
|
switch reason {
|
|
case .completed:
|
|
// Obtener el resultado de la firma
|
|
guard let signatureResult = taskViewController.result.stepResult(forStepIdentifier: "ConsentReviewStep")?.firstResult as? ORKConsentSignatureResult else {
|
|
print("No se pudo obtener el resultado de la firma.")
|
|
return
|
|
}
|
|
|
|
// Crear una copia del documento de consentimiento
|
|
let consentDocument = ConsentDocument.copy() as! ORKConsentDocument
|
|
signatureResult.apply(to: consentDocument)
|
|
|
|
// Generar el PDF
|
|
consentDocument.makePDF { (data, error) in
|
|
if let error = error {
|
|
print("Error al crear PDF: \(error.localizedDescription)")
|
|
return
|
|
}
|
|
|
|
guard let data = data else {
|
|
print("No se generó ningún dato para el PDF.")
|
|
return
|
|
}
|
|
|
|
// Guardar el PDF en el directorio de documentos
|
|
do {
|
|
var docURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last
|
|
docURL = docURL?.appendingPathComponent("consent.pdf")
|
|
try data.write(to: docURL!, options: .atomicWrite)
|
|
print("PDF guardado en: \(docURL!.path)")
|
|
} catch {
|
|
print("Error al guardar el PDF: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
// Realizar la transición al siguiente controlador
|
|
performSegue(withIdentifier: "unwindToTasks", sender: nil)
|
|
|
|
case .discarded, .failed, .saved:
|
|
dismiss(animated: true, completion: nil)
|
|
|
|
case .earlyTermination:
|
|
dismiss(animated: true, completion: nil)
|
|
|
|
@unknown default:
|
|
// Manejar casos no contemplados
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
}
|
|
```
|
|
|
|
y se creará un nuevo botón en la pantalla del **TasksViewController** donde al presionar se pueda visualizar el consentimiento que se firmó.
|
|
|
|
Al botón generado se le generará un Button Action con el Nombre consentButtonTapped de tipo UIButton, directamente ne **TasksViewController**.
|
|
|
|
![](imagenes/consentPDF.png)
|
|
|
|
y se le agrega el siguiente código para generar el PDF.
|
|
|
|
```
|
|
@IBAction func consentButtonTapped(_ sender: UIButton) {
|
|
let taskViewController = ORKTaskViewController(task: consentPDFViewerTask(), taskRun: nil)
|
|
taskViewController.delegate = self
|
|
present(taskViewController, animated: true, completion: nil)
|
|
|
|
}
|
|
|
|
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])
|
|
}
|
|
|
|
@objc(taskViewController:didFinishWithReason:error:) func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskFinishReason, error: Error?) {
|
|
taskViewController.dismiss(animated: true, completion: nil)
|
|
}
|
|
```
|
|
|
|
sin olvidar agregar *ORKTaskViewControllerDelegate* en la clase principal
|
|
|
|
Para comprobar el funcionameinto, se procede a realizar el consentimiento y al teminar, se da click en el Consentimiento PDF y generará algo como lo siguiente:
|
|
|
|
![](imagenes/pdfC.png)
|
|
|
|
Ahora para crear distintos formularios, test, historias clinicas, actividades, entre otras, es necesario crear diferentes botones con la acción a realizar, las cuales serán activadas desde el **TaskViewController** ya que ahí es donde se alojarán los botones. Por lo tanto, se crearán para 3 distintas actividades, una Ficha de Identificación, un Formulario, y algunas actividades como de medicion de pasos, de memoria, reacción, voz, etc.
|
|
|
|
![](imagenes/botonesFFA.png)
|
|
|
|
Primero se realizará la ficha de identificación de los pacientes o los usuarios. Por lo tanto, se crea un nuevo archivo Swift File con el nombre **FichaManager** que es donde se pondrá todas las preguntas con relación a la Ficha de identificación, y con distintos tipos de respuesta, como de opcion multiple, de opcion unica, abiertas, entre otras.
|
|
|
|
El código para la Ficha de identificación queda de la siguiente manera:
|
|
|
|
```
|
|
import Foundation
|
|
import ResearchKit
|
|
|
|
class FichaManager {
|
|
static let shared = FichaManager()
|
|
|
|
func createFichaTask() -> ORKTask {
|
|
|
|
let bienvenida = ORKInstructionStep(identifier: "bienvenida")
|
|
bienvenida.title = "Ficha Identificación"
|
|
bienvenida.iconImage = UIImage(systemName: "person.text.rectangle")
|
|
bienvenida.detailText = "Por favor, llena la siguiente ficha de identificación con tus datos"
|
|
|
|
// Nombre
|
|
let textQuestionStepTitle = "Nombre"
|
|
let textAnswerFormat = ORKTextAnswerFormat(maximumLength: 30)
|
|
textAnswerFormat.multipleLines = false
|
|
let textQuestionStep = ORKQuestionStep(identifier: "Nombre", title: textQuestionStepTitle, question: "¿Cuál es tu nombre?", answer: textAnswerFormat)
|
|
textQuestionStep.isOptional = false
|
|
|
|
// Sexo
|
|
let multiGenderChoice = "Sexo"
|
|
let gendChoices = [
|
|
ORKTextChoice(text: "Hombre", value: "hombre" as NSString),
|
|
ORKTextChoice(text: "Mujer", value: "mujer" as NSString),
|
|
ORKTextChoice(text: "Prefiero no decirlo", value: "Inf" as NSString)]
|
|
|
|
let genderChoiceFormat = ORKTextChoiceAnswerFormat(style: .singleChoice, textChoices: gendChoices)
|
|
let multiGenderQuestionStep = ORKQuestionStep(identifier: "Sexo", title: multiGenderChoice,question: "¿Cuál es tu sexo?", answer: genderChoiceFormat)
|
|
multiGenderQuestionStep.isOptional = false
|
|
|
|
// Edad
|
|
var ageChoices: [ORKTextChoice] = []
|
|
for age in 16...50 {
|
|
let ageChoice = ORKTextChoice(text: "\(age) años", value: "\(age)" as NSString)
|
|
ageChoices.append(ageChoice)
|
|
}
|
|
|
|
let ageQuestionStep = ORKQuestionStep(
|
|
identifier: "ageQuestionStep",
|
|
title: "Pregunta sobre Edad",
|
|
question: "¿Qué edad tienes?",
|
|
answer: ORKValuePickerAnswerFormat(textChoices: ageChoices)
|
|
)
|
|
ageQuestionStep.isOptional = false // Hacemos que esta pregunta sea obligatoria
|
|
|
|
// Peso
|
|
let pesoQuestionStep = ORKQuestionStep(identifier: "peso", title: "Peso", question: "¿Cuál es tu peso en kilogramos?", answer: ORKNumericAnswerFormat(style: .integer, unit: "kg"))
|
|
pesoQuestionStep.isOptional = false
|
|
|
|
// Altura
|
|
let alturaQuestionStep = ORKQuestionStep(identifier: "altura", title: "Altura", question: "¿Cuál es tu altura en metros?", answer: ORKNumericAnswerFormat(style: .decimal, unit: "m"))
|
|
alturaQuestionStep.isOptional = false
|
|
|
|
|
|
// Sintomas
|
|
let symptomsChoices = [
|
|
ORKTextChoice(text: "Dolor de cabeza", value: "dolor_de_cabeza" as NSString),
|
|
ORKTextChoice(text: "Fiebre", value: "fiebre" as NSString),
|
|
ORKTextChoice(text: "Tos", value: "tos" as NSString),
|
|
ORKTextChoice(text: "Dificultad para respirar", value: "dificultad_respirar" as NSString),
|
|
ORKTextChoice(text: "Dolor en el pecho", value: "dolor_pecho" as NSString),
|
|
ORKTextChoice(text: "Fatiga", value: "fatiga" as NSString),
|
|
ORKTextChoice(text: "Náuseas", value: "nauseas" as NSString),
|
|
ORKTextChoice(text: "Otros", value: "otros" as NSString)
|
|
]
|
|
|
|
// Crear la pregunta de síntomas
|
|
let symptomsStep = ORKQuestionStep(
|
|
identifier: "sintomas",
|
|
title: "¿Qué síntomas estás presentando?",
|
|
answer: ORKAnswerFormat.choiceAnswerFormat(with: .multipleChoice, textChoices: symptomsChoices)
|
|
)
|
|
symptomsStep.isOptional = false
|
|
|
|
let termino = ORKInstructionStep(identifier: "termino")
|
|
termino.title = "Gracias"
|
|
termino.iconImage = UIImage(systemName: "checkmark.circle.fill")
|
|
termino.detailText = "Gracias por llenar la ficha de identificación"
|
|
|
|
return ORKOrderedTask(identifier: "FichaIdentificacion", steps: [bienvenida, textQuestionStep, multiGenderQuestionStep, ageQuestionStep, pesoQuestionStep, alturaQuestionStep, symptomsStep, termino])
|
|
}
|
|
}
|
|
```
|
|
|
|
En donde encontramos preguntas muy simples y personales a las que se les puede agregar muchas más preguntas que en lugar de hacer una Ficha de Identificación se puede hacer una Historia clínica.
|
|
|
|
De manera similar a los otros botones, es necesario que del botón de Ficha se cree un buttonAction en el TasksViewController, para ahi poner la accion que es abrir la Ficha.
|
|
|
|
![](imagenes/fichaBA.png)
|
|
|
|
donde agregaremos el siguiente código:
|
|
|
|
```
|
|
let taskViewController = ORKTaskViewController(task: FichaManager.shared.createFichaTask(), taskRun: nil)
|
|
taskViewController.delegate = self
|
|
taskViewController.modalPresentationStyle = .fullScreen
|
|
present(taskViewController, animated: true, completion: nil)
|
|
```
|
|
|
|
Esto ya abriría la ficha de identificación con las preguntas propuestas.
|
|
|
|
![](imagenes/ficha1ex.png)
|
|
|
|
sin embargo, las respuestas no son conocidas, para ello, se requiere que en el **TasksViewController** se haga una funcion en donde se podran visualizar y manipular para hacer algunos procesos que se necesiten, como calculo de IMC, entre otros, la función para este momento de mostrara todas esta de la siguiente manera:
|
|
|
|
```
|
|
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskFinishReason, error: Error?) {
|
|
// 1. Dismiss the task view controller
|
|
taskViewController.dismiss(animated: true, completion: nil)
|
|
|
|
// 2. Get the task result from the taskViewController
|
|
let taskResult = taskViewController.result
|
|
print("Estructura completa de los resultados: \(taskResult)")
|
|
|
|
|
|
// 3. Verificar que los resultados estén presentes
|
|
if let stepResults = taskResult.results as? [ORKStepResult] {
|
|
// 4. Imprimir los resultados de los pasos
|
|
for stepResult in stepResults {
|
|
print("Identificador del paso: \(stepResult.identifier)")
|
|
|
|
// Dependiendo del tipo de resultado, extraemos la información adecuada
|
|
if let textResult = stepResult.results?.first as? ORKTextQuestionResult {
|
|
// Resultado de una pregunta de texto
|
|
print("Respuesta de \(stepResult.identifier): \(textResult.textAnswer ?? "Sin respuesta")")
|
|
} else if let choiceResult = stepResult.results?.first as? ORKChoiceQuestionResult {
|
|
// Resultado de una pregunta de opción múltiple
|
|
print("Respuesta de \(stepResult.identifier): \(choiceResult.choiceAnswers ?? [])")
|
|
} else if let numericResult = stepResult.results?.first as? ORKNumericQuestionResult {
|
|
// Resultado de una pregunta numérica
|
|
print("Respuesta de \(stepResult.identifier): \(numericResult.numericAnswer ?? 0)")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Esto mostraria todas las respuestas, pero para seleccionar solo una, por ejemplo el nombre, se puede hacer de manera individual con el siguiente código:
|
|
|
|
```
|
|
if let nombreResult = taskResult.result(forIdentifier: "Nombre") as? ORKTextQuestionResult {
|
|
if let nombre = nombreResult.textAnswer {
|
|
print("Nombre: \(nombre)") // Debería imprimir "David"
|
|
} else {
|
|
print("No se encontró respuesta para el nombre.")
|
|
}
|
|
}
|
|
```
|
|
|
|
Para la creación de tests más largos y elaborados es de la misma manera, por ello, no se colocará el código, ya que tiene la misma estructura, solo cambiarían las preguntas, y la extension de ellas.
|
|
|
|
El test que se realizó es acerca del estado de animo y ansiedad, el cuál luce de la siguiente manera:
|
|
|
|
![](imagenes/testAn.png)
|
|
|
|
Para la parte de actividades, se requiere crear de nuevo un Swift File, con el nombre de ActiveTaskManager
|
|
|
|
La estructura para la creacion de las tareas activas es la misma a los formularios, sin embargo, como mencionamos anteriormente los desarrolladores crearon distintos módulos en el formato ingles, asi que para personas que hablan español, es necesario poner indicaciones antes de la actividad. En esta ocasión se pondran # actividades que abarcan las distintas áreas precargadas de ResearchKit.
|
|
|
|
EL fommato de archivo para actividades es el siguiente:
|
|
|
|
```
|
|
import Foundation
|
|
import ResearchKit
|
|
import ResearchKitUI
|
|
import ResearchKitActiveTask
|
|
import ResearchKitActiveTask_Private
|
|
|
|
|
|
class ActiveTaskManager {
|
|
static let shared = ActiveTaskManager()
|
|
|
|
func createActiveTasks() -> ORKOrderedTask {
|
|
|
|
|
|
|
|
return ORKOrderedTask(identifier: "activeTasks", steps: [])
|
|
}
|
|
}
|
|
```
|
|
|
|
en la función se irán colocando las actividades y seguido de step el orden en que se quiere mostrar.
|
|
|
|
Las actividades están variadas, en algunas de las áreas que permite, ResearchKit, como por ejemplo, *Motor Activities, Cognition, Speech, Hearing, Hand Dexterity, Vision* los ejemplos se pueden observar en el siguiente código.
|
|
|
|
```
|
|
import Foundation
|
|
import ResearchKit
|
|
import ResearchKitUI
|
|
import ResearchKitActiveTask
|
|
import ResearchKitActiveTask_Private
|
|
|
|
|
|
class ActiveTaskManager {
|
|
static let shared = ActiveTaskManager()
|
|
|
|
func createActiveTasks() -> ORKOrderedTask {
|
|
|
|
let bienvenido = ORKInstructionStep(identifier: "bienvenido")
|
|
bienvenido.title = "Bienvenido"
|
|
bienvenido.iconImage = UIImage(systemName: "hand.wave")
|
|
bienvenido.detailText = "Bienvenido a la sección de tareas activas. Aquí encontrarás diversas actividades que podrás realizar para obtener información sobre algunos aspectos de tu salud."
|
|
|
|
//Motor Activities
|
|
let tappingStep = ORKTappingIntervalStep(identifier: "tapping")
|
|
tappingStep.title = "Prueba de tapping"
|
|
tappingStep.detailText = "Toca repetidamente en las áreas designadas lo más rápido posible durante 10 segundos."
|
|
tappingStep.stepDuration = 10 // Duración en segundos
|
|
|
|
|
|
//Cognition
|
|
// Cognition - Trail Making (Solo números)
|
|
let trailMakingStep = ORKTrailmakingStep(identifier: "trail")
|
|
trailMakingStep.title = "Prueba de Trail Making"
|
|
trailMakingStep.detailText = "En este ejercicio, tendrás que conectar los círculos numerados en orden, lo más rápido posible."
|
|
|
|
|
|
let hanoitext = ORKInstructionStep(identifier: "towerOfHanoiInstructions")
|
|
hanoitext.title = "Torre de Hanoi"
|
|
hanoitext.iconImage = UIImage(systemName: "triangle")
|
|
hanoitext.detailText = "Transfiere los discos de la barra inicial a la barra final, siguiendo estas reglas:\n\n1. Solo puedes mover un disco a la vez.\n\n2. No puedes colocar un disco grande encima de uno más pequeño."
|
|
|
|
|
|
let towerOfHanoiStep = ORKTowerOfHanoiStep(identifier: "towerOfHanoiStep")
|
|
towerOfHanoiStep.numberOfDisks = 3
|
|
towerOfHanoiStep.isOptional = false
|
|
|
|
let flowerPatternInstructionStep = ORKInstructionStep(identifier: "flowerPatternInstructions")
|
|
flowerPatternInstructionStep.title = "Patrón de las Flores"
|
|
flowerPatternInstructionStep.iconImage = UIImage(systemName: "flower.fill")
|
|
flowerPatternInstructionStep.detailText = """
|
|
En esta tarea, verás un patrón formado por flores en una cuadrícula. Tu objetivo será recordar el orden en que aparecen y reproducirlo correctamente.
|
|
|
|
1. Observa el patrón de flores que aparece en la pantalla.
|
|
2. Memoriza el orden y la posición de cada flor.
|
|
3. Una vez que el patrón desaparezca, toca en las celdas de la cuadrícula para reproducir el orden que viste.
|
|
4. Trata de ser lo más preciso posible.
|
|
"""
|
|
|
|
|
|
let memoryStep = ORKSpatialSpanMemoryStep(identifier: "memory")
|
|
memoryStep.initialSpan = 2
|
|
memoryStep.minimumSpan = 2
|
|
memoryStep.maximumSpan = 3
|
|
memoryStep.playSpeed = 1
|
|
memoryStep.maximumTests = 3
|
|
memoryStep.maximumConsecutiveFailures = 10 // Asegúrate de que sea un número válido
|
|
|
|
|
|
// Audio Capture - Speech Recording Step
|
|
let speechRecognitionStep = ORKSpeechInNoiseStep(identifier: "speechRecognition")
|
|
speechRecognitionStep.title = "Prueba de Reconocimiento de Voz"
|
|
speechRecognitionStep.detailText = "Escucha el siguiente audio, después escribe lo que escuchaste."
|
|
|
|
let speechText = ORKQuestionStep(
|
|
identifier: "textoAudio",
|
|
title: "Escribe lo que escuchaste en el audio",
|
|
answer: {
|
|
let formatoTexto = ORKTextAnswerFormat(maximumLength: 500)
|
|
formatoTexto.multipleLines = true // Permite que el cuadro sea más grande
|
|
return formatoTexto
|
|
}()
|
|
)
|
|
speechText.isOptional = false
|
|
|
|
|
|
//Hearing
|
|
let toneStep = ORKToneAudiometryStep(identifier: "toneAudiometry")
|
|
toneStep.toneDuration = 10
|
|
toneStep.title = "Prueba de Audiometría de Tono"
|
|
toneStep.detailText = "En este ejercicio escucharás tonos a diferentes frecuencias y volúmenes. Por favor, indica cuándo puedas escuchar el tono y por cuál oido lo escuhas. El objetivo es evaluar tu capacidad auditiva."
|
|
|
|
|
|
//Hand Dexterity
|
|
let handStep = ORKHolePegTestPlaceStep(identifier: "hole")
|
|
handStep.title = "Prueba de Destreza Manual"
|
|
handStep.detailText = "En este ejercicio, debes insertar los pegs (barras) en los agujeros lo más rápido posible. Trata de ser preciso y rápido, y asegúrate de no dejar caer ninguna pieza."
|
|
handStep.numberOfPegs = 9
|
|
handStep.stepDuration = 30
|
|
|
|
let visionInstructionStep = ORKInstructionStep(identifier: "amslerGridInstructions")
|
|
visionInstructionStep.title = "Rejilla de Amsler"
|
|
visionInstructionStep.iconImage = UIImage(systemName: "eye.fill")
|
|
visionInstructionStep.detailText = """
|
|
En esta prueba, observa la rejilla de Amsler en la pantalla y responde a las preguntas a continuación.
|
|
|
|
1. Asegúrate de estar en un lugar cómodo y con buena iluminación.
|
|
2. Si usas gafas, póntelas antes de comenzar.
|
|
3. Mira la rejilla con ambos ojos, pero si es necesario, cierra un ojo a la vez.
|
|
4. Indica si ves alguna distorsión, línea torcida o áreas vacías.
|
|
5. cuando indiques sobre que areas hay distorsión, da swipe sobre la pantalla negra haceia la derecha.
|
|
"""
|
|
|
|
// Vision - Amsler Grid
|
|
let visionStep = ORKAmslerGridStep(identifier: "vision")
|
|
visionStep.stepDuration = 30
|
|
visionStep.title = "Prueba de la Rejilla de Amsler"
|
|
visionStep.detailText = "Este test se usa para detectar problemas visuales relacionados con la macula. Mira el centro de la rejilla y marca cualquier distorsión o área oscura que puedas ver en la cuadrícula."
|
|
|
|
let completionStep = ORKCompletionStep(identifier: "taskCompletion")
|
|
completionStep.title = "¡Tareas Completadas!"
|
|
completionStep.iconImage = UIImage(systemName: "checkmark.circle.fill")
|
|
completionStep.detailText = """
|
|
Has completado todas las tareas del estudio. ¡Gracias por tu participación!
|
|
|
|
Recuerda que tu contribución es muy valiosa para esta investigación.
|
|
"""
|
|
|
|
|
|
return ORKOrderedTask(identifier: "activeTasks", steps: [bienvenido, tappingStep, trailMakingStep, hanoitext, towerOfHanoiStep, flowerPatternInstructionStep, memoryStep, speechRecognitionStep, speechText, toneStep, handStep, visionInstructionStep, visionStep, completionStep])
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
Unicamente quedaría correr el programa y observar las distintas etapas desarrolladas, en caso de querer agregar o quitar es necesario cambiar el formato, además de agregar condicionales, o algunas cararcteristicas más a nuestras actividades.
|
|
|
|
Algunas imagenes de como se vería corriendo el simulador son las siguientes:
|
|
|
|
![](imagenes/simu1.png)
|
|
![](imagenes/simu2.png)
|
|
![](imagenes/simu3.png)
|
|
![](imagenes/simu4.png)
|
|
![](imagenes/simu5.png)
|
|
![](imagenes/simu6.png)
|
|
![](imagenes/simu7.png)
|
|
|
|
Así es como termina la introducción base a ResearchKit y las herramientas más importantes, sin embargo, esto está a tu creatividad y necesidad, puedes hacer test super grandes, formularios específicos, agregar imagenes a las preguntas, una infinidad de usos, es además que es multi variado, ya que este puede funcionar con frameworks como HealthKit y CareKit, y no solo podemos quedar hasta ahí, si no que todas las respuestas puedes ubirse a una base de datos, generar un CSV, JASON y más que nos ayudaran al procesamiento de los datos.
|
|
|
|
|