Skip to content

Deprecate WebView and add an environment-aware Color.rgbo(in:) - #6

Merged
Jeehut merged 4 commits into
mainfrom
wip/deprecate-superseded-helpers
Oct 14, 2026
Merged

Jeehut merged 4 commits into
mainfrom
wip/deprecate-superseded-helpers

Conversation

@Jeehut

@Jeehut Jeehut commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Overview

Ahead of 2.1.0, every public symbol in Sources/HandySwiftUI was checked for a built-in SwiftUI, Foundation or system API that now does the same job. One helper, WebView, has a real replacement, but only from OS 26, so it is deprecated per platform from exactly that version. Color.rgbo stays public: resolve(in:) is not a readable replacement. Instead, rgbo gains an environment-aware overload built on resolve(in:). Nothing is removed.

Helper Replacement Available since How deprecated
WebView WebKit's SwiftUI WebView with WebPage (NavigationDeciding for link taps, callJavaScript(_:) / webViewScrollPosition(_:) for the scroll offset) iOS 26, macOS 26, visionOS 26 (WebKit's WebView is unavailable on tvOS and watchOS, and so is HandySwiftUI's) OS-versioned: @available(iOS, deprecated: 26.0, …), macOS 26, visionOS 26. No renamed:, since the initializers differ
Color.rgbo – – Not deprecated, see below

The WebView doc comment explains the migration, including one trap: in a file that imports both WebKit and HandySwiftUI, WebView used as a type is ambiguous and needs HandySwiftUI.WebView. The DocC article lists WebView in a "Deprecated" topic group.

rgbo and the new rgbo(in:)

  • rgbo(in: EnvironmentValues) (new) returns the same tuple. Starting with iOS 17, macOS 14, tvOS 17, watchOS 10 and visionOS 1, it reads Color.resolve(in:), so a view gets the components it actually displays, e.g. the dark variant of .primary in dark mode or under .environment(\.colorScheme, .dark). On older versions it returns rgbo.
  • rgbo (parameterless) keeps resolving through UIColor/NSColor. A first attempt routed it through resolve(in: EnvironmentValues()), but measurements showed that this always returns the light variant of dynamic colors, while the native path follows the app's current appearance and contrast setting (see the table below). Without a view there is no environment to pass, so the native path is the correct one for the parameterless variant.
  • On macOS, rgbo and hsbo now convert to extended sRGB instead of device RGB. Device RGB clipped wide-gamut colors: Display P3 red came out as 1, 0, 0, a different color than displayed. Extended sRGB is also what UIColor and Color.Resolved use, so for plain colors macOS, the iOS-based platforms and rgbo(in:) now agree. Dynamic colors can still differ between rgbo and rgbo(in:), because they resolve against different traits. For every sRGB color the values are unchanged. The internal color space conversions (hlco, change(_:by:), …) build on rgbo/hsbo and get the same correction. getHue on extended sRGB components returns saturation and brightness above 1 for wide-gamut colors, and Color(hue:saturation:brightness:opacity:) recreates them exactly (Display P3 red: h 0.9903, s 1.2075, b 1.0931 → 1.0931, −0.2268, −0.1501).
  • Visible behavior change: on macOS, rgbo can now return components below 0 or above 1 for wide-gamut colors, as it always did on iOS. Code like UInt8(red * 255) must clamp first; the doc comment says so.
  • The rgbo doc comment now says precisely which traits dynamic colors resolve against: UITraitCollection.current on iOS-based platforms (usually default traits outside of trait-aware callbacks) and NSAppearance.currentDrawing() on macOS. It points to rgbo(in:) for the values a view actually displays.

Measured on macOS 27 with the test process in dark appearance (red, green, blue, opacity):

Color Old rgbo (device RGB) New rgbo resolve(in: EnvironmentValues()) rgbo(in:) light rgbo(in:) dark
.red 1.0000, 0.2588, 0.2706 same 1.0000, 0.2196, 0.2353 1.0000, 0.2196, 0.2353 1.0000, 0.2588, 0.2706
Color(red: 0.2, green: 0.4, blue: 0.6) 0.2, 0.4, 0.6 same same same same
Color(.sRGB, …, opacity: 0.5) 0.9, 0.1, 0.3, 0.5 same same same same
.primary 1, 1, 1, 0.847 same 0, 0, 0 0, 0, 0 1, 1, 1, 0.847
Color(.displayP3, red: 1, …) 1, 0, 0 (clipped) 1.0931, −0.2266, −0.1504 1.0931, −0.2266, −0.1504 same same
.accentColor 0, 0.4784, 1 (system accent) same 0, 0.5333, 1 0, 0.5333, 1 0, 0.5686, 1

