diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6420182..64f3436 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -95,3 +95,5 @@ jobs: uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 with: coveragereports: SpeziViews-iOS.xcresult SpeziViews-watchOS.xcresult SpeziViews-visionOS.xcresult SpeziViews-tvOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult + secrets: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Sources/SpeziViews/SpeziViews.docc/SpeziViews.md b/Sources/SpeziViews/SpeziViews.docc/SpeziViews.md index 7820e49..064bbeb 100644 --- a/Sources/SpeziViews/SpeziViews.docc/SpeziViews.md +++ b/Sources/SpeziViews/SpeziViews.docc/SpeziViews.md @@ -60,6 +60,7 @@ Automatically adapt your view layouts to dynamic type sizes, device orientation, - ``AsyncButton`` - ``SwiftUI/EnvironmentValues/processingDebounceDuration`` - ``CanvasView`` +- ``DismissButton`` ### Displaying Text @@ -73,6 +74,11 @@ Automatically adapt your view layouts to dynamic type sizes, device orientation, - ``SwiftUI/View/focusOnTap()`` - ``SwiftUI/View/observeOrientationChanges(_:)`` +### Styles + +- ``ReverseLabelStyle`` +- ``SwiftUI/LabelStyle/reverse`` + ### Localization - ``Foundation/LocalizedStringResource/BundleDescription/atURL(from:)`` diff --git a/Sources/SpeziViews/Styles/ReverseLabelStyle.swift b/Sources/SpeziViews/Styles/ReverseLabelStyle.swift new file mode 100644 index 0000000..44e7e32 --- /dev/null +++ b/Sources/SpeziViews/Styles/ReverseLabelStyle.swift @@ -0,0 +1,48 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +/// A label style that shows the title and icon in reverse layout compared to the standard `titleAndIcon` label style. +public struct ReverseLabelStyle: LabelStyle { + public func makeBody(configuration: Configuration) -> some View { + HStack { + configuration.title + configuration.icon + } + .accessibilityElement(children: .combine) + } +} + + +extension LabelStyle where Self == ReverseLabelStyle { + /// A label style that shows the title and icon in reverse layout compared to the standard `titleAndIcon` label style. + public static var reverse: ReverseLabelStyle { + ReverseLabelStyle() + } +} + + +#if DEBUG +#Preview { + VStack { + SwiftUI.Label { + Text(verbatim: "75 %") + } icon: { + Image(systemName: "battery.100") + } + SwiftUI.Label { + Text(verbatim: "75 %") + } icon: { + Image(systemName: "battery.100") + } + .labelStyle(.reverse) + } +} +#endif diff --git a/Sources/SpeziViews/Views/Button/DismissButton.swift b/Sources/SpeziViews/Views/Button/DismissButton.swift new file mode 100644 index 0000000..73cb5e4 --- /dev/null +++ b/Sources/SpeziViews/Views/Button/DismissButton.swift @@ -0,0 +1,75 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +/// Circular Dismiss button. +public struct DismissButton: View { + @Environment(\.dismiss) private var dismiss + + public var body: some View { +#if os(visionOS) || os(tvOS) || os(macOS) + Button("Dismiss", systemImage: "xmark") { + dismiss() + } +#else + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.system(size: 10, weight: .bold, design: .rounded)) +#if !os(watchOS) + .foregroundStyle(.secondary) +#endif + .background { + Circle() +#if os(iOS) + .fill(Color(uiColor: .secondarySystemBackground)) +#elseif os(watchOS) + .fill(Color(uiColor: .darkGray)) +#endif + .frame(width: 25, height: 25) + } + .frame(width: 27, height: 27) // make the tap-able button region slightly larger + } + .accessibilityLabel("Dismiss") + .buttonStyle(.plain) +#endif + } + + public init() {} +} + + +#if DEBUG +#Preview { +#if os(macOS) // cannot preview sheets in macOS + NavigationStack { + Text(verbatim: "Hello World") + .toolbar { + DismissButton() + } + } + .frame(width: 500, height: 350) +#else + Text(verbatim: "") + .sheet(isPresented: .constant(true)) { + NavigationStack { + Text(verbatim: "Hello World") + .toolbar { + DismissButton() + } + } + .frame(width: 200, height: 200) + .presentationDetents([.medium]) + .presentationCornerRadius(25) + } +#endif +} +#endif diff --git a/Tests/SpeziViewsTests/SnapshotTests.swift b/Tests/SpeziViewsTests/SnapshotTests.swift index d1b3644..77b4d0e 100644 --- a/Tests/SpeziViewsTests/SnapshotTests.swift +++ b/Tests/SpeziViewsTests/SnapshotTests.swift @@ -30,6 +30,25 @@ final class SnapshotTests: XCTestCase { assertSnapshot(of: largeRow, as: .image(layout: .device(config: .iPhone13Pro)), named: "iphone-XA3") assertSnapshot(of: largeRow, as: .image(layout: .device(config: .iPadPro11)), named: "ipad-XA3") +#endif + } + + func testReverseLabelStyle() { + let label = SwiftUI.Label("100 %", image: "battery.100") + .labelStyle(.reverse) + +#if os(iOS) + assertSnapshot(of: label, as: .image(layout: .device(config: .iPhone13Pro)), named: "iphone-regular") + assertSnapshot(of: label, as: .image(layout: .device(config: .iPadPro11)), named: "ipad-regular") +#endif + } + + func testDismissButton() { + let dismissButton = DismissButton() + +#if os(iOS) + assertSnapshot(of: dismissButton, as: .image(layout: .device(config: .iPhone13Pro)), named: "iphone-regular") + assertSnapshot(of: dismissButton, as: .image(layout: .device(config: .iPadPro11)), named: "ipad-regular") #endif } } diff --git a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testDismissButton.ipad-regular.png b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testDismissButton.ipad-regular.png new file mode 100644 index 0000000..6707f47 Binary files /dev/null and b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testDismissButton.ipad-regular.png differ diff --git a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testDismissButton.iphone-regular.png b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testDismissButton.iphone-regular.png new file mode 100644 index 0000000..9413e0a Binary files /dev/null and b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testDismissButton.iphone-regular.png differ diff --git a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testReverseLabelStyle.ipad-regular.png b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testReverseLabelStyle.ipad-regular.png new file mode 100644 index 0000000..e1cb3ae Binary files /dev/null and b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testReverseLabelStyle.ipad-regular.png differ diff --git a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testReverseLabelStyle.iphone-regular.png b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testReverseLabelStyle.iphone-regular.png new file mode 100644 index 0000000..20f6b2f Binary files /dev/null and b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testReverseLabelStyle.iphone-regular.png differ diff --git a/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift b/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift index a4ef331..8617e49 100644 --- a/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift @@ -33,18 +33,12 @@ final class ViewsTests: XCTestCase { let app = XCUIApplication() - #if os(visionOS) - let paletteToolPencil = "palette_tool_pencil_band" - #else - let paletteToolPencil = "palette_tool_pencil_base" - #endif - XCTAssert(app.collectionViews.buttons["Canvas"].waitForExistence(timeout: 2)) app.collectionViews.buttons["Canvas"].tap() XCTAssert(app.staticTexts["Did Draw Anything: false"].waitForExistence(timeout: 2)) - XCTAssertFalse(app.images[paletteToolPencil].waitForExistence(timeout: 2)) + XCTAssertFalse(app.images["palette_tool_pencil_base"].waitForExistence(timeout: 2)) let canvasView = app.scrollViews.firstMatch canvasView.swipeRight() @@ -55,7 +49,12 @@ final class ViewsTests: XCTestCase { XCTAssert(app.buttons["Show Tool Picker"].waitForExistence(timeout: 2)) app.buttons["Show Tool Picker"].tap() - XCTAssert(app.images[paletteToolPencil].waitForExistence(timeout: 10)) + #if os(visionOS) + // visionOS doesn't have the image anymore, this should be enough to check + XCTAssert(app.scrollViews.otherElements["Pen, black"].waitForExistence(timeout: 2.0)) + #else + XCTAssert(app.images["palette_tool_pencil_base"].waitForExistence(timeout: 10)) + #endif canvasView.swipeLeft() sleep(1) @@ -66,7 +65,7 @@ final class ViewsTests: XCTestCase { #endif sleep(15) // waitForExistence will otherwise return immediately - XCTAssertFalse(app.images[paletteToolPencil].waitForExistence(timeout: 10)) + XCTAssertFalse(app.images["palette_tool_pencil_base"].waitForExistence(timeout: 10)) canvasView.swipeUp() }