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

Compatibility with macOS #39

Merged
merged 7 commits into from
Mar 1, 2024
Merged
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
53 changes: 41 additions & 12 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# This source file is part of the Stanford Spezi open-source project
# 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-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
#
# SPDX-License-Identifier: MIT
#
Expand All @@ -24,14 +24,16 @@ jobs:
include:
- buildConfig: Debug
artifactname: SpeziOnboarding-iOS.xcresult
resultBundle: SpeziOnboarding-iOS.xcresult
- buildConfig: Release
artifactname: SpeziOnboarding-iOS-Release.xcresult
resultBundle: SpeziOnboarding-iOS-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziOnboarding
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
resultBundle: ${{ matrix.artifactname }}
buildandtest_visionos:
name: Build and Test Swift Package visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand All @@ -40,71 +42,98 @@ jobs:
include:
- buildConfig: Debug
artifactname: SpeziOnboarding-visionOS.xcresult
resultBundle: SpeziOnboarding-visionOS.xcresult
- buildConfig: Release
artifactname: SpeziOnboarding-visionOS-Release.xcresult
resultBundle: SpeziOnboarding-visionOS-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziOnboarding
destination: 'platform=visionOS Simulator,name=Apple Vision Pro'
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
buildandtest_macos:
name: Build and Test Swift Package macOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
strategy:
matrix:
include:
- buildConfig: Debug
artifactname: SpeziOnboarding-macOS.xcresult
resultBundle: SpeziOnboarding-macOS.xcresult
- buildConfig: Release
artifactname: SpeziOnboarding-macOS-Release.xcresult
resultBundle: SpeziOnboarding-macOS-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziOnboarding
destination: 'platform=macOS,arch=arm64'
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
resultBundle: ${{ matrix.artifactname }}
buildandtestuitests_ios:
name: Build and Test UI Tests iOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
strategy:
matrix:
include:
- buildConfig: Debug
resultBundle: TestApp-iOS.xcresult
artifactname: TestApp-iOS.xcresult
- buildConfig: Release
resultBundle: TestApp-iOS-Release.xcresult
artifactname: TestApp-iOS-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
path: 'Tests/UITests'
scheme: TestApp
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
resultBundle: ${{ matrix.artifactname }}
buildandtestuitests_ipad:
name: Build and Test UI Tests iPad
name: Build and Test UI Tests iPadOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
strategy:
matrix:
include:
- buildConfig: Debug
resultBundle: TestApp-iPad.xcresult
artifactname: TestApp-iPad.xcresult
- buildConfig: Release
resultBundle: TestApp-iPad-Release.xcresult
artifactname: TestApp-iPad-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
destination: 'platform=iOS Simulator,name=iPad Air (5th generation)'
path: 'Tests/UITests'
scheme: TestApp
destination: 'platform=iOS Simulator,name=iPad Air (5th generation)'
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
resultBundle: ${{ matrix.artifactname }}
buildandtestuitests_visionos:
name: Build and Test UI Tests visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
strategy:
matrix:
include:
- buildConfig: Debug
resultBundle: TestApp-visionOS.xcresult
artifactname: TestApp-visionOS.xcresult
- buildConfig: Release
resultBundle: TestApp-visionOS-Release.xcresult
artifactname: TestApp-visionOS-Release.xcresult
with:
runsonlabels: '["macOS", "self-hosted"]'
destination: 'platform=visionOS Simulator,name=Apple Vision Pro'
path: 'Tests/UITests'
scheme: TestApp
destination: 'platform=visionOS Simulator,name=Apple Vision Pro'
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
resultBundle: ${{ matrix.artifactname }}
uploadcoveragereport:
name: Upload Coverage Report
needs: [buildandtest_ios, buildandtest_visionos, buildandtestuitests_ios, buildandtestuitests_ipad, buildandtestuitests_visionos]
needs: [buildandtest_ios, buildandtest_visionos, buildandtest_macos, buildandtestuitests_ios, buildandtestuitests_ipad, buildandtestuitests_visionos]
uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
with:
coveragereports: SpeziOnboarding-iOS.xcresult SpeziOnboarding-visionOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult
coveragereports: 'SpeziOnboarding-iOS.xcresult SpeziOnboarding-visionOS.xcresult SpeziOnboarding-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult'
7 changes: 4 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ let package = Package(
defaultLocalization: "en",
platforms: [
.iOS(.v17),
.visionOS(.v1)
.visionOS(.v1),
.macOS(.v14)
],
products: [
.library(name: "SpeziOnboarding", targets: ["SpeziOnboarding"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.3.0")
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.3.1")
],
targets: [
.target(
Expand Down
142 changes: 142 additions & 0 deletions Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//
// 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


/// Extension of `ConsentDocument` enabling the export of the signed consent page.
extension ConsentDocument {
#if !os(macOS)
/// 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.
private var blackInkSignatureImage: UIImage {
var updatedDrawing = PKDrawing()

for stroke in signature.strokes {
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


/// Creates a representation of the consent form that is ready to be exported via the SwiftUI `ImageRenderer`.
///
/// - Parameters:
/// - markdown: The markdown consent content as an `AttributedString`.
///
/// - Returns: A SwiftUI `View` representation of the consent content and signature.
///
/// - Note: This function avoids the use of asynchronous operations.
/// Asynchronous tasks are incompatible with SwiftUI's `ImageRenderer`,
/// which expects all rendering processes to be synchronous.
private func exportBody(markdown: AttributedString) -> some View {
VStack {
if exportConfiguration.includingTimestamp {
HStack {
Spacer()

Text("EXPORTED_TAG", bundle: .module)
+ Text(verbatim: ": \(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short))")
}
.font(.caption)
.padding()
}

OnboardingTitleView(title: exportConfiguration.consentTitle)

Text(markdown)
.padding()

Spacer()

ZStack(alignment: .bottomLeading) {
SignatureViewBackground(name: name, backgroundColor: .clear)

#if !os(macOS)
Image(uiImage: blackInkSignatureImage)
#else
Text(signature)
.padding(.bottom, 32)
.padding(.leading, 46)
.font(.custom("Snell Roundhand", size: 24))
#endif
}
#if !os(macOS)
.frame(width: signatureSize.width, height: signatureSize.height)
#else
.padding(.horizontal, 100)
#endif
}
}

/// Exports the signed consent form as a `PDFDocument` via the SwiftUI `ImageRenderer`.
///
/// Renders the `PDFDocument` according to the specified ``ConsentDocument/ExportConfiguration``.
///
/// - Returns: The exported consent form in PDF format as a PDFKit `PDFDocument`
@MainActor
func export() async -> PDFDocument? {
let markdown = await asyncMarkdown()

let markdownString = (try? AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)) ?? AttributedString(String(localized: "MARKDOWN_LOADING_ERROR", bundle: .module))

let renderer = ImageRenderer(content: exportBody(markdown: markdownString))
let paperSize = CGSize(
width: exportConfiguration.paperSize.dimensions.width,
height: exportConfiguration.paperSize.dimensions.height
)
renderer.proposedSize = .init(paperSize)

return await withCheckedContinuation { continuation in
renderer.render { _, context in
var box = CGRect(origin: .zero, size: paperSize)

/// Create in-memory `CGContext` that stores the PDF
guard let mutableData = CFDataCreateMutable(kCFAllocatorDefault, 0),
let consumer = CGDataConsumer(data: mutableData),
let pdf = CGContext(consumer: consumer, mediaBox: &box, nil) else {
continuation.resume(returning: nil)
return

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

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziOnboarding/ConsentView/ConsentDocument+Export.swift#L126-L127

Added lines #L126 - L127 were not covered by tests
}

pdf.beginPDFPage(nil)
pdf.translateBy(x: 0, y: 0)

context(pdf)

pdf.endPDFPage()
pdf.closePDF()

continuation.resume(returning: PDFDocument(data: mutableData as Data))
}
}
}
}
Loading
Loading