.accentColor differs between the two paths in a test process without an asset catalog: NSColor falls back to the user's system accent color, SwiftUI's environment to its default blue. Both are consistent within their path, so this was left as is.

Deliberately not deprecated

These have a related built-in API, but it is not a real equivalent:

  • DisclosureSection: Section(_:isExpanded:content:) exists since iOS 17 / macOS 14 / tvOS 17 / watchOS 10, but the SDK docs say only sidebar lists show a disclosure control ("Not all contexts provide a default control to trigger collapse or expansion"). DisclosureSection exists for Forms, where the built-in one shows nothing.
  • UIImage.heicData(compressionQuality:): UIKit's heicData() (iOS 17, tvOS 17, watchOS 10) has no compression quality parameter.
  • Color.rgbo, hsbo, hlco, toHex(alpha:), init(hex:): Color.Resolved only gives RGB, and not in a readable call (see above).
  • onAppBecomeActive / onAppResignActive: scenePhase is per scene, not per app, and does not follow app activation on macOS.
  • confirmDeleteDialog(…): confirmationDialog(_:isPresented:presenting:…) has no localized default title, message and buttons.
  • PrimaryButtonStyle / SecondaryButtonStyle: .borderedProminent / .bordered look different and have no disabled/compact options.
  • CheckboxUniversalToggleStyle: .checkbox is unavailable on iOS, tvOS, watchOS and visionOS.
  • CachedAsyncImage: AsyncImage has no cache.
  • SideTabView: .sidebarAdaptable (iOS 18) is a collapsible sidebar / tab bar, not a vertical icon rail.
  • PersistentModel.isPersisted: the new PersistentIdentifier.isTemporary (iOS 27) flips on save(), while isPersisted flips on insert(_:).
  • Notification.Name.publisher / post(object:on:): thin sugar over NotificationCenter APIs that have existed since before the helper.
  • No built-in counterpart in the iOS 27.1 / macOS 27.0 SDKs for: AsyncButton, AsyncView, HPicker, VPicker, MultiSelector, LimitedTextField, SearchableGridPicker, OpenPanel, Platform, Xcode.isRunningForPreviews, onFirstAppear, throwingTask, progressOverlay, applyIf / ifLet / platform modifiers, the Binding helpers, Text(format:partialStyling:), highlightMatchingTokenizedPrefixes, the semantic Color statics, the label and labeled-content styles, Emoji, SFSymbol.

How this was verified

Availability was read from the .swiftinterface files of the SDKs in Xcode 27.1 (iPhoneOS 27.1, MacOSX 27.0):

