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

Swift 6 Compatibility #55

Merged
merged 10 commits into from
Aug 13, 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
39 changes: 25 additions & 14 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ jobs:
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
buildandtest_ios_latest:
name: Build and Test Swift Package iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziOnboarding
xcodeversion: latest
swiftVersion: 6
resultBundle: SpeziOnboarding-iOS-Latest.xcresult
artifactname: SpeziOnboarding-iOS-Latest.xcresult
buildandtest_visionos:
name: Build and Test Swift Package visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down Expand Up @@ -91,26 +101,27 @@ jobs:
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
buildandtestuitests_ios_latest:
name: Build and Test UI Tests iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
path: Tests/UITests
scheme: TestApp
xcodeversion: latest
swiftVersion: 6
resultBundle: TestApp-iOS-Latest.xcresult
artifactname: TestApp-iOS-Latest.xcresult
buildandtestuitests_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"]'
path: 'Tests/UITests'
scheme: TestApp
destination: 'platform=iOS Simulator,name=iPad Air (5th generation)'
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
destination: 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)'
resultBundle: TestApp-iPadOS.xcresult
artifactname: TestApp-iPadOS.xcresult
buildandtestuitests_visionos:
name: Build and Test UI Tests visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand All @@ -136,6 +147,6 @@ jobs:
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 SpeziOnboarding-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult'
coveragereports: 'SpeziOnboarding-iOS.xcresult SpeziOnboarding-visionOS.xcresult SpeziOnboarding-macOS.xcresult TestApp-iOS.xcresult TestApp-iPadOS.xcresult TestApp-visionOS.xcresult'
secrets:
token: ${{ secrets.CODECOV_TOKEN }}
40 changes: 37 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@
// SPDX-License-Identifier: MIT
//

import class Foundation.ProcessInfo
import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


let package = Package(
name: "SpeziOnboarding",
defaultLocalization: "en",
Expand All @@ -26,7 +34,7 @@ let package = Package(
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.3.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0")
],
] + swiftLintPackage(),
targets: [
.target(
name: "SpeziOnboarding",
Expand All @@ -35,13 +43,39 @@ let package = Package(
.product(name: "SpeziViews", package: "SpeziViews"),
.product(name: "SpeziPersonalInfo", package: "SpeziViews"),
.product(name: "OrderedCollections", package: "swift-collections")
]
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziOnboardingTests",
dependencies: [
.target(name: "SpeziOnboarding")
]
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
)
]
)


func swiftLintPlugin() -> [Target.PluginUsage] {
// Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app`
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
[.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")]
} else {
[]
}
}

