Skip to content

Commit

Permalink
Upgrade Spezi to 0.8.0 (#18)
Browse files Browse the repository at this point in the history
# Upgrade Spezi to 0.8.0

## ♻️ Current situation & Problem
This PR upgrades to Spezi 0.8.0 and moves to observable.

As discussed, this PR removes the Questionnaire Standard Constraint, as
we identified that questionnaire responses typically require local
context (e.g., the current selected patient). Therefore, questionnaire
completion shall always be handled through the closure.

## ⚙️ Release Notes 
* Migrate to Observable
* Upgrade to Spezi 0.8.0
* Introduce a new `QuestionnaireResult` type, received via the closure
* Removed the Standard constraints.


## 📚 Documentation
Documentation was updated and respective parts were removed.


## ✅ Testing
--


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Nov 15, 2023
1 parent 7318923 commit 4f3e1cd
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 272 deletions.
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.7
// swift-tools-version:5.9

//
// This source file is part of the Stanford Spezi open source project
Expand All @@ -14,15 +14,15 @@ import PackageDescription
let package = Package(
name: "SpeziQuestionnaire",
platforms: [
.iOS(.v16)
.iOS(.v17)
],
products: [
.library(name: "SpeziQuestionnaire", targets: ["SpeziQuestionnaire"])
],
dependencies: [
.package(url: "https://github.com/apple/FHIRModels", .upToNextMinor(from: "0.5.0")),
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.0")),
.package(url: "https://github.com/StanfordBDHG/ResearchKit", from: "2.2.9"),
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")),
.package(url: "https://github.com/StanfordBDHG/ResearchKit", from: "2.2.19"),
.package(url: "https://github.com/StanfordBDHG/ResearchKitOnFHIR", .upToNextMinor(from: "0.2.1"))
],
targets: [
Expand Down
60 changes: 11 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,55 +32,12 @@ Questionnaires are displayed using [ResearchKit](https://github.com/ResearchKit/

## Setup

### 1. Add Spezi Questionnaire as a Dependency

You need to add the Spezi Questionnaire Swift package to
[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or
[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package).

> [!IMPORTANT]
> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) setup the core Spezi infrastructure.
### 2. Ensure that your Standard Conforms to the [`QuestionnaireConstraint`](https://swiftpackageindex.com/stanfordspezi/speziquestionnaire/documentation/speziquestionnaire/questionnaireconstraint) Protocol

In order to receive responses from Questionnaires, the [`Standard`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/standard) defined in your configuration within your [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate) should conform to the [`QuestionnaireConstraint`](https://swiftpackageindex.com/stanfordspezi/speziquestionnaire/documentation/speziquestionnaire/questionnaireconstraint) protocol.

Below, we create an `ExampleStandard` and extend it to implement an `add` function which receives the result of our questionnaire as a [FHIR QuestionnaireResponse](http://hl7.org/fhir/R4/questionnaireresponse.html). In this simple example, completing a survey increases the surveyResponseCount.

```swift
/// An example Standard used for the configuration.
actor ExampleStandard: Standard, ObservableObject, ObservableObjectProvider {
@Published @MainActor var surveyResponseCount: Int = 0
}


extension ExampleStandard: QuestionnaireConstraint {
func add(response: ModelsR4.QuestionnaireResponse) async {
surveyResponseCount += 1
}
}
```

> Tip: You can learn more about a [`Standard` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/standard)
### 3. Register the Questionnaire Data Source Component

The [`QuestionnaireDataSource`](https://swiftpackageindex.com/stanfordspezi/speziquestionnaire/documentation/speziquestionnaire/questionnairedatasource) component needs to be registered in a Spezi-based application using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) in a
[`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate):

```swift
class ExampleAppDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration(standard: ExampleStandard()) {
QuestionnaireDataSource()
// ...
}
}
}
```

> Tip: You can learn more about a [`Component` in the Spezi documentation](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component).
> If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) and set up the core Spezi infrastructure.
## Example

Expand All @@ -94,17 +51,22 @@ import SwiftUI

struct ExampleQuestionnaireView: View {
@State var displayQuestionnaire = false


var body: some View {
Button("Display Questionnaire") {
displayQuestionnaire.toggle()
}
.sheet(isPresented: $displayQuestionnaire) {
QuestionnaireView(
questionnaire: Questionnaire.gcs,
isPresented: $displayQuestionnaire
)
questionnaire: Questionnaire.gcs
) { result in
guard case let .completed(response) = result else {
return // user cancelled
}

// ... save the FHIR response to your data store
}
}
}
}
Expand Down
40 changes: 20 additions & 20 deletions Sources/SpeziQuestionnaire/ORKOrderedTaskView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,40 @@
import ModelsR4
import ResearchKit
import ResearchKitOnFHIR
import Spezi
import SwiftUI
import UIKit


struct ORKOrderedTaskView: UIViewControllerRepresentable {
class Coordinator: NSObject, ORKTaskViewControllerDelegate, ObservableObject {
private let isPresented: Binding<Bool>
private let questionnaireResponse: @MainActor (QuestionnaireResponse) async -> Void

private let result: @MainActor (QuestionnaireResult) async -> Void


init(isPresented: Binding<Bool>, questionnaireResponse: @escaping @MainActor (QuestionnaireResponse) async -> Void) {
self.isPresented = isPresented
self.questionnaireResponse = questionnaireResponse
init(result: @escaping @MainActor (QuestionnaireResult) async -> Void) {
self.result = result
}


func taskViewController(
_ taskViewController: ORKTaskViewController,
didFinishWith reason: ORKTaskViewControllerFinishReason,
error: Error?
) {
_Concurrency.Task { @MainActor in
isPresented.wrappedValue = false

switch reason {
case .completed:
let fhirResponse = taskViewController.result.fhirResponse
fhirResponse.subject = Reference(reference: FHIRPrimitive(FHIRString("My Patient")))

await questionnaireResponse(fhirResponse)
default:
await result(.completed(fhirResponse))
case .discarded, .earlyTermination:
await result(.cancelled)
case .failed:
await result(.failed)
case .saved:
break // we don't support that currently
@unknown default:
break
}
}
Expand All @@ -49,29 +52,26 @@ struct ORKOrderedTaskView: UIViewControllerRepresentable {

private let tasks: ORKOrderedTask
private let tintColor: Color
private let questionnaireResponse: @MainActor (QuestionnaireResponse) async -> Void

@Binding private var isPresented: Bool
private let questionnaireResponse: @MainActor (QuestionnaireResult) async -> Void


/// - Parameters:
/// - tasks: The `ORKOrderedTask` that should be displayed by the `ORKTaskViewController`
/// - delegate: An `ORKTaskViewControllerDelegate` that handles delegate calls from the `ORKTaskViewController`. If no view controller delegate is provided the view uses an instance of `CKUploadFHIRTaskViewControllerDelegate`.
/// - result: A closure receiving the ``QuestionnaireResult`` for the task view.
/// - tintColor: The tint color to use with ResearchKit views
init(
tasks: ORKOrderedTask,
isPresented: Binding<Bool>,
questionnaireResponse: @escaping @MainActor (QuestionnaireResponse) async -> Void,
result: @escaping @MainActor (QuestionnaireResult) async -> Void,
tintColor: Color = Color(UIColor(named: "AccentColor") ?? .systemBlue)
) {
self.tasks = tasks
self._isPresented = isPresented
self.tintColor = tintColor
self.questionnaireResponse = questionnaireResponse
self.questionnaireResponse = result
}


func makeCoordinator() -> Coordinator {
Coordinator(isPresented: $isPresented, questionnaireResponse: questionnaireResponse)
Coordinator(result: questionnaireResponse)
}

func updateUIViewController(_ uiViewController: ORKTaskViewController, context: Context) {
Expand Down
18 changes: 0 additions & 18 deletions Sources/SpeziQuestionnaire/QuestionnaireConstraint.swift

This file was deleted.

48 changes: 0 additions & 48 deletions Sources/SpeziQuestionnaire/QuestionnaireDataSource.swift

This file was deleted.

20 changes: 20 additions & 0 deletions Sources/SpeziQuestionnaire/QuestionnaireResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import ModelsR4


/// The result of a questionnaire.
public enum QuestionnaireResult {
/// The questionnaire was successfully completed with a `QuestionnaireResponse`.
case completed(QuestionnaireResponse)
/// The questionnaire task was cancelled by the user.
case cancelled
/// The questionnaire task failed due to an error.
case failed
}
33 changes: 13 additions & 20 deletions Sources/SpeziQuestionnaire/QuestionnaireView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import FHIRQuestionnaires
import ModelsR4
import OSLog
import ResearchKit
import ResearchKitOnFHIR
import SwiftUI
Expand Down Expand Up @@ -35,28 +36,23 @@ import SwiftUI
/// }
/// ```
public struct QuestionnaireView: View {
@EnvironmentObject private var questionnaireDataSource: QuestionnaireDataSource

@Binding private var isPresented: Bool

private static let logger = Logger(subsystem: "edu.stanford.spezi.questionnaire", category: "QuestionnaireView")

private let questionnaire: Questionnaire
private let questionnaireResponse: ((QuestionnaireResponse) async -> Void)?
private let questionnaireResult: (QuestionnaireResult) async -> Void
private let completionStepMessage: String?


public var body: some View {
if let task = createTask(questionnaire: questionnaire) {
ORKOrderedTaskView(
tasks: task,
isPresented: $isPresented,
questionnaireResponse: { response in
await questionnaireResponse?(response)
await questionnaireDataSource.add(response)
},
result: questionnaireResult,
tintColor: .accentColor
)
.ignoresSafeArea(.container, edges: .bottom)
.ignoresSafeArea(.keyboard, edges: .bottom)
.interactiveDismissDisabled()
} else {
Text("QUESTIONNAIRE_LOADING_ERROR_MESSAGE")
}
Expand All @@ -65,19 +61,16 @@ public struct QuestionnaireView: View {

/// - Parameters:
/// - questionnaire: The `Questionnaire` that should be displayed.
/// - isPresented: Indication from the questionnaire view if should be presented (not "Done" pressed or cancelled).
/// - completionStepMessage: Optional completion message that can be appended at the end of the questionnaire.
/// - questionnaireResponse: Optional response closure that can be used to manually obtain the `QuestionnaireResponse`.
/// - questionnaireResult: Result closure that processes the ``QuestionnaireResult``.
public init(
questionnaire: Questionnaire,
isPresented: Binding<Bool> = .constant(true),
completionStepMessage: String? = nil,
questionnaireResponse: (@MainActor (QuestionnaireResponse) async -> Void)? = nil
questionnaireResult: @escaping @MainActor (QuestionnaireResult) async -> Void
) {
self.questionnaire = questionnaire
self._isPresented = isPresented
self.completionStepMessage = completionStepMessage
self.questionnaireResponse = questionnaireResponse
self.questionnaireResult = questionnaireResult
}


Expand All @@ -96,17 +89,17 @@ public struct QuestionnaireView: View {
do {
return try ORKNavigableOrderedTask(questionnaire: questionnaire, completionStep: completionStep)
} catch {
print("Error creating task: \(error)")
Self.logger.error("Failed to create ORK task: \(error)")
return nil
}
}
}


#if DEBUG
struct QuestionnaireView_Previews: PreviewProvider {
static var previews: some View {
QuestionnaireView(questionnaire: Questionnaire.dateTimeExample, isPresented: .constant(false))
#Preview {
QuestionnaireView(questionnaire: .dateTimeExample) { response in
print("Received response \(response)")
}
}
#endif
Loading

0 comments on commit 4f3e1cd

Please sign in to comment.