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

Cleanup, Swift 6, Consent Contraint and SignatureView improvements #60

Merged
merged 26 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0dcd044
Allow to use SignatureView manually
Supereg Oct 23, 2024
82299b6
Merge branch 'main' into feature/signature-view-access
philippzagar Jan 22, 2025
2d2fd61
String-based name components
philippzagar Jan 22, 2025
57ce8bc
Signature date
philippzagar Jan 22, 2025
73f34f4
Refactorings to Preview
philippzagar Jan 22, 2025
c150498
Fix export of date below signature
philippzagar Jan 22, 2025
8fd56a2
Allow to use SignatureView manually
Supereg Oct 23, 2024
bb74e07
String-based name components
philippzagar Jan 22, 2025
a6dfc2c
Signature date
philippzagar Jan 22, 2025
eab5689
Refactorings to Preview
philippzagar Jan 22, 2025
9b6e6ec
Some general improvements
philippzagar Jan 22, 2025
a0c47fc
Merge remote-tracking branch 'origin/feature/signature-view-access' i…
philippzagar Jan 22, 2025
2888134
.
philippzagar Jan 22, 2025
c619a07
doc update
philippzagar Jan 22, 2025
06edcf3
test fix
philippzagar Jan 22, 2025
ded0a33
Lots of cleanup
philippzagar Jan 23, 2025
188debf
More cleanup
philippzagar Jan 24, 2025
ce44c56
More cleanup
philippzagar Jan 24, 2025
4f6f7b5
linter
philippzagar Jan 24, 2025
ef0827f
Make tests more robust
philippzagar Jan 24, 2025
97c933d
Remove OnboardingDataSource and ConsentContraint.
philippzagar Jan 25, 2025
8f662fb
linter
philippzagar Jan 25, 2025
aa24e2f
linter
philippzagar Jan 25, 2025
8ea59c9
Fix undo manager
philippzagar Jan 26, 2025
5a4ebae
Fix undomanager, further state handelling improvements and fixes
philippzagar Jan 26, 2025
bfb2528
Final improvements
philippzagar Jan 28, 2025
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
6 changes: 0 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ let package = Package(
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "TPPDF", package: "TPPDF")
],
swiftSettings: [
.enableUpcomingFeature("ExistentialAny")
],
philippzagar marked this conversation as resolved.
Show resolved Hide resolved
plugins: [] + swiftLintPlugin()
),
.testTarget(
Expand All @@ -52,9 +49,6 @@ let package = Package(
resources: [
.process("Resources/")
],
swiftSettings: [
.enableUpcomingFeature("ExistentialAny")
],
plugins: [] + swiftLintPlugin()
)
]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ struct ConsentViewExample: View {
action: {
// Action to perform once the user has given their consent
},
exportConfiguration: .init(paperSize: .usLetter) // Configure the properties of the exported consent form
exportConfiguration: .init(paperSize: .usLetter), // Configure the properties of the exported consent form
currentDateInSignature: true // Indicates if the consent signature should include the current date.
)
}
}
Expand Down
18 changes: 15 additions & 3 deletions Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,26 @@
///
/// - Returns: The exported consent form in PDF format as a PDFKit `PDFDocument`
@MainActor
func export() async throws -> PDFKit.PDFDocument {
func export() async throws -> ConsentDocumentExport {
var documentExport = ConsentDocumentExport(
markdown: self.markdown,
exportConfiguration: self.exportConfiguration,
documentIdentifier: self.documentIdentifier
)

documentExport.signature = signature
documentExport.formattedSignatureDate = formattedConsentSignatureDate
documentExport.name = name

#if !os(macOS)
documentExport.signatureImage = blackInkSignatureImage
return try await documentExport.export()
let pdf = try await documentExport.export()
#else
return try await documentExport.export()
let pdf = try await documentExport.export()

Check warning on line 67 in Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift#L67

Added line #L67 was not covered by tests
#endif

documentExport.cachedPDF = pdf

return documentExport
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ extension ConsentDocument {
/// The ``FontSettings`` store configuration of the fonts used to render the exported
/// consent document, i.e., fonts for the content, title and signature.
public struct FontSettings: Sendable {
/// The font of the name rendered below the signature line.
public let signatureNameFont: UIFont
/// The font of the caption rendered below the signature line.
public let signatureCaptionFont: UIFont
/// The font of the prefix of the signature ("X" in most cases).
public let signaturePrefixFont: UIFont
/// The font of the content of the document (i.e., the rendered markdown text)
Expand All @@ -63,19 +63,19 @@ extension ConsentDocument {
/// Creates an instance`FontSettings` specifying the fonts of various components of the exported document
///
/// - Parameters:
/// - signatureNameFont: The font used for the signature name.
/// - signatureCaptionFont: The font used for the signature caption.
/// - signaturePrefixFont: The font used for the signature prefix text.
/// - documentContentFont: The font used for the main content of the document.
/// - headerTitleFont: The font used for the header title.
/// - headerExportTimeStampFont: The font used for the header timestamp.
public init(
signatureNameFont: UIFont,
signatureCaptionFont: UIFont,
signaturePrefixFont: UIFont,
documentContentFont: UIFont,
headerTitleFont: UIFont,
headerExportTimeStampFont: UIFont
) {
self.signatureNameFont = signatureNameFont
self.signatureCaptionFont = signatureCaptionFont
self.signaturePrefixFont = signaturePrefixFont
self.documentContentFont = documentContentFont
self.headerTitleFont = headerTitleFont
Expand All @@ -86,8 +86,8 @@ extension ConsentDocument {
/// The ``FontSettings`` store configuration of the fonts used to render the exported
/// consent document, i.e., fonts for the content, title and signature.
public struct FontSettings: @unchecked Sendable {
/// The font of the name rendered below the signature line.
public let signatureNameFont: NSFont
/// The font of the caption rendered below the signature line.
public let signatureCaptionFont: NSFont
/// The font of the prefix of the signature ("X" in most cases).
public let signaturePrefixFont: NSFont
/// The font of the content of the document (i.e., the rendered markdown text)
Expand All @@ -101,19 +101,19 @@ extension ConsentDocument {
/// Creates an instance`FontSettings` specifying the fonts of various components of the exported document
///
/// - Parameters:
/// - signatureNameFont: The font used for the signature name.
/// - signatureCaptionFont: The font used for the signature caption.
/// - signaturePrefixFont: The font used for the signature prefix text.
/// - documentContentFont: The font used for the main content of the document.
/// - headerTitleFont: The font used for the header title.
/// - headerExportTimeStampFont: The font used for the header timestamp.
public init(
signatureNameFont: NSFont,
signatureCaptionFont: NSFont,
signaturePrefixFont: NSFont,
documentContentFont: NSFont,
headerTitleFont: NSFont,
headerExportTimeStampFont: NSFont
) {
self.signatureNameFont = signatureNameFont
self.signatureCaptionFont = signatureCaptionFont
self.signaturePrefixFont = signaturePrefixFont
self.documentContentFont = documentContentFont
self.headerTitleFont = headerTitleFont
Expand All @@ -134,6 +134,7 @@ extension ConsentDocument {
/// - paperSize: The page size of the exported form represented by ``ConsentDocument/ExportConfiguration/PaperSize``.
/// - consentTitle: The title of the exported consent form.
/// - includingTimestamp: Indicates if the exported form includes a timestamp.
/// - fontSettings: Font settings for the exported form.
public init(
paperSize: PaperSize = .usLetter,
consentTitle: LocalizedStringResource = LocalizationDefaults.exportedConsentFormTitle,
Expand Down
102 changes: 61 additions & 41 deletions Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
/// In addition, it enables the export of the signed form as a PDF document.
///
/// To observe and control the current state of the `ConsentDocument`, the view requires passing down a ``ConsentViewState`` as a SwiftUI `Binding` in the
/// ``init(markdown:viewState:givenNameTitle:givenNamePlaceholder:familyNameTitle:familyNamePlaceholder:exportConfiguration:)`` initializer.
/// ``init(markdown:viewState:givenNameTitle:givenNamePlaceholder:familyNameTitle:familyNamePlaceholder:exportConfiguration:documentIdentifier:consentSignatureDate:consentSignatureDateFormatter:)`` initializer.
/// This `Binding` can then be used to trigger the export of the consent form via setting the state to ``ConsentViewState/export``.
/// After the rendering completes, the finished `PDFDocument` from Apple's PDFKit is accessible via the associated value of the view state in ``ConsentViewState/exported(document:)``.
/// After the rendering completes, the finished `PDFDocument` from Apple's PDFKit is accessible via the associated value of the view state in ``ConsentViewState/exported(document:export:)``.
/// Other possible states of the `ConsentDocument` are the SpeziViews `ViewState`'s accessible via the associated value in ``ConsentViewState/base(_:)``.
/// In addition, the view provides information about the signing progress via the ``ConsentViewState/signing`` and ``ConsentViewState/signed`` states.
///
Expand All @@ -34,7 +34,8 @@
/// Data("This is a *markdown* **example**".utf8)
/// },
/// viewState: $state,
/// exportConfiguration: .init(paperSize: .usLetter) // Configure the properties of the exported consent form
/// exportConfiguration: .init(paperSize: .usLetter), // Configure the properties of the exported consent form
/// consentSignatureDate: .now
/// )
/// ```
public struct ConsentDocument: View {
Expand All @@ -45,8 +46,12 @@
private let givenNamePlaceholder: LocalizedStringResource
private let familyNameTitle: LocalizedStringResource
private let familyNamePlaceholder: LocalizedStringResource

let documentExport: ConsentDocumentExport
private let consentSignatureDate: Date?
private let consentSignatureDateFormatter: DateFormatter

let markdown: () async -> Data
let exportConfiguration: ExportConfiguration
let documentIdentifier: String

@Environment(\.colorScheme) var colorScheme
@State var name = PersonNameComponents()
Expand Down Expand Up @@ -85,7 +90,6 @@
signature.removeAll()
#endif
}
documentExport.name = name
}

Divider()
Expand All @@ -112,9 +116,19 @@
@MainActor private var signatureView: some View {
Group {
#if !os(macOS)
SignatureView(signature: $signature, isSigning: $viewState.signing, canvasSize: $signatureSize, name: name)
SignatureView(
signature: $signature,
isSigning: $viewState.signing,
canvasSize: $signatureSize,
name: name,
formattedDate: formattedConsentSignatureDate
)
#else
SignatureView(signature: $signature, name: name)
SignatureView(
signature: $signature,
name: name,
formattedDate: formattedConsentSignatureDate
)

Check warning on line 131 in Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift#L127-L131

Added lines #L127 - L131 were not covered by tests
#endif
}
.padding(.vertical, 4)
Expand All @@ -130,13 +144,12 @@
} else {
viewState = .namesEntered
}
documentExport.signature = signature
}
}

public var body: some View {
VStack {
MarkdownView(asyncMarkdown: documentExport.asyncMarkdown, state: $viewState.base)
MarkdownView(asyncMarkdown: markdown, state: $viewState.base)
Spacer()
Group {
nameView
Expand All @@ -155,11 +168,9 @@
if case .export = viewState {
Task {
do {
/// Stores the finished PDF in the Spezi `Standard`.
// Stores the finished PDF in the Spezi `Standard`.
let exportedConsent = try await export()

documentExport.cachedPDF = exportedConsent
viewState = .exported(document: exportedConsent, export: documentExport)
viewState = .exported(document: exportedConsent)
} catch {
// In case of error, go back to previous state.
viewState = .base(.error(AnyLocalizedError(error: error)))
Expand Down Expand Up @@ -187,7 +198,15 @@
private var inputFieldsDisabled: Bool {
viewState == .base(.processing) || viewState == .export || viewState == .storing
}


var formattedConsentSignatureDate: String? {
if let consentSignatureDate {
consentSignatureDateFormatter.string(from: consentSignatureDate)
} else {
nil
}

Check warning on line 207 in Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift#L206-L207

Added lines #L206 - L207 were not covered by tests
}


/// Creates a `ConsentDocument` which renders a consent document with a markdown view.
///
Expand All @@ -202,6 +221,8 @@
/// - familyNamePlaceholder: The localization to use for the family name field placeholder.
/// - exportConfiguration: Defines the properties of the exported consent form via ``ConsentDocument/ExportConfiguration``.
/// - documentIdentifier: A unique identifier or "name" for the consent form, helpful for distinguishing consent forms when storing in the `Standard`.
/// - consentSignatureDate: The date that is displayed under the signature line.
/// - consentSignatureDateFormatter: The date formatter used to format the date that is displayed under the signature line.
public init(
markdown: @escaping () async -> Data,
viewState: Binding<ConsentViewState>,
Expand All @@ -210,43 +231,42 @@
familyNameTitle: LocalizedStringResource = LocalizationDefaults.familyNameTitle,
familyNamePlaceholder: LocalizedStringResource = LocalizationDefaults.familyNamePlaceholder,
exportConfiguration: ExportConfiguration = .init(),
documentIdentifier: String = ConsentDocumentExport.Defaults.documentIdentifier
documentIdentifier: String = ConsentDocumentExport.Defaults.documentIdentifier,
consentSignatureDate: Date? = nil,
consentSignatureDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}()
) {
self.markdown = markdown
self._viewState = viewState
self.givenNameTitle = givenNameTitle
self.givenNamePlaceholder = givenNamePlaceholder
self.familyNameTitle = familyNameTitle
self.familyNamePlaceholder = familyNamePlaceholder

self.documentExport = ConsentDocumentExport(
markdown: markdown,
exportConfiguration: exportConfiguration,
documentIdentifier: documentIdentifier
)
// Set initial values for the name and signature.
// These will be updated once the name and signature change.
self.documentExport.name = name
self.documentExport.signature = signature
self.exportConfiguration = exportConfiguration
self.documentIdentifier = documentIdentifier
self.consentSignatureDate = consentSignatureDate
self.consentSignatureDateFormatter = consentSignatureDateFormatter
}
}


#if DEBUG
struct ConsentDocument_Previews: PreviewProvider {
@State private static var viewState: ConsentViewState = .base(.idle)


static var previews: some View {
NavigationStack {
ConsentDocument(
markdown: {
Data("This is a *markdown* **example**".utf8)
},
viewState: $viewState
)
.navigationTitle(Text(verbatim: "Consent"))
.padding()
}
#Preview {
@Previewable @State var viewState: ConsentViewState = .base(.idle)


NavigationStack {
ConsentDocument(
markdown: {
Data("This is a *markdown* **example**".utf8)
},
viewState: $viewState
)
.navigationTitle(Text(verbatim: "Consent"))
.padding()
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)) ?? AttributedString(String(localized: "MARKDOWN_LOADING_ERROR", bundle: .module))

markdownString.font = exportConfiguration.fontSettings.documentContentFont

Check warning on line 61 in Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport+Export.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package macOS (Debug, SpeziOnboarding-macOS.xcresult, SpeziOnboarding-macOS.... / Test using xcodebuild or run fastlane

conformance of 'NSFont' to 'Sendable' is unavailable

Check warning on line 61 in Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport+Export.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package macOS (Release, SpeziOnboarding-macOS-Release.xcresult, SpeziOnboard... / Test using xcodebuild or run fastlane

conformance of 'NSFont' to 'Sendable' is unavailable

return PDFAttributedText(text: NSAttributedString(markdownString))
}
Expand Down Expand Up @@ -90,12 +90,40 @@
#endif

group.set(font: exportConfiguration.fontSettings.signaturePrefixFont)
group.add(PDFGroupContainer.left, text: signaturePrefix)
group.add(.left, text: signaturePrefix)

group.addLineSeparator(style: PDFLineStyle(color: .black))

group.set(font: exportConfiguration.fontSettings.signatureNameFont)
group.add(PDFGroupContainer.left, text: personName)

// Add person name and date as the caption below the signature line
philippzagar marked this conversation as resolved.
Show resolved Hide resolved
// Sadly a quite complex table is required to have the caption within one line
let table = PDFTable(rows: 1, columns: 2)
table.widths = [0.5, 0.5] // Two equal-width columns for left and right alignment
table.margin = .zero
table.padding = 0
table.style.outline = .none

let cellStyle = PDFTableCellStyle(
colors: (Color.clear, Color.black),
borders: .none,
font: exportConfiguration.fontSettings.signatureCaptionFont
)

// Add person name to the left cell
table[0, 0] = PDFTableCell(
content: try? .init(content: personName),
alignment: .left,
style: cellStyle
)

// Add formatted date to the right cell
table[0, 1] = PDFTableCell(
content: try? .init(content: formattedSignatureDate ?? ""),
alignment: .right,
style: cellStyle
)

group.add(.left, table: table)

return group
}

Expand All @@ -116,8 +144,8 @@
signatureFooter: PDFGroup,
exportTimeStamp: PDFAttributedText? = nil
) async throws -> PDFKit.PDFDocument {
let document = TPPDF.PDFDocument(format: exportConfiguration.getPDFPageFormat())
let document = TPPDF.PDFDocument(format: exportConfiguration.pdfPageFormat)

if let exportStamp = exportTimeStamp {
document.add(.contentRight, attributedTextObject: exportStamp)
}
Expand Down
Loading
Loading