_WebKit_SwiftUI.swiftmodule/arm64e-apple-ios.swiftinterface
@available(macOS 26.0, iOS 26.0, visionOS 26.0, *)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
@_Concurrency::MainActor @preconcurrency public struct WebView : SwiftUICore::View {

WebKit.swiftmodule/arm64e-apple-ios.swiftinterface
@_hasMissingDesignatedInitializers @available(macOS 26.0, iOS 26.0, visionOS 26.0, *)
@_Concurrency::MainActor final public class WebPage {

SwiftUICore.swiftmodule/arm64e-apple-ios.swiftinterface
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
  public func resolve(in environment: SwiftUICore::EnvironmentValues) -> SwiftUICore::Color.SwiftUICore::Resolved

This branch adds a Swift Testing target (CI runs swift test --build-system swiftbuild). ColorComponentsTests compares the new rgbo with the old device RGB implementation for nine plain and dynamic colors and checks rgbo(in:) in light and dark environments. To get the red run, the macOS conversion was set back to device RGB and rgbo(in:) was made to ignore its environment (what callers had before):

Red:
✘ Test dynamicColorsResolveAgainstTheGivenEnvironment() failed with 1 issue.
     Expectation failed: light.red < 0.1 && light.green < 0.1 && light.blue < 0.1
✘ Test wideGamutColorIsNotClipped() failed with 2 issues.
     Expectation failed: displayP3Red.rgbo.red > 1.05
     Expectation failed: displayP3Red.rgbo.green < -0.1
✔ Test srgbColorsKeepTheirComponents(name:color:) with 9 test cases passed
✔ Test rgboMatchesResolvedComponentsForTheCurrentAppearance(name:color:) with 9 test cases passed
✔ Test staticColorsAreTheSameInEveryEnvironment(name:color:) with 9 test cases passed
✘ Test run with 5 tests in 1 suite failed with 3 issues.

Green:
✔ Test wideGamutColorIsNotClipped() passed
✔ Test dynamicColorsResolveAgainstTheGivenEnvironment() passed
✔ Test srgbColorsKeepTheirComponents(name:color:) with 9 test cases passed
✔ Test rgboMatchesResolvedComponentsForTheCurrentAppearance(name:color:) with 9 test cases passed
✔ Test staticColorsAreTheSameInEveryEnvironment(name:color:) with 9 test cases passed
✔ Test run with 5 tests in 1 suite passed

hsbo got the same treatment in a follow-up commit, with its own red run (device RGB restored):

✘ Test wideGamutColorKeepsItsHSBComponents() failed with 7 issues.
     Expectation failed: hsbo.saturation > 1.05
     Expectation failed: abs(lhs.red - rhs.red) < tolerance   (HSB round trip and change(.brightness, by: 0))
Green: ✔ Test run with 6 tests in 1 suite passed

rgboMatchesResolvedComponentsForTheCurrentAppearance is disabled through a test condition while "Increase contrast" is on, because rgbo then returns the high-contrast variants, which an EnvironmentValues() cannot request. dynamicColorsResolveAgainstTheGivenEnvironment carries the availability of resolve(in:), since the fallback ignores the environment.

The three tests that pass in the red run guard what must not change: sRGB components are identical to the old implementation, and rgbo agrees with rgbo(in:) for the current appearance.

The package was built on main and on this branch, and the deduplicated diagnostics (file + message) were compared. swift build (macOS) and xcodebuild for generic iOS, tvOS and visionOS were identical on both sides: no new warnings.

A throwaway consumer package showed that the WebView deprecation fires only from the intended deployment target:

platforms: [.macOS(.v26)]
Usage.swift:7:11: warning: 'WebView' was deprecated in macOS 26.0: Since macOS 26, WebKit provides a native SwiftUI `WebView`. Use it with a `WebPage` whose `NavigationDeciding` opens link taps externally.

platforms: [.macOS(.v13)]
Build complete! (no warnings from the consumer)

The same consumer, importing WebKit as well, compiled WebView(url:) and WebView(WebPage()) without ambiguity, while a stored property typed WebView failed with 'WebView' is ambiguous for type lookup in this context until it was written as HandySwiftUI.WebView.

Two public helpers now have a built-in counterpart that covers what
they do, but only from an OS version above the package minimum. They
are deprecated per platform from exactly that version, so only apps
whose deployment target already includes the replacement see a
warning; nothing is removed in this minor release.

- Color.rgbo: SwiftUI's resolve(in:) returns a Color.Resolved with
  red, green, blue and opacity (iOS 17, macOS 14, tvOS 17,
  watchOS 10, visionOS 1). It also resolves dynamic colors against
  the given environment, which rgbo cannot do.
- WebView: WebKit ships a native SwiftUI WebView with WebPage
  (iOS 26, macOS 26, visionOS 26). Using WebView as a type in files
  that import both modules is ambiguous, so the docs explain the
  qualified name and how WebPage covers link handling and scrolling.

The color space conversions inside the package read the components
through a new internal property, so the package itself compiles
without deprecation warnings on visionOS, where rgbo is deprecated
from the package minimum on. The DocC articles move both symbols into
a Deprecated group that points to the replacement.
The package had no tests. CI uses the swiftbuild build system so that
the String Catalog is compiled, which the tests depend on.
Deprecating rgbo in favor of resolve(in:) traded a readable API for a
less readable one. rgbo stays public and gains rgbo(in:), which uses
resolve(in:) where available, so a view gets the components it
actually displays, including color scheme overrides. On older OS
versions it falls back to rgbo.

The parameterless rgbo keeps resolving through UIColor/NSColor:
without a view there is no environment, and EnvironmentValues() would
always resolve the light variant, while the native path follows the
app's current appearance and contrast setting. On macOS it now reads
extended sRGB instead of device RGB, which clipped wide-gamut colors
and differed from UIColor and Color.Resolved.
@Jeehut Jeehut changed the title Deprecate helpers superseded by built-in SwiftUI APIs Deprecate WebView and add an environment-aware Color.rgbo(in:) Oct 13, 2026
hsbo still converted to device RGB on macOS, so HSB values and every
change(.hueHSB/.saturation/.brightness, …) clipped wide-gamut colors
while rgbo no longer did. getHue on extended sRGB components returns
saturation and brightness above 1 for such colors, and
Color(hue:saturation:brightness:opacity:) recreates them exactly.

The rgbo documentation now says precisely which traits dynamic colors
resolve against and that wide-gamut components must be clamped before
converting to a fixed range. The test comparing rgbo with rgbo(in:)
is skipped when "Increase contrast" is on, and the environment test
only runs where resolve(in:) exists.
@Jeehut
Jeehut merged commit 35edb69 into main Oct 14, 2026
1 check passed
@Jeehut
Jeehut deleted the wip/deprecate-superseded-helpers branch October 15, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant