Skip to content

Commit

Permalink
Cleanup, Swift 6, Consent Contraint and SignatureView improvements (#60)
Browse files Browse the repository at this point in the history
# Cleanup, Swift 6, Consent Contraint and SignatureView improvements

## ♻️ Current situation & Problem
The repo needed some cleanup and improvements. Similar, the
`SignatureView` should be able to be used individually.


## ⚙️ Release Notes 
- Swift 6 language mode
- The `OnboardingDateSource` and `ConsentConstraint` were removed in
favor of a way simple export-handeling closure on the
`OnboardingConsentView`.
- `SignatureView` is now `public` and has a date associated with it
(also exported in the PDF)
- `OnboardingConsentView` now has proper progress indication for the
`action` closure.
- General cleanup of the application, as quite some dead code /
inconsistencies has been accumulated, especially in the consent export
part.


## 📚 Documentation
Some doc fixes and improvements.


## ✅ Testing
Manual testing and UI tests


## 📝 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).

---------

Co-authored-by: Philipp Zagar <[email protected]>
  • Loading branch information
Supereg and philippzagar authored Jan 29, 2025
1 parent e3b8d50 commit a7cc789
Show file tree
Hide file tree
Showing 65 changed files with 1,238 additions and 1,378 deletions.
10 changes: 6 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@ let package = Package(
.library(name: "SpeziOnboarding", targets: ["SpeziOnboarding"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi.git", from: "1.8.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.8.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"),
.package(url: "https://github.com/techprimate/TPPDF.git", from: "2.6.1")
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.8.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "2.1.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.9.0"),
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),
.package(url: "https://github.com/techprimate/TPPDF", from: "2.6.1")
] + swiftLintPackage(),
targets: [
.target(
name: "SpeziOnboarding",
dependencies: [
.product(name: "Spezi", package: "Spezi"),
.product(name: "SpeziFoundation", package: "SpeziFoundation"),
.product(name: "SpeziViews", package: "SpeziViews"),
.product(name: "SpeziPersonalInfo", package: "SpeziViews"),
.product(name: "OrderedCollections", package: "swift-collections"),
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ struct SequentialOnboardingViewExample: View {

The [`OnboardingConsentView`](https://swiftpackageindex.com/stanfordspezi/spezionboarding/documentation/spezionboarding/onboardingconsentview) can be used to allow your users to read and agree to a document, e.g., a consent document for a research study or a terms and conditions document for an app. The document can be signed using a family and given name and a hand-drawn signature. The signed consent form can then be exported and shared as a PDF file.

The following example demonstrates how the [`OnboardingConsentView`](https://swiftpackageindex.com/stanfordspezi/spezionboarding/documentation/spezionboarding/onboardingconsentview) shown above is constructed by providing markdown content encoded as a [UTF8](https://www.swift.org/blog/utf8-string/) [`Data`](https://developer.apple.com/documentation/foundation/data) instance (which may be provided asynchronously), an action that should be performed once the consent has been given, as well as a configuration defining the properties of the exported consent form.
The following example demonstrates how the [`OnboardingConsentView`](https://swiftpackageindex.com/stanfordspezi/spezionboarding/documentation/spezionboarding/onboardingconsentview) shown above is constructed by providing markdown content encoded as a [UTF8](https://www.swift.org/blog/utf8-string/) [`Data`](https://developer.apple.com/documentation/foundation/data) instance (which may be provided asynchronously), an action that should be performed once the consent has been given (which receives the exported consent form as a PDF), as well as a configuration defining the properties of the exported consent form.

```swift
import SpeziOnboarding
Expand All @@ -139,10 +139,12 @@ struct ConsentViewExample: View {
markdown: {
Data("This is a *markdown* **example**".utf8)
},
action: {
// Action to perform once the user has given their consent
action: { exportedConsentPdf in
// Action to perform once the user has given their consent.
// Closure receives the exported consent PDF to persist or upload it.
},
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SpeziViews
import SwiftUI


/// The `ConsentViewState` indicates in what state the ``ConsentDocument`` currently is.
/// The ``ConsentViewState`` indicates in what state the ``ConsentDocument`` currently is.
///
/// It can be used to observe and control the behavior of the ``ConsentDocument``, especially in regards
/// to the export functionality.
Expand All @@ -26,15 +26,15 @@ public enum ConsentViewState: Equatable {
/// The `signed` state indicates that the ``ConsentDocument`` is signed by the user.
case signed
/// The `export` state can be set by an outside view
/// encapsulating the ``ConsentDocument`` to trigger the export of the consent document as a PDF.
/// encapsulating the ``ConsentDocument`` to trigger the export of the consent document as a ``ConsentDocumentExportRepresentation``.
///
/// The previous state must be ``ConsentViewState/signed``, indicating that the consent document is signed.
case export
/// The `exported` state indicates that the
/// ``ConsentDocument`` has been successfully exported. The rendered `PDFDocument` can be found as the associated value of the state.
/// ``ConsentDocumentExportRepresentation`` has been successfully created.
/// The ``ConsentDocumentExportRepresentation`` can then be rendered to a PDF via ``ConsentDocumentExportRepresentation/render()``.
///
/// The export procedure (resulting in the ``ConsentViewState/exported(document:)`` state) can be triggered via setting the ``ConsentViewState/export`` state of the ``ConsentDocument`` .
case exported(document: PDFDocument, export: ConsentDocumentExport)
/// The `storing` state indicates that the ``ConsentDocument`` is currently being stored to the Standard.
case storing
/// The export representation creation (resulting in the ``ConsentViewState/exported(representation:)`` state) can be triggered
/// via setting the ``ConsentViewState/export`` state of the ``ConsentDocument`` .
case exported(representation: ConsentDocumentExportRepresentation)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// 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 PencilKit
import SwiftUI
import TPPDF


/// Extension of `ConsentDocument` enabling the export of the signed consent page.
extension ConsentDocument {
/// Creates the export representation of the ``ConsentDocument`` including all necessary content.
var exportRepresentation: ConsentDocumentExportRepresentation {
get async {
#if !os(macOS)
.init(
markdown: await self.markdown(),
signature: signatureImage,
name: self.name,
formattedSignatureDate: self.formattedConsentSignatureDate,
configuration: self.exportConfiguration
)
#else
.init(
markdown: await self.markdown(),
signature: self.signature,
name: self.name,
formattedSignatureDate: self.formattedConsentSignatureDate,
configuration: self.exportConfiguration
)
#endif
}
}

#if !os(macOS)
private var signatureImage: UIImage {
var updatedDrawing = PKDrawing()

for stroke in signature.strokes {
// As the `PKDrawing.image()` function automatically converts the ink color dependent on the used color scheme (light or dark mode),
// force the ink used in the `UIImage` of the `PKDrawing` to always be black by adjusting the signature ink according to the color scheme.
let blackStroke = PKStroke(
ink: PKInk(stroke.ink.inkType, color: colorScheme == .light ? .black : .white),
path: stroke.path,
transform: stroke.transform,
mask: stroke.mask
)

updatedDrawing.strokes.append(blackStroke)
}

#if os(iOS)
let scale = UIScreen.main.scale
#else
let scale = 3.0 // retina scale is default
#endif

return updatedDrawing.image(
from: .init(x: 0, y: 0, width: signatureSize.width, height: signatureSize.height),
scale: scale
)
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@

import Foundation
import SwiftUI
import TPPDF


extension ConsentDocument {
/// The ``ExportConfiguration`` enables developers to define the properties of the exported consent form.
public struct ExportConfiguration: Sendable {
extension ConsentDocumentExportRepresentation {
/// The ``Configuration`` enables developers to define the properties of the exported consent form.
public struct Configuration: Equatable, Sendable {
/// Represents common paper sizes with their dimensions.
///
/// You can use the `dimensions` property to get the width and height of each paper size in points.
///
/// - Note: The dimensions are calculated based on the standard DPI (dots per inch) of 72 for print.
public enum PaperSize: Sendable {
public enum PaperSize: Equatable, Sendable {
/// Standard US Letter paper size.
case usLetter
/// Standard DIN A4 paper size.
Expand All @@ -42,14 +43,22 @@ extension ConsentDocument {
return (widthInInches * pointsPerInch, heightInInches * pointsPerInch)
}
}

/// `TPPDF/PDFPageFormat` which corresponds to SpeziOnboarding's `PaperSize`.
var pdfPageFormat: PDFPageFormat {
switch self {
case .usLetter: .usLetter
case .dinA4: .a4
}
}
}

#if !os(macOS)
/// 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
public struct FontSettings: Equatable, Sendable {
/// 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 +72,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 @@ -85,9 +94,9 @@ extension ConsentDocument {
#else
/// 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
public struct FontSettings: Equatable, @unchecked Sendable {
/// 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 +110,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 @@ -122,23 +131,25 @@ extension ConsentDocument {
}
#endif


let consentTitle: LocalizedStringResource
let paperSize: PaperSize
let includingTimestamp: Bool
let fontSettings: FontSettings


/// Creates an `ExportConfiguration` specifying the properties of the exported consent form.
/// Creates an ``ConsentDocumentExportRepresentation/Configuration`` specifying the properties of the exported consent form.
///
/// - Parameters:
/// - paperSize: The page size of the exported form represented by ``ConsentDocument/ExportConfiguration/PaperSize``.
/// - paperSize: The page size of the exported form represented by ``ConsentDocumentExportRepresentation/Configuration/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,
consentTitle: LocalizedStringResource = Configuration.Defaults.exportedConsentFormTitle,
includingTimestamp: Bool = true,
fontSettings: FontSettings = ExportConfiguration.Defaults.defaultExportFontSettings
fontSettings: FontSettings = Configuration.Defaults.defaultExportFontSettings
) {
self.paperSize = paperSize
self.consentTitle = consentTitle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ import Foundation
import SwiftUI


extension ConsentDocument.ExportConfiguration {
/// Provides default values for fields related to the `ConsentDocumentExportConfiguration`.
extension ConsentDocumentExportRepresentation.Configuration {
/// Provides default values for fields related to the ``ConsentDocumentExportRepresentation/Configuration``.
public enum Defaults {
/// Default localized value for the title of the exported consent form.
public static let exportedConsentFormTitle = LocalizedStringResource("CONSENT_TITLE", bundle: .atURL(from: .module))

#if !os(macOS)
/// Default export font settings with fixed font sizes, ensuring a consistent appearance across platforms.
///
/// This configuration uses `systemFont` and `boldSystemFont` with absolute font sizes to achieve uniform font sizes
/// on different operating systems such as macOS, iOS, and visionOS.
public static let defaultExportFontSettings = FontSettings(
signatureNameFont: UIFont.systemFont(ofSize: 10),
signatureCaptionFont: UIFont.systemFont(ofSize: 10),
signaturePrefixFont: UIFont.boldSystemFont(ofSize: 12),
documentContentFont: UIFont.systemFont(ofSize: 12),
headerTitleFont: UIFont.boldSystemFont(ofSize: 28),
Expand All @@ -30,7 +33,7 @@ extension ConsentDocument.ExportConfiguration {
/// the font sizes might change according to the system settings, potentially leading to varying exported PDF documents
/// on devices with different system settings (e.g., larger default font size).
public static let defaultSystemDefaultFontSettings = FontSettings(
signatureNameFont: UIFont.preferredFont(forTextStyle: .subheadline),
signatureCaptionFont: UIFont.preferredFont(forTextStyle: .subheadline),
signaturePrefixFont: UIFont.preferredFont(forTextStyle: .title2),
documentContentFont: UIFont.preferredFont(forTextStyle: .body),
headerTitleFont: UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize),
Expand All @@ -42,7 +45,7 @@ extension ConsentDocument.ExportConfiguration {
/// This configuration uses `systemFont` and `boldSystemFont` with absolute font sizes to achieve uniform font sizes
/// on different operating systems such as macOS, iOS, and visionOS.
public static let defaultExportFontSettings = FontSettings(
signatureNameFont: NSFont.systemFont(ofSize: 10),
signatureCaptionFont: NSFont.systemFont(ofSize: 10),
signaturePrefixFont: NSFont.boldSystemFont(ofSize: 12),
documentContentFont: NSFont.systemFont(ofSize: 12),
headerTitleFont: NSFont.boldSystemFont(ofSize: 28),
Expand All @@ -53,7 +56,7 @@ extension ConsentDocument.ExportConfiguration {
/// the font sizes might change according to the system settings, potentially leading to varying exported PDF documents
/// on devices with different system settings (e.g., larger default font size).
public static let defaultSystemDefaultFontSettings = FontSettings(
signatureNameFont: NSFont.preferredFont(forTextStyle: .subheadline),
signatureCaptionFont: NSFont.preferredFont(forTextStyle: .subheadline),
signaturePrefixFont: NSFont.preferredFont(forTextStyle: .title2),
documentContentFont: NSFont.preferredFont(forTextStyle: .body),
headerTitleFont: NSFont.boldSystemFont(ofSize: NSFont.preferredFont(forTextStyle: .largeTitle).pointSize),
Expand Down
Loading

0 comments on commit a7cc789

Please sign in to comment.