Skip to content

Commit

Permalink
Allowing to identify multiple consent forms during onboarding via a n…
Browse files Browse the repository at this point in the history
…ew parameter "identifier" added to OnboardingConsentView and OnboardingConstraint.store(...).

The parameter can be optionally specified when using an OnboardingConsentView to distinguish multiple consent forms when storing. If not specified in OnboardingConsentView, the default value „DefaultConsentDocument“ is set.

UITests/TestApp has been changed accordingly to test the new functionality of having multiple (in this case two) consent forms.
OnboardingConsentMarkdownTestView and OnboardingConsentMarkdownRenderingView have been renamed to OnboardingFirstConsentMarkdownRenderingView and
OnboardingFirstConsentMarkdownRenderingView accordingly. Views for a second consent form were added (OnboardingSecondConsentMarkdownRenderingView and -TestView).
ExampleStandard has been changed to distinguish the two documents based on their identifiers ("FirstConsentDocument" and "SecondConsentDocument") during store and loadConsentDocument.
  • Loading branch information
RealLast committed Jun 19, 2024
1 parent 8d6dda3 commit 2b70a78
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 50 deletions.
12 changes: 10 additions & 2 deletions Sources/SpeziOnboarding/OnboardingConsentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import SwiftUI
/// The `OnboardingConsentView` builds on top of the SpeziOnboarding ``ConsentDocument``
/// by providing a more developer-friendly, convenient API with additional functionalities like the share consent option.
///
/// If you want to use multiple `OnboardingConsentView`, you can provide each with an identifier (see below).
/// The identifier allows to distinguish the consent forms in the `Standard`.
///
/// ```swift
/// OnboardingConsentView(
/// markdown: {
Expand All @@ -30,6 +33,7 @@ import SwiftUI
/// // The action that should be performed once the user has provided their consent.
/// },
/// title: "Consent", // Configure the title of the consent view
/// identifier: "MyFirstConsentForm", // Specify a unique identifier for the consent form, helpful for distinguishing consent forms when storing.
/// exportConfiguration: .init(paperSize: .usLetter) // Configure the properties of the exported consent form
/// )
/// ```
Expand All @@ -46,8 +50,9 @@ public struct OnboardingConsentView: View {
private let markdown: () async -> Data
private let action: () async -> Void
private let title: LocalizedStringResource?
private let identifier: String
private let exportConfiguration: ConsentDocument.ExportConfiguration

@Environment(OnboardingDataSource.self) private var onboardingDataSource
@State private var viewState: ConsentViewState = .base(.idle)
@State private var willShowShareSheet = false
Expand Down Expand Up @@ -90,7 +95,7 @@ public struct OnboardingConsentView: View {
if !willShowShareSheet {
Task { @MainActor in
/// Stores the finished PDF in the Spezi `Standard`.
await onboardingDataSource.store(exportedConsentDocumented)
await onboardingDataSource.store(exportedConsentDocumented, identifier: identifier)
await action()
}
} else {
Expand Down Expand Up @@ -166,17 +171,20 @@ public struct OnboardingConsentView: View {
/// - markdown: The markdown content provided as an UTF8 encoded `Data` instance that can be provided asynchronously.
/// - action: The action that should be performed once the consent is given.
/// - title: The title of the view displayed at the top. Can be `nil`, meaning no title is displayed.
/// - identifier: A unique identifier or "name" for the consent form, helpful for distinguishing consent forms when storing in the `Standard`.
/// - exportConfiguration: Defines the properties of the exported consent form via ``ConsentDocument/ExportConfiguration``.
public init(
markdown: @escaping () async -> Data,
action: @escaping () async -> Void,
title: LocalizedStringResource? = LocalizationDefaults.consentFormTitle,
identifier: String = "DefaultConsentDocument",
exportConfiguration: ConsentDocument.ExportConfiguration = .init()
) {
self.markdown = markdown
self.exportConfiguration = exportConfiguration
self.title = title
self.action = action
self.identifier = identifier
}
}

Expand Down
6 changes: 4 additions & 2 deletions Sources/SpeziOnboarding/OnboardingConstraint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import Spezi
public protocol OnboardingConstraint: Standard {
/// Adds a new exported consent form represented as `PDFDocument` to the `Standard` conforming to ``OnboardingConstraint``.
///
/// - Parameter consent: The exported consent form represented as `PDFDocument` that should be added.
func store(consent: PDFDocument) async
/// - Parameters:
/// - consent: The exported consent form represented as `PDFDocument` that should be added.
/// - identifier: A 'String' identifying the consent form as specified in OnboardingConsentView.
func store(consent: PDFDocument, identifier: String) async
}
4 changes: 2 additions & 2 deletions Sources/SpeziOnboarding/OnboardingDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ public class OnboardingDataSource: Module, EnvironmentAccessible {
/// Adds a new exported consent form represented as `PDFDocument` to the ``OnboardingDataSource``.
///
/// - Parameter consent: The exported consent form represented as `PDFDocument` that should be added.
public func store(_ consent: PDFDocument) async {
public func store(_ consent: PDFDocument, identifier: String) async {
Task { @MainActor in
await standard.store(consent: consent)
await standard.store(consent: consent, identifier: identifier)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ OnboardingConsentView(
action: {
// Action to perform once the user has given their consent
},
identifier: "MyFirstConsentForm", // Specify an optional unique identifier for the consent form, helpful for distinguishing consent forms when storing.
exportConfiguration: .init(paperSize: .usLetter) // Configure the properties of the exported consent form
)
```
Expand Down
34 changes: 29 additions & 5 deletions Tests/UITests/TestApp/ExampleStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,43 @@ import SwiftUI

/// An example Standard used for the configuration.
actor ExampleStandard: Standard, EnvironmentAccessible {
@Published @MainActor var consentData: PDFDocument = .init()
@Published @MainActor var firstConsentData: PDFDocument = .init()
@Published @MainActor var secondConsentData: PDFDocument = .init()

Check failure on line 19 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Vertical Whitespace before Closing Braces Violation: Don't include vertical whitespace (empty line) before closing braces (vertical_whitespace_closing_braces)
}


extension ExampleStandard: OnboardingConstraint {
func store(consent: PDFDocument) async {
func store(consent: PDFDocument, identifier: String) async {
await MainActor.run {
self.consentData = consent
if(identifier == "FirstConsentDocument")

Check failure on line 26 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)

Check warning on line 26 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests iOS (Debug, TestApp-iOS.xcresult, TestApp-iOS.xcresult) / Test using xcodebuild or run fastlane

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
{

Check failure on line 27 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Opening Brace Spacing Violation: Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)
self.firstConsentData = consent
}
if(identifier == "SecondConsentDocument")

Check failure on line 30 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
{

Check failure on line 31 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Opening Brace Spacing Violation: Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)
self.secondConsentData = consent
}
}
try? await Task.sleep(for: .seconds(0.5))
}

func loadConsent() async throws -> PDFDocument {
await self.consentData
func loadConsentDocument(identifier: String) async throws -> PDFDocument? {

if(identifier == "FirstConsentDocument")

Check failure on line 40 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
{

Check failure on line 41 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Opening Brace Spacing Violation: Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)

Check warning on line 41 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests iOS (Debug, TestApp-iOS.xcresult, TestApp-iOS.xcresult) / Test using xcodebuild or run fastlane

Opening Brace Spacing Violation: Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)
return await self.firstConsentData
}
if(identifier == "SecondConsentDocument")

Check failure on line 44 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)

Check warning on line 44 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests iOS (Debug, TestApp-iOS.xcresult, TestApp-iOS.xcresult) / Test using xcodebuild or run fastlane

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
{

Check failure on line 45 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Opening Brace Spacing Violation: Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)
return await self.secondConsentData
}

// In case an invalid identifier is provided, return nil.
// The OnboardingConsentMarkdownRenderingView checks if the document
// is nil, and if so, displays an error.
return nil
}

Check failure on line 54 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Vertical Whitespace before Closing Braces Violation: Don't include vertical whitespace (empty line) before closing braces (vertical_whitespace_closing_braces)

Check warning on line 54 in Tests/UITests/TestApp/ExampleStandard.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests iOS (Debug, TestApp-iOS.xcresult, TestApp-iOS.xcresult) / Test using xcodebuild or run fastlane

Vertical Whitespace before Closing Braces Violation: Don't include vertical whitespace (empty line) before closing braces (vertical_whitespace_closing_braces)

}
6 changes: 4 additions & 2 deletions Tests/UITests/TestApp/OnboardingTestsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ struct OnboardingTestsView: View {
)
OnboardingWelcomeTestView()
OnboardingSequentialTestView()
OnboardingConsentMarkdownTestView()
OnboardingConsentMarkdownRenderingView()
OnboardingFirstConsentMarkdownTestView()
OnboardingFirstConsentMarkdownRenderingView()
OnboardingSecondConsentMarkdownTestView()
OnboardingSecondConsentMarkdownRenderingView()
OnboardingTestViewNotIdentifiable(text: "Leland").onboardingIdentifier("a")
OnboardingTestViewNotIdentifiable(text: "Stanford").onboardingIdentifier("b")
OnboardingCustomToggleTestView(showConditionalView: $showConditionalView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import SpeziViews
import SwiftUI


struct OnboardingConsentMarkdownRenderingView: View {
struct OnboardingFirstConsentMarkdownRenderingView: View {

Check warning on line 15 in Tests/UITests/TestApp/Views/OnboardingFirstConsentMarkdownRenderingView.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests iOS (Debug, TestApp-iOS.xcresult, TestApp-iOS.xcresult) / Test using xcodebuild or run fastlane

Type Name Violation: Type name 'OnboardingFirstConsentMarkdownRenderingView' should be between 3 and 40 characters long (type_name)
@Environment(OnboardingNavigationPath.self) private var path
@Environment(ExampleStandard.self) private var standard
@State var exportedConsent: PDFDocument?


private var documentIdentifier = "FirstConsentDocument"

var body: some View {
VStack {
Expand All @@ -25,7 +26,7 @@ struct OnboardingConsentMarkdownRenderingView: View {
.fill(Color.red)
.frame(width: 200, height: 200)
.overlay(
Text("Consent PDF rendering doesn't exist")
Text("First Consent PDF rendering doesn't exist")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.padding()
Expand All @@ -35,7 +36,7 @@ struct OnboardingConsentMarkdownRenderingView: View {
.fill(Color.green)
.frame(width: 200, height: 200)
.overlay(
Text("Consent PDF rendering exists")
Text("First Consent PDF rendering exists")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.padding()
Expand All @@ -54,9 +55,9 @@ struct OnboardingConsentMarkdownRenderingView: View {
.navigationBarTitleDisplayMode(.inline)
#endif
.task {
self.exportedConsent = try? await standard.loadConsent()
self.exportedConsent = try? await standard.loadConsentDocument(identifier: documentIdentifier)
// Reset OnboardingDataSource
await standard.store(consent: .init())
await standard.store(consent: .init(), identifier: documentIdentifier)
}
}
}
Expand All @@ -68,7 +69,7 @@ struct OnboardingConsentMarkdownRenderingView_Previews: PreviewProvider {


static var previews: some View {
OnboardingStack(startAtStep: OnboardingConsentMarkdownRenderingView.self) {
OnboardingStack(startAtStep: OnboardingFirstConsentMarkdownRenderingView.self) {
for onboardingView in OnboardingFlow.previewSimulatorViews {
onboardingView
.environment(standard)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,32 @@ import SpeziViews
import SwiftUI


struct OnboardingConsentMarkdownTestView: View {
struct OnboardingFirstConsentMarkdownTestView: View {
@Environment(OnboardingNavigationPath.self) private var path

private var documentIdentifier = "FirstConsentDocument"


var body: some View {
OnboardingConsentView(
markdown: {
Data("This is a *markdown* **example**".utf8)
Data("This is the first *markdown* **example**".utf8)
},
action: {
path.nextStep()
},
title: "First Consent",
identifier: documentIdentifier,
exportConfiguration: .init(paperSize: .dinA4, includingTimestamp: true)
)
}
}


#if DEBUG
struct OnboardingConsentMarkdownTestView_Previews: PreviewProvider {
struct OnboardingFirstConsentMarkdownTestView_Previews: PreviewProvider {
static var previews: some View {
OnboardingStack(startAtStep: OnboardingConsentMarkdownTestView.self) {
OnboardingStack(startAtStep: OnboardingFirstConsentMarkdownTestView.self) {
for onboardingView in OnboardingFlow.previewSimulatorViews {
onboardingView
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ enum OnboardingFlow {
OnboardingStartTestView(showConditionalView: .constant(true)),
OnboardingWelcomeTestView(),
OnboardingSequentialTestView(),
OnboardingConsentMarkdownTestView(),
OnboardingConsentMarkdownRenderingView(),
OnboardingFirstConsentMarkdownTestView(),
OnboardingFirstConsentMarkdownRenderingView(),
OnboardingSecondConsentMarkdownTestView(),
OnboardingSecondConsentMarkdownRenderingView(),
OnboardingCustomTestView1(exampleArgument: "test"),
OnboardingCustomTestView2(),
OnboardingConditionalTestView()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import PDFKit
import SpeziOnboarding
import SpeziViews
import SwiftUI


struct OnboardingSecondConsentMarkdownRenderingView: View {

Check warning on line 15 in Tests/UITests/TestApp/Views/OnboardingSecondConsentMarkdownRenderingView.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests iOS (Debug, TestApp-iOS.xcresult, TestApp-iOS.xcresult) / Test using xcodebuild or run fastlane

Type Name Violation: Type name 'OnboardingSecondConsentMarkdownRenderingView' should be between 3 and 40 characters long (type_name)
@Environment(OnboardingNavigationPath.self) private var path
@Environment(ExampleStandard.self) private var standard
@State var exportedConsent: PDFDocument?

private var documentIdentifier = "SecondConsentDocument"

var body: some View {
VStack {
if (exportedConsent?.pageCount ?? 0) == 0 {
Circle()
.fill(Color.red)
.frame(width: 200, height: 200)
.overlay(
Text("Second Consent PDF rendering doesn't exist")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.padding()
)
} else {
Circle()
.fill(Color.green)
.frame(width: 200, height: 200)
.overlay(
Text("Second Consent PDF rendering exists")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.padding()
)
}

Button {
path.nextStep()
} label: {
Text("Next")
}
.buttonStyle(.borderedProminent)
}
.padding()
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.task {
self.exportedConsent = try? await standard.loadConsentDocument(identifier: documentIdentifier)
// Reset OnboardingDataSource
await standard.store(consent: .init(), identifier: documentIdentifier)
}
}
}


#if DEBUG
struct OnboardingSecondConsentMarkdownRenderingView_Previews: PreviewProvider {
static var standard: OnboardingDataSource = .init()


static var previews: some View {
OnboardingStack(startAtStep: OnboardingSecondConsentMarkdownRenderingView.self) {
for onboardingView in OnboardingFlow.previewSimulatorViews {
onboardingView
.environment(standard)
}
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SpeziOnboarding
import SpeziViews
import SwiftUI


struct OnboardingSecondConsentMarkdownTestView: View {
@Environment(OnboardingNavigationPath.self) private var path

private var documentIdentifier = "SecondConsentDocument"


var body: some View {
OnboardingConsentView(
markdown: {
Data("This is the second *markdown* **example**".utf8)
},
action: {
path.nextStep()
},
title: "Second Consent",
identifier: documentIdentifier,
exportConfiguration: .init(paperSize: .dinA4, includingTimestamp: true)
)
}
}


#if DEBUG
struct OnboardingSecondConsentMarkdownTestView_Previews: PreviewProvider {
static var previews: some View {
OnboardingStack(startAtStep: OnboardingSecondConsentMarkdownTestView.self) {
for onboardingView in OnboardingFlow.previewSimulatorViews {
onboardingView
}
}
}
}
#endif
Loading

0 comments on commit 2b70a78

Please sign in to comment.