Skip to content

Commit

Permalink
Consent View (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
PSchmiedmayer committed Dec 17, 2022
1 parent 3cdf7df commit b02f0da
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 19 deletions.
138 changes: 138 additions & 0 deletions Sources/Onboarding/ConsentView.swift
@@ -0,0 +1,138 @@
//
// This source file is part of the CardinalKit open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import PencilKit
import SwiftUI
import Views


/// The ``ConsentView`` allows the display of markdown-based documents that can be signed using a family and given name and a hand drawn signature.
///
/// The ``ConsentView`` provides a convenience initalizer with a provided action view using an ``OnboardingActionsView`` (``ConsentView/init(header:asyncMarkdown:footer:action:)``)
/// or a more customized ``ConsentView/init(contentView:actionView:)`` initializer with a custom-provided content and action view.
///
/// ```
/// ConsentView(
/// asyncMarkdown: {
/// Data("This is a *markdown* **example**".utf8)
/// },
/// action: {
/// // The action that should be performed once the user has providided their consent.
/// }
/// )
/// ```
public struct ConsentView<ContentView: View, Action: View>: View {
private let contentView: ContentView
private let action: Action
@State private var name = PersonNameComponents()
@State private var showSignatureView = false
@State private var isSigning = false
@State private var signature = PKDrawing()


public var body: some View {
ScrollViewReader { proxy in
OnboardingView(
contentView: {
contentView
},
actionView: {
VStack {
Divider()
NameFields(name: $name)
if showSignatureView {
Divider()
SignatureView(signature: $signature, isSigning: $isSigning, name: name)
.padding(.vertical, 4)
}
Divider()
action
.disabled(buttonDisabled)
.animation(.easeInOut, value: buttonDisabled)
.id("ActionButton")
.onChange(of: showSignatureView) { _ in
proxy.scrollTo("ActionButton")
}
}
.transition(.opacity)
.animation(.easeInOut, value: showSignatureView)
}
)
.scrollDisabled(isSigning)
}
}

var buttonDisabled: Bool {
let showSignatureView = !(name.givenName?.isEmpty ?? true) && !(name.familyName?.isEmpty ?? true)
if !self.showSignatureView && showSignatureView {
Task { @MainActor in
self.showSignatureView = showSignatureView
}
}

return signature.strokes.isEmpty || (name.givenName?.isEmpty ?? true) || (name.familyName?.isEmpty ?? true)
}


/// Creates a ``ConsentView`` with a provided action view using an``OnboardingActionsView`` and renders a markdown view.
/// - Parameters:
/// - header: The header view will be displayed above the markdown content.
/// - asyncMarkdown: The markdown content provided as an UTF8 encoded `Data` instance that can be provided asynchronously.
/// - footer: The footer view will be displayed above the markdown content.
/// - action: The action that should be performed once the consent has been given.
public init(
@ViewBuilder header: () -> (some View) = { EmptyView() },
asyncMarkdown: @escaping () async -> Data,
@ViewBuilder footer: () -> (some View) = { EmptyView() },
action: @escaping () -> Void
) where ContentView == MarkdownView<AnyView, AnyView>, Action == OnboardingActionsView {
self.init(
contentView: {
MarkdownView(
asyncMarkdown: asyncMarkdown,
header: { AnyView(header()) },
footer: { AnyView(footer()) }
)
},
actionView: {
OnboardingActionsView(String(localized: "CONSENT_ACTION", bundle: .module)) {
action()
}
}
)
}

/// Creates a ``ConsentView`` with a custom-provided action view.
/// - Parameters:
/// - contentView: The content view providing context about the consent view.
/// - actionView: The action view that should be displayed under the name and signature boxes.
public init(
@ViewBuilder contentView: () -> (ContentView),
@ViewBuilder actionView: () -> (Action)
) {
self.contentView = contentView()
self.action = actionView()
}
}


struct ConsentView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
ConsentView(
asyncMarkdown: {
Data("This is a *markdown* **example**".utf8)
},
action: {
print("Next step ...")
}
)
.navigationTitle("Consent")
}
}
}
4 changes: 2 additions & 2 deletions Sources/Onboarding/OnboardingView.swift
Expand Up @@ -74,9 +74,9 @@ public struct OnboardingView<TitleView: View, ContentView: View, ActionView: Vie
/// - contentView: The content view.
/// - actionView: The action view displayed at the bottom.
public init(
@ViewBuilder titleView: () -> TitleView,
@ViewBuilder titleView: () -> TitleView = { EmptyView() },
@ViewBuilder contentView: () -> ContentView,
@ViewBuilder actionView: () -> ActionView? = { nil }
@ViewBuilder actionView: () -> ActionView
) {
self.titleView = titleView()
self.contentView = contentView()
Expand Down
7 changes: 7 additions & 0 deletions Sources/Onboarding/Resources/de.lproj/Localizable.strings
Expand Up @@ -6,6 +6,13 @@
// SPDX-License-Identifier: MIT
//

// MARK: Consent View
"CONSENT_ACTION" = "Ich Stimme Zu";


// MARK: Sequential Onboarding
"SEQUENTIAL_ONBOARDING_NEXT" = "Nächster Schritt";


// MARK: Signature View
"SIGNATURE_VIEW_UNDO" = "Rückgängig Machen";
7 changes: 7 additions & 0 deletions Sources/Onboarding/Resources/en.lproj/Localizable.strings
Expand Up @@ -6,6 +6,13 @@
// SPDX-License-Identifier: MIT
//

// MARK: Consent View
"CONSENT_ACTION" = "I Consent";


// MARK: Sequential Onboarding
"SEQUENTIAL_ONBOARDING_NEXT" = "Next";


// MARK: Signature View
"SIGNATURE_VIEW_UNDO" = "Undo";
99 changes: 99 additions & 0 deletions Sources/Onboarding/SignatureView.swift
@@ -0,0 +1,99 @@
//
// This source file is part of the CardinalKit open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import PencilKit
import SwiftUI
import Views


/// A ``SignatureView`` enables the collection of signatures using a view that allows freeform signatures using a finger and the Apple Pencil.
///
/// Use SwiftUI `Bindings` to obtain information like the content of the signature and if the user is currently signing:
/// ```
/// @State var signature = PKDrawing()
/// @State var isSigning = false
///
///
/// SignatureView(
/// signature: $signature,
/// isSigning: $isSigning,
/// name: name
/// )
/// ```
public struct SignatureView: View {
@Environment(\.undoManager) private var undoManager
@Binding private var signature: PKDrawing
@Binding private var isSigning: Bool
@State private var canUndo = false
private let name: PersonNameComponents
private let lineOffset: CGFloat


public var body: some View {
VStack {
ZStack(alignment: .bottomLeading) {
Color(.secondarySystemBackground)
Rectangle()
.fill(.secondary)
.frame(maxWidth: .infinity, maxHeight: 1)
.padding(.horizontal, 20)
.padding(.bottom, lineOffset)
Text("X")
.font(.title2)
.foregroundColor(.secondary)
.padding(.horizontal, 20)
.padding(.bottom, lineOffset + 2)
Text(name.formatted(.name(style: .long)))
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.horizontal, 20)
.padding(.bottom, lineOffset - 18)
CanvasView(drawing: $signature, isDrawing: $isSigning, showToolPicker: .constant(false))
}
.frame(height: 120)
Button(String(localized: "SIGNATURE_VIEW_UNDO", bundle: .module)) {
undoManager?.undo()
canUndo = undoManager?.canUndo ?? false
}
.disabled(!canUndo)
}
.onChange(of: isSigning) { _ in
Task { @MainActor in
canUndo = undoManager?.canUndo ?? false
}
}
.transition(.opacity)
.animation(.easeInOut, value: canUndo)
}


/// Creates a new instance of an ``SignatureView``.
/// - Parameters:
/// - signature: A `Binding` containing the current signature as an `PKDrawing`.
/// - isSigning: A `Binding` indicating if the user is currently signing.
/// - name: The name that is deplayed under the signature line.
/// - lineOffset: Defines the distance of the signature line from the bottom of the view. The default value is 30.
init(
signature: Binding<PKDrawing> = .constant(PKDrawing()),
isSigning: Binding<Bool> = .constant(false),
name: PersonNameComponents = PersonNameComponents(),
lineOffset: CGFloat = 30
) {
self._signature = signature
self._isSigning = isSigning
self.name = name
self.lineOffset = lineOffset
}
}


struct SignatureView_Previews: PreviewProvider {
static var previews: some View {
SignatureView()
}
}
21 changes: 20 additions & 1 deletion Tests/UITests/TestApp/OnboardingTests/OnboardingTestsView.swift
Expand Up @@ -13,6 +13,7 @@ import SwiftUI

struct OnboardingTestsView: View {
enum OnboardingStep: String, CaseIterable, Codable {
case consentView = "Consent View"
case onboardingView = "Onboarding View"
case sequentialOnboarding = "Sequential Onboarding"
}
Expand All @@ -28,6 +29,8 @@ struct OnboardingTestsView: View {
.navigationTitle("Onboarding")
.navigationDestination(for: OnboardingStep.self) { onboardingStep in
switch onboardingStep {
case .consentView:
consentView
case .onboardingView:
onboardingView
case .sequentialOnboarding:
Expand All @@ -36,6 +39,22 @@ struct OnboardingTestsView: View {
}
}


private var consentView: some View {
ConsentView(
header: {
OnboardingTitleView(title: "Consent", subtitle: "Version 1.0")
},
asyncMarkdown: {
Data("This is a *markdown* **example**".utf8)
},
action: {
path.append(OnboardingStep.onboardingView)
}
)
.navigationBarTitleDisplayMode(.inline)
}

private var onboardingView: some View {
OnboardingView(
title: "Welcome",
Expand Down Expand Up @@ -64,7 +83,7 @@ struct OnboardingTestsView: View {
],
actionText: "Continue"
) {
path.append(OnboardingStep.onboardingView)
path.append(OnboardingStep.consentView)
}
.navigationBarTitleDisplayMode(.inline)
}
Expand Down
12 changes: 6 additions & 6 deletions Tests/UITests/TestAppUITests/HealthKitTests.swift
Expand Up @@ -52,7 +52,7 @@ final class HealthKitTests: TestAppUITests {

try exitAppAndOpenHealth(.electrocardiograms)

sleep(1)
sleep(2)

XCTAssertEqual(
HealthDataType.numberOfHKTypeNames(in: app),
Expand All @@ -61,7 +61,7 @@ final class HealthKitTests: TestAppUITests {

try exitAppAndOpenHealth(.steps)

sleep(1)
sleep(2)

XCTAssertEqual(
HealthDataType.numberOfHKTypeNames(in: app),
Expand All @@ -70,7 +70,7 @@ final class HealthKitTests: TestAppUITests {

try exitAppAndOpenHealth(.pushes)

sleep(1)
sleep(2)

XCTAssertEqual(
HealthDataType.numberOfHKTypeNames(in: app),
Expand All @@ -79,7 +79,7 @@ final class HealthKitTests: TestAppUITests {

try exitAppAndOpenHealth(.restingHeartRate)

sleep(1)
sleep(2)

XCTAssertEqual(
HealthDataType.numberOfHKTypeNames(in: app),
Expand All @@ -88,7 +88,7 @@ final class HealthKitTests: TestAppUITests {

try exitAppAndOpenHealth(.activeEnergy)

sleep(1)
sleep(2)

XCTAssertEqual(
HealthDataType.numberOfHKTypeNames(in: app),
Expand All @@ -97,7 +97,7 @@ final class HealthKitTests: TestAppUITests {

app.buttons["Trigger data source collection"].tap()

sleep(1)
sleep(2)

XCTAssertEqual(
HealthDataType.numberOfHKTypeNames(in: app),
Expand Down

0 comments on commit b02f0da

Please sign in to comment.