From e6c6a463596defb8fb8918e0cb272f8ceabfb756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Tue, 13 Oct 2026 14:13:37 +0200 Subject: [PATCH 1/3] Make the package build for watchOS watchOS is a declared platform, but the package failed to compile for it with 52 distinct errors. UIKit on watchOS has no semantic colors, no UIApplication, UIScreen or UIGraphicsImageRenderer, and SwiftUI there has no GroupBox, DisclosureGroup or rounded text field style. The label and standard colors now map to SwiftUI's own colors on watchOS, which follow the watch's system palette. Helpers without a sensible watchOS counterpart are compiled out: the background, fill, separator, link, tertiary/quaternary label and gray 2-6 colors, UIImage.resized, onAppBecomeActive/onAppResignActive and SideTabView. HPicker and VPicker need the materials of watchOS 10 and are marked accordingly. Previews that use unavailable views skip watchOS, and importing ImageIO makes heicData(compressionQuality:) work there. --- .../HandySwiftUI/Extensions/ColorExt.swift | 116 +++++++++++------ .../HandySwiftUI/Extensions/ImageExt.swift | 58 +++++---- Sources/HandySwiftUI/Modifiers/ViewExt.swift | 123 +++++++++--------- .../HorizontalLabeledContentStyle.swift | 2 +- .../Types/Views/AsyncButton.swift | 2 +- .../Types/Views/DisclosureSection.swift | 2 +- .../HandySwiftUI/Types/Views/HPicker.swift | 7 +- .../Types/Views/LimitedTextField.swift | 2 +- .../Types/Views/SideTabView.swift | 2 +- .../HandySwiftUI/Types/Views/VPicker.swift | 7 +- 10 files changed, 185 insertions(+), 136 deletions(-) diff --git a/Sources/HandySwiftUI/Extensions/ColorExt.swift b/Sources/HandySwiftUI/Extensions/ColorExt.swift index 15b989d..edb0185 100644 --- a/Sources/HandySwiftUI/Extensions/ColorExt.swift +++ b/Sources/HandySwiftUI/Extensions/ColorExt.swift @@ -7,6 +7,8 @@ import SwiftUI #endif // MARK: - Apple-provided colors +// UIKit on watchOS has no semantic colors. There, the label and standard colors map to SwiftUI's own colors, which use the watchOS +// system palette, and the colors without a watchOS counterpart (backgrounds, fills, separators, …) are not available. extension Color { // MARK: - UI Element Colors (https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors) // MARK: Label Colors @@ -14,6 +16,8 @@ extension Color { public static var label: Color { #if os(macOS) return Color(NSColor.labelColor) + #elseif os(watchOS) + return .primary #else return Color(UIColor.label) #endif @@ -23,31 +27,35 @@ extension Color { public static var secondaryLabel: Color { #if os(macOS) return Color(NSColor.secondaryLabelColor) + #elseif os(watchOS) + return .secondary #else return Color(UIColor.secondaryLabel) #endif } - /// The color for text labels that contain tertiary content. - public static var tertiaryLabel: Color { - #if os(macOS) - return Color(NSColor.tertiaryLabelColor) - #else - return Color(UIColor.tertiaryLabel) - #endif - } + #if !os(watchOS) + /// The color for text labels that contain tertiary content. + public static var tertiaryLabel: Color { + #if os(macOS) + return Color(NSColor.tertiaryLabelColor) + #else + return Color(UIColor.tertiaryLabel) + #endif + } - /// The color for text labels that contain quaternary content. - public static var quaternaryLabel: Color { - #if os(macOS) - return Color(NSColor.quaternaryLabelColor) - #else - return Color(UIColor.quaternaryLabel) - #endif - } + /// The color for text labels that contain quaternary content. + public static var quaternaryLabel: Color { + #if os(macOS) + return Color(NSColor.quaternaryLabelColor) + #else + return Color(UIColor.quaternaryLabel) + #endif + } + #endif // MARK: Fill Colors - #if !os(tvOS) + #if !os(tvOS) && !os(watchOS) /// An overlay fill color for thin and small shapes. public static var systemFill: Color { #if os(macOS) @@ -152,39 +160,45 @@ extension Color { #endif // MARK: Separator Colors - /// The color for thin borders or divider lines that allows some underlying content to be visible. - public static var separator: Color { - #if os(macOS) - return Color(NSColor.separatorColor) - #else - return Color(UIColor.separator) - #endif - } + #if !os(watchOS) + /// The color for thin borders or divider lines that allows some underlying content to be visible. + public static var separator: Color { + #if os(macOS) + return Color(NSColor.separatorColor) + #else + return Color(UIColor.separator) + #endif + } - /// The color for borders or divider lines that hides any underlying content. - public static var opaqueSeparator: Color { - #if os(macOS) - return Color(NSColor.separatorColor).opacity(1) - #else - return Color(UIColor.opaqueSeparator) - #endif - } + /// The color for borders or divider lines that hides any underlying content. + public static var opaqueSeparator: Color { + #if os(macOS) + return Color(NSColor.separatorColor).opacity(1) + #else + return Color(UIColor.opaqueSeparator) + #endif + } + #endif // MARK: Link Color - /// The color for links. - public static var link: Color { - #if os(macOS) - return Color(NSColor.linkColor) - #else - return Color(UIColor.link) - #endif - } + #if !os(watchOS) + /// The color for links. + public static var link: Color { + #if os(macOS) + return Color(NSColor.linkColor) + #else + return Color(UIColor.link) + #endif + } + #endif // MARK: - Standard Colors (https://developer.apple.com/documentation/uikit/uicolor/standard_colors) // MARK: Adaptable Colors public static var systemBlue: Color { #if os(macOS) return Color(NSColor.systemBlue) + #elseif os(watchOS) + return .blue #else return Color(UIColor.systemBlue) #endif @@ -193,6 +207,8 @@ extension Color { public static var systemGreen: Color { #if os(macOS) return Color(NSColor.systemGreen) + #elseif os(watchOS) + return .green #else return Color(UIColor.systemGreen) #endif @@ -201,6 +217,8 @@ extension Color { public static var systemIndigo: Color { #if os(macOS) return Color(NSColor.systemIndigo) + #elseif os(watchOS) + return .indigo #else return Color(UIColor.systemIndigo) #endif @@ -209,6 +227,8 @@ extension Color { public static var systemOrange: Color { #if os(macOS) return Color(NSColor.systemOrange) + #elseif os(watchOS) + return .orange #else return Color(UIColor.systemOrange) #endif @@ -217,6 +237,8 @@ extension Color { public static var systemPink: Color { #if os(macOS) return Color(NSColor.systemPink) + #elseif os(watchOS) + return .pink #else return Color(UIColor.systemPink) #endif @@ -225,6 +247,8 @@ extension Color { public static var systemPurple: Color { #if os(macOS) return Color(NSColor.systemPurple) + #elseif os(watchOS) + return .purple #else return Color(UIColor.systemPurple) #endif @@ -233,6 +257,8 @@ extension Color { public static var systemRed: Color { #if os(macOS) return Color(NSColor.systemRed) + #elseif os(watchOS) + return .red #else return Color(UIColor.systemRed) #endif @@ -241,6 +267,8 @@ extension Color { public static var systemTeal: Color { #if os(macOS) return Color(NSColor.systemTeal) + #elseif os(watchOS) + return .teal #else return Color(UIColor.systemTeal) #endif @@ -249,6 +277,8 @@ extension Color { public static var systemYellow: Color { #if os(macOS) return Color(NSColor.systemYellow) + #elseif os(watchOS) + return .yellow #else return Color(UIColor.systemYellow) #endif @@ -258,12 +288,14 @@ extension Color { public static var systemGray: Color { #if os(macOS) return Color(NSColor.systemGray) + #elseif os(watchOS) + return .gray #else return Color(UIColor.systemGray) #endif } - #if !os(tvOS) + #if !os(tvOS) && !os(watchOS) public static var systemGray2: Color { #if os(macOS) return Color(NSColor.systemGray) diff --git a/Sources/HandySwiftUI/Extensions/ImageExt.swift b/Sources/HandySwiftUI/Extensions/ImageExt.swift index 832cb55..3d10af4 100644 --- a/Sources/HandySwiftUI/Extensions/ImageExt.swift +++ b/Sources/HandySwiftUI/Extensions/ImageExt.swift @@ -1,3 +1,4 @@ +import ImageIO import SwiftUI import UniformTypeIdentifiers @@ -106,35 +107,38 @@ extension Image { import UIKit extension UIImage { - /// Resizes the image to fit within the specified maximum width and height. - /// - /// - Parameters: - /// - maxWidth: The maximum width for the resized image. - /// - maxHeight: The maximum height for the resized image. - /// - Returns: A new `UIImage` instance with the resized dimensions, or `nil` if resizing fails. - @MainActor - public func resized(maxWidth: CGFloat, maxHeight: CGFloat) -> UIImage? { - let size = self.size - if size.width <= maxWidth && size.height <= maxHeight { return self } - - #if os(visionOS) - let screenScale = 1.0 - #else - let screenScale = UIScreen.main.scale - #endif - let widthRatio = (maxWidth / screenScale) / size.width - let heightRatio = (maxHeight / screenScale) / size.height - let scaleFactor = min(widthRatio, heightRatio) - - let newSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor) + // watchOS has neither UIGraphicsImageRenderer nor UIScreen + #if !os(watchOS) + /// Resizes the image to fit within the specified maximum width and height. + /// + /// - Parameters: + /// - maxWidth: The maximum width for the resized image. + /// - maxHeight: The maximum height for the resized image. + /// - Returns: A new `UIImage` instance with the resized dimensions, or `nil` if resizing fails. + @MainActor + public func resized(maxWidth: CGFloat, maxHeight: CGFloat) -> UIImage? { + let size = self.size + if size.width <= maxWidth && size.height <= maxHeight { return self } + + #if os(visionOS) + let screenScale = 1.0 + #else + let screenScale = UIScreen.main.scale + #endif + let widthRatio = (maxWidth / screenScale) / size.width + let heightRatio = (maxHeight / screenScale) / size.height + let scaleFactor = min(widthRatio, heightRatio) + + let newSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor) + + let renderer = UIGraphicsImageRenderer(size: newSize) + let image = renderer.image { _ in + self.draw(in: CGRect(origin: .zero, size: newSize)) + } - let renderer = UIGraphicsImageRenderer(size: newSize) - let image = renderer.image { _ in - self.draw(in: CGRect(origin: .zero, size: newSize)) + return image.withRenderingMode(self.renderingMode) } - - return image.withRenderingMode(self.renderingMode) - } + #endif /// Converts the image to HEIC data with specified compression quality. /// diff --git a/Sources/HandySwiftUI/Modifiers/ViewExt.swift b/Sources/HandySwiftUI/Modifiers/ViewExt.swift index 37397fe..d777f21 100644 --- a/Sources/HandySwiftUI/Modifiers/ViewExt.swift +++ b/Sources/HandySwiftUI/Modifiers/ViewExt.swift @@ -300,67 +300,70 @@ extension View { } } - /// Executes the provided closure when the app is about to resign its active state (e.g., when switching to another app or minimizing the window). - /// - /// Use this modifier to perform cleanup tasks or save state when the app moves to the background. - /// - /// Example: - /// ```swift - /// struct VideoPlayerView: View { - /// @StateObject private var player = VideoPlayer() - /// - /// var body: some View { - /// VideoContent(player: player) - /// .onAppResignActive { - /// player.pause() - /// } - /// } - /// } - /// ``` - public func onAppResignActive(_ closure: @escaping () -> Void) -> some View { - #if os(macOS) - self.onReceive( - NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification), - perform: { _ in closure() } - ) - #else - self.onReceive( - NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), - perform: { _ in closure() } - ) - #endif - } + // watchOS apps have no UIApplication, their lifecycle notifications come from WKApplication + #if !os(watchOS) + /// Executes the provided closure when the app is about to resign its active state (e.g., when switching to another app or minimizing the window). + /// + /// Use this modifier to perform cleanup tasks or save state when the app moves to the background. + /// + /// Example: + /// ```swift + /// struct VideoPlayerView: View { + /// @StateObject private var player = VideoPlayer() + /// + /// var body: some View { + /// VideoContent(player: player) + /// .onAppResignActive { + /// player.pause() + /// } + /// } + /// } + /// ``` + public func onAppResignActive(_ closure: @escaping () -> Void) -> some View { + #if os(macOS) + self.onReceive( + NotificationCenter.default.publisher(for: NSApplication.willResignActiveNotification), + perform: { _ in closure() } + ) + #else + self.onReceive( + NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), + perform: { _ in closure() } + ) + #endif + } - /// Executes the provided closure when the app becomes active (e.g., when returning from another app or un-minimizing the window). - /// - /// Use this modifier to resume tasks or refresh content when the app returns to the foreground. - /// - /// Example: - /// ```swift - /// struct ChatView: View { - /// @StateObject private var viewModel = ChatViewModel() - /// - /// var body: some View { - /// MessagesList(messages: viewModel.messages) - /// .onAppBecomeActive { - /// viewModel.refreshMessages() - /// } - /// } - /// } - /// ``` - public func onAppBecomeActive(_ closure: @escaping () -> Void) -> some View { - #if os(macOS) - self.onReceive( - NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification), - perform: { _ in closure() } - ) - #else - self.onReceive( - NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), - perform: { _ in closure() } - ) - #endif - } + /// Executes the provided closure when the app becomes active (e.g., when returning from another app or un-minimizing the window). + /// + /// Use this modifier to resume tasks or refresh content when the app returns to the foreground. + /// + /// Example: + /// ```swift + /// struct ChatView: View { + /// @StateObject private var viewModel = ChatViewModel() + /// + /// var body: some View { + /// MessagesList(messages: viewModel.messages) + /// .onAppBecomeActive { + /// viewModel.refreshMessages() + /// } + /// } + /// } + /// ``` + public func onAppBecomeActive(_ closure: @escaping () -> Void) -> some View { + #if os(macOS) + self.onReceive( + NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification), + perform: { _ in closure() } + ) + #else + self.onReceive( + NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), + perform: { _ in closure() } + ) + #endif + } + #endif /// Presents a confirmation dialog for delete actions with a default title and warning message. /// diff --git a/Sources/HandySwiftUI/Styles/HorizontalLabeledContentStyle.swift b/Sources/HandySwiftUI/Styles/HorizontalLabeledContentStyle.swift index 601b7f1..9a4eba9 100644 --- a/Sources/HandySwiftUI/Styles/HorizontalLabeledContentStyle.swift +++ b/Sources/HandySwiftUI/Styles/HorizontalLabeledContentStyle.swift @@ -27,7 +27,7 @@ extension LabeledContentStyle where Self == HorizontalLabeledContentStyle { } } -#if DEBUG && !os(tvOS) +#if DEBUG && !os(tvOS) && !os(watchOS) #Preview { VStack { Form { diff --git a/Sources/HandySwiftUI/Types/Views/AsyncButton.swift b/Sources/HandySwiftUI/Types/Views/AsyncButton.swift index e389e51..be85748 100644 --- a/Sources/HandySwiftUI/Types/Views/AsyncButton.swift +++ b/Sources/HandySwiftUI/Types/Views/AsyncButton.swift @@ -162,7 +162,7 @@ public struct AsyncButton: View { } } -#if DEBUG && swift(>=6.0) +#if DEBUG && swift(>=6.0) && !os(watchOS) @available(iOS 17, macOS 14, tvOS 17, visionOS 1, watchOS 10, *) #Preview { @Previewable @State var errorMessage: String? diff --git a/Sources/HandySwiftUI/Types/Views/DisclosureSection.swift b/Sources/HandySwiftUI/Types/Views/DisclosureSection.swift index cc8bd58..da17000 100644 --- a/Sources/HandySwiftUI/Types/Views/DisclosureSection.swift +++ b/Sources/HandySwiftUI/Types/Views/DisclosureSection.swift @@ -142,7 +142,7 @@ public struct DisclosureSection: View } } -#if DEBUG && !os(tvOS) +#if DEBUG && !os(tvOS) && !os(watchOS) #Preview { Form { DisclosureGroup("Original Disclosure Group") { diff --git a/Sources/HandySwiftUI/Types/Views/HPicker.swift b/Sources/HandySwiftUI/Types/Views/HPicker.swift index 77e0580..6fe7553 100644 --- a/Sources/HandySwiftUI/Types/Views/HPicker.swift +++ b/Sources/HandySwiftUI/Types/Views/HPicker.swift @@ -41,6 +41,7 @@ import SwiftUI /// } /// } /// ``` +@available(watchOS 10.0, *) public struct HPicker: View { /// The array of selectable options. let options: [T] @@ -150,6 +151,7 @@ public struct HPicker: View { /// The array of selectable options. let options: [T] @@ -148,6 +149,7 @@ public struct VPicker Date: Tue, 13 Oct 2026 15:17:06 +0200 Subject: [PATCH 2/3] Build every platform in CI watchOS was a declared platform that did not compile, and nothing noticed because CI only built for macOS. A matrix job now builds the package for iOS, tvOS, visionOS and watchOS with generic destinations, which need the SDKs but no simulator runtimes. The macOS job uses the swiftbuild build system like on the other open branches, so the workflow changes merge cleanly. --- .github/workflows/main.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 96926b9..798ea8d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,21 @@ jobs: - uses: actions/checkout@v4 - name: Build package - run: swift build + run: swift build --build-system swiftbuild - name: Run tests if: hashFiles('Tests/**') != '' - run: swift test + run: swift test --build-system swiftbuild + + build-platforms: + runs-on: macos-26 + strategy: + fail-fast: false + matrix: + platform: [iOS, tvOS, visionOS, watchOS] + + steps: + - uses: actions/checkout@v4 + + - name: Build for ${{ matrix.platform }} + run: xcodebuild -scheme HandySwiftUI -destination 'generic/platform=${{ matrix.platform }}' CODE_SIGNING_ALLOWED=NO build From de09b56067aa57320f8d6b9ff2da99f2d7e1030d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Tue, 13 Oct 2026 15:26:18 +0200 Subject: [PATCH 3/3] List the platform build job first in the workflow With the new job appended right after the swift test line, git could not merge this workflow with the swiftbuild change of the other open branches. Placed before test-macos, both changes merge cleanly. --- .github/workflows/main.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 798ea8d..774aa32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,19 +5,6 @@ on: branches: [main] jobs: - test-macos: - runs-on: macos-26 - - steps: - - uses: actions/checkout@v4 - - - name: Build package - run: swift build --build-system swiftbuild - - - name: Run tests - if: hashFiles('Tests/**') != '' - run: swift test --build-system swiftbuild - build-platforms: runs-on: macos-26 strategy: @@ -30,3 +17,16 @@ jobs: - name: Build for ${{ matrix.platform }} run: xcodebuild -scheme HandySwiftUI -destination 'generic/platform=${{ matrix.platform }}' CODE_SIGNING_ALLOWED=NO build + + test-macos: + runs-on: macos-26 + + steps: + - uses: actions/checkout@v4 + + - name: Build package + run: swift build --build-system swiftbuild + + - name: Run tests + if: hashFiles('Tests/**') != '' + run: swift test --build-system swiftbuild