func swiftLintPackage() -> [PackageDescription.Package.Dependency] {
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
[.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMinor(from: "0.55.1"))]
} else {
[]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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 {
@MainActor private var blackInkSignatureImage: UIImage {
var updatedDrawing = PKDrawing()

for stroke in signature.strokes {
Expand Down Expand Up @@ -54,6 +54,7 @@ extension ConsentDocument {
/// - 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.
@MainActor
private func exportBody(markdown: AttributedString) -> some View {
VStack {
if exportConfiguration.includingTimestamp {
Expand Down
12 changes: 0 additions & 12 deletions Sources/SpeziOnboarding/ConsentView/PDFDocument+Sendable.swift

This file was deleted.

1 change: 1 addition & 0 deletions Sources/SpeziOnboarding/OnboardingConstraint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ public protocol OnboardingConstraint: Standard {
/// Adds a new exported consent form represented as `PDFDocument` to the `Standard` conforming to ``OnboardingConstraint``.
///
/// - Parameter consent: The exported consent form represented as `PDFDocument` that should be added.
@MainActor
func store(consent: PDFDocument) async
}
5 changes: 2 additions & 3 deletions Sources/SpeziOnboarding/OnboardingDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ public class OnboardingDataSource: Module, EnvironmentAccessible {
/// Adds a new exported consent form represented as `PDFDocument` to the ``OnboardingDataSource``.
///
/// - Parameter consent: The exported consent form represented as `PDFDocument` that should be added.
@MainActor
public func store(_ consent: PDFDocument) async {
Task { @MainActor in
await standard.store(consent: consent)
}
await standard.store(consent: consent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ struct OnboardingStepIdentifier: Hashable, Codable {
/// - Parameters:
/// - view: The view used to initialize the identifier.
/// - custom: A flag indicating whether the step is custom.
@MainActor
init<V: View>(view: V, custom: Bool = false) {
self.custom = custom
var hasher = Hasher()
if let identifiable = view as? any Identifiable {
let id = identifiable.id
hasher.combine(id)
} else if let identifiable = view as? any OnboardingIdentifiable {
let id = identifiable.id
hasher.combine(id)
} else {
hasher.combine(String(describing: type(of: view)))
hasher.combine(String(describing: type(of: view as Any)))
}
self.identifierHash = hasher.finalize()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftUI
@resultBuilder
public enum OnboardingViewBuilder {
/// If declared, provides contextual type information for statement expressions to translate them into partial results.
public static func buildExpression(_ expression: any View) -> [any View] {
public static func buildExpression<V: View>(_ expression: V) -> [any View] {
[expression]
}

Expand Down
35 changes: 27 additions & 8 deletions Sources/SpeziOnboarding/OnboardingIdentifiableViewModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,30 @@
import SwiftUI


private struct OnboardingIdentifiableViewModifier<ID>: ViewModifier, Identifiable where ID: Hashable {
/// A `Identifiable` protocol that is isolated to the MainActor.
///
/// The `id` property of the `Identifiable` protocol a non-isolation requirement we cannot fulfill. Therefore, we need to introduce our own requirement.
@MainActor
protocol OnboardingIdentifiable {
associatedtype ID: Hashable

var id: ID { get }
}


@_documentation(visibility: internal)
public struct _OnboardingIdentifiableViewModifier<ID>: ViewModifier, OnboardingIdentifiable where ID: Hashable {
// swiftlint:disable:previous type_name
let id: ID

func body(content: Content) -> some View { content }
public func body(content: Content) -> some View {
content
}

Check warning on line 30 in Sources/SpeziOnboarding/OnboardingIdentifiableViewModifier.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziOnboarding/OnboardingIdentifiableViewModifier.swift#L28-L30

Added lines #L28 - L30 were not covered by tests
}


extension View {
/// Assign a unique identifier to a ``SwiftUI/View`` appearing in an ``OnboardingStack``.
/// Assign a unique identifier to a `View` appearing in an `OnboardingStack`.
///
/// A `ViewModifier` assigning an identifier to the `View` it is applied to.
/// When applying this modifier repeatedly, the outermost ``SwiftUI/View/onboardingIdentifier(_:)`` counts.
Expand All @@ -29,7 +44,8 @@
///
/// ```swift
/// struct Onboarding: View {
/// @AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false
/// @AppStorage(StorageKeys.onboardingFlowComplete)
/// var completedOnboardingFlow = false
///
/// var body: some View {
/// OnboardingStack(onboardingFlowComplete: $completedOnboardingFlow) {
Expand All @@ -41,14 +57,17 @@
/// }
/// }
/// ```
public func onboardingIdentifier<ID>(_ identifier: ID) -> some View where ID: Hashable {
modifier(OnboardingIdentifiableViewModifier(id: identifier))
@MainActor
public func onboardingIdentifier<ID: Hashable>(_ identifier: ID) -> ModifiedContent<Self, _OnboardingIdentifiableViewModifier<ID>> {
// For some reason, we need to explicitly spell the return type, otherwise the type will be `AnyView`.
// Not sure how that happens, but it does with Xcode 16 toolchain.
modifier(_OnboardingIdentifiableViewModifier(id: identifier))
}
}


extension ModifiedContent: Identifiable where Modifier: Identifiable {
public var id: Modifier.ID {
extension ModifiedContent: OnboardingIdentifiable where Modifier: OnboardingIdentifiable {
var id: Modifier.ID {
self.modifier.id
}
}
8 changes: 8 additions & 0 deletions Tests/SpeziOnboardingTests/SpeziOnboardingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ final class SpeziOnboardingTests: XCTestCase {
XCTAssert(true)
}

@MainActor
func testAnyViewIssue() throws {
let view = Text("Hello World")
.onboardingIdentifier("Custom Identifier")

XCTAssertFalse((view as Any) is AnyView)
}

@MainActor
func testOnboardingIdentifierModifier() throws {
let stack = OnboardingStack {
Expand Down
12 changes: 6 additions & 6 deletions Tests/UITests/TestApp/ExampleStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ import SwiftUI

/// An example Standard used for the configuration.
actor ExampleStandard: Standard, EnvironmentAccessible {
@Published @MainActor var consentData: PDFDocument = .init()
@MainActor private(set) var consentData: PDFDocument = .init()
}


extension ExampleStandard: OnboardingConstraint {
@MainActor
func store(consent: PDFDocument) async {
await MainActor.run {
self.consentData = consent
}
self.consentData = consent
try? await Task.sleep(for: .seconds(0.5))
}


@MainActor
func loadConsent() async throws -> PDFDocument {
await self.consentData
self.consentData
}
}
Loading
Loading