Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown Checkbox Functionality #54

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ extension ConsentDocument {
Text(markdown)
.padding()

if checkboxSnapshot != nil {
Image(uiImage: checkboxSnapshot!)
}

Spacer()

ZStack(alignment: .bottomLeading) {
Expand Down Expand Up @@ -101,7 +105,10 @@ extension ConsentDocument {
/// - Returns: The exported consent form in PDF format as a PDFKit `PDFDocument`
@MainActor
func export() async -> PDFDocument? {
let markdown = await asyncMarkdown()
var markdown = await asyncMarkdown()
if cleanedMarkdownData != nil {
markdown = cleanedMarkdownData!
}

let markdownString = (try? AttributedString(
markdown: markdown,
Expand Down
229 changes: 197 additions & 32 deletions Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import PencilKit
import SpeziPersonalInfo
import SpeziViews
import SwiftUI

import SpeziValidation

/// Display markdown-based consent documents that can be signed and exported.
///
Expand Down Expand Up @@ -57,7 +57,15 @@ public struct ConsentDocument: View {
#endif
@State var signatureSize: CGSize = .zero
@Binding private var viewState: ConsentViewState
public static var checked: [String: String] = [:]
@State public var checkboxSnapshot: UIImage?
@State public var allElements = [MarkdownElement]()

public enum MarkdownElement: Hashable {
case signature(String)
case checkbox(String, [String])
case text(String)
}

private var nameView: some View {
VStack {
Expand Down Expand Up @@ -90,6 +98,128 @@ public struct ConsentDocument: View {
Divider()
}
}

private func extractMarkdownCB() async -> [MarkdownElement] {
let data = await asyncMarkdown()
let dataString = String(data: data, encoding: .utf8)!

var elements = [MarkdownElement]()
var textBeforeCB = ""
var searchForOptions = false
var textCB = ""
var options: [String] = []

let lines = dataString.split(separator: "\n", omittingEmptySubsequences: false)
print(lines)

for line in lines {
print("plain line: " + line)
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
print("trimmed line: " + trimmedLine)

if trimmedLine.hasPrefix("- [ ]") {
if !textBeforeCB.isEmpty {
print("currentText:")
print(textBeforeCB)
elements.append(.text(textBeforeCB))
print(elements)
textBeforeCB = ""
}

textCB = trimmedLine.dropFirst(5).trimmingCharacters(in: .whitespaces)
print("task:")
print(textCB)
searchForOptions = true

} else if searchForOptions {
print("search for options")

if trimmedLine.hasPrefix(">") {
print("an option")
let option = trimmedLine.dropFirst(1).trimmingCharacters(in: .whitespaces)
options.append(option)

} else {
searchForOptions = false

if !options.isEmpty {
elements.append(.checkbox(textCB, options))
options = []
textCB = ""
} else {
elements.append(.checkbox(textCB, ["Yes", "No"]))
textCB = ""
}

textBeforeCB += line + "\n"
}

} else {
searchForOptions = false
textBeforeCB += line + "\n"
}
}

if !textBeforeCB.isEmpty {
elements.append(.text(textBeforeCB))
}

return elements
}


struct CheckBoxView: View {
let question: String
let options: [String]

@State private var elementSelected = "-"
@ValidationState private var validation

var body: some View {
VStack {
HStack {
Text(question)
.frame(maxWidth: .infinity, alignment: .leading)

Menu {
ForEach(options, id: \.self) { theElement in
Button(theElement) {
withAnimation {
elementSelected = theElement
ConsentDocument.checked[question] = theElement
}
}
}
} label: {
Text(elementSelected)
.font(.system(size: 17))
.padding(5)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(5)
}
.validate(elementSelected != "-", message: "Please select an option")
}

if let firstValidationError = validation.allDisplayedValidationResults.first {
Text(firstValidationError.message)
.foregroundColor(.red)
.font(.caption)
.padding(.top, 4)
}

Divider()
.gridCellUnsizedAxes(.horizontal)
}
.receiveValidation(in: $validation)
.onChange(of: elementSelected) { _, _ in
validation.validateSubviews()
}
.onAppear {
validation.validateSubviews()
}
}
}

private var nameInputView: some View {
Grid(horizontalSpacing: 15) {
Expand Down Expand Up @@ -131,10 +261,31 @@ public struct ConsentDocument: View {
}
}
}

public var body: some View {
VStack {
MarkdownView(asyncMarkdown: asyncMarkdown, state: $viewState.base)
ScrollView {
ForEach(allElements, id: \.self) { element in
switch element {
case .text(let text):
if let data = text.data(using: .utf8) {
MarkdownView(asyncMarkdown: {data}, state: $viewState.base)
}
case .checkbox(let question, let options):
CheckBoxView(
question: question,
options: options
)
.onAppear {
if let selectedOption = ConsentDocument.checked[question] {
ConsentDocument.checked[question] = selectedOption
} else {
ConsentDocument.checked[question] = "-"
}
}
case .signature:
signatureView
}
}

Spacer()
Group {
nameView
Expand All @@ -145,50 +296,64 @@ public struct ConsentDocument: View {
signatureView
}
}
.frame(maxWidth: Self.maxWidthDrawing) // Limit the max view size so it fits on the PDF
.frame(maxWidth: Self.maxWidthDrawing) // Limit the max view size so it fits on the PDF
}
.transition(.opacity)
.animation(.easeInOut, value: viewState == .namesEntered)
.onChange(of: viewState) {
if case .export = viewState {
Task {
guard let exportedConsent = await export() else {
viewState = .base(.error(Error.memoryAllocationError))
return
.transition(.opacity)
.animation(.easeInOut, value: viewState == .namesEntered)
.onChange(of: viewState) {
if case .export = viewState {

// print(checked)
// let renderer = ImageRenderer(content: checkboxesView)
// if let uiImage = renderer.uiImage {
// checkboxSnapshot = uiImage
// }
// Task {
// guard let exportedConsent = await export() else {
// viewState = .base(.error(Error.memoryAllocationError))
// return
// }
// viewState = .exported(document: exportedConsent)
// }
} else if case .base(let baseViewState) = viewState,
case .idle = baseViewState {
// Reset view state to correct one after handling an error view state via `.viewStateAlert()`
#if !os(macOS)
let isSignatureEmpty = signature.strokes.isEmpty
#else
let isSignatureEmpty = signature.isEmpty
#endif

if !isSignatureEmpty {
viewState = .signed
} else if !(name.givenName?.isEmpty ?? true) || !(name.familyName?.isEmpty ?? true) {
viewState = .namesEntered
}
}
viewState = .exported(document: exportedConsent)
}
} else if case .base(let baseViewState) = viewState,
case .idle = baseViewState {
// Reset view state to correct one after handling an error view state via `.viewStateAlert()`
#if !os(macOS)
let isSignatureEmpty = signature.strokes.isEmpty
#else
let isSignatureEmpty = signature.isEmpty
#endif

if !isSignatureEmpty {
viewState = .signed
} else if !(name.givenName?.isEmpty ?? true) || !(name.familyName?.isEmpty ?? true) {
viewState = .namesEntered
}
.viewStateAlert(state: $viewState.base)
.onAppear {
Task {
let elements = await extractMarkdownCB()
allElements = elements
for case let .checkbox(question, options) in elements {
ConsentDocument.checked[question] = "-"
}
}
.viewStateAlert(state: $viewState.base)
}
}

private var inputFieldsDisabled: Bool {
viewState == .base(.processing) || viewState == .export
}


/// Creates a `ConsentDocument` which renders a consent document with a markdown view.
///
/// The passed ``ConsentViewState`` indicates in which state the view currently is.
/// This is especially useful for exporting the consent form as well as error management.
/// - Parameters:
/// - markdown: The markdown content provided as an UTF8 encoded `Data` instance that can be provided asynchronously.
/// - viewState: A `Binding` to observe the ``ConsentViewState`` of the ``ConsentDocument``.
/// - viewState: A `Binding` to observe the ``ConsentViewState`` of the ``ConsentDocument``.
/// - givenNameTitle: The localization to use for the given (first) name field.
/// - givenNamePlaceholder: The localization to use for the given name field placeholder.
/// - familyNameTitle: The localization to use for the family (last) name field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import PDFKit
import SpeziOnboarding
import SpeziViews
import SwiftUI

import QuickLook

struct OnboardingConsentMarkdownRenderingView: View {
@Environment(OnboardingNavigationPath.self) private var path
@Environment(ExampleStandard.self) private var standard
@State var exportedConsent: PDFDocument?

@State var pdfURL: URL?

var body: some View {
VStack {
Expand All @@ -41,6 +41,15 @@ struct OnboardingConsentMarkdownRenderingView: View {
.padding()
)
}
Button("PDF preview") {
if let document = exportedConsent, let data = document.dataRepresentation() {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("consent.pdf")
try? data.write(to: tempURL)
pdfURL = tempURL
}
}
.quickLookPreview($pdfURL)
.buttonStyle(.borderedProminent)

Button {
path.nextStep()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct OnboardingConsentMarkdownTestView: View {
var body: some View {
OnboardingConsentView(
markdown: {
Data("This is a *markdown* **example**".utf8)
Data("This is a *markdown* **example** [May we contact you for future studies?]".utf8)
},
action: {
path.nextStep()
Expand Down