diff --git a/Package.resolved b/Package.resolved index 9b8bb2c3a..c06a70fec 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9cc57ad3a17b893e0be1065c0c70ebe99d327b76a45f90e3f6041166a242ff00", + "originHash" : "74798eb9c125fb3e83f0649a0dbc39da02790c08a88a3c0c4b662ac8db6c02dd", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index 66cf97770..8772f5b53 100644 --- a/Package.swift +++ b/Package.swift @@ -617,7 +617,14 @@ let openSwiftUITarget = Target.target( cSettings: sharedCSettings, cxxSettings: sharedCxxSettings, swiftSettings: sharedSwiftSettings, - linkerSettings: [.unsafeFlags(["-framework", "CoreServices"], .when(platforms: [.iOS]))] // For CS private API link support + linkerSettings: [ + // -framework CoreServices + // For CS private API link support + .linkedFramework("CoreServices", .when(platforms: [.iOS])), + // -lAccessibility + // For Accessibility private API link support + .linkedLibrary("Accessibility", .when(platforms: [.iOS])), + ] ) let openSwiftUITestsSupportTarget = Target.target( diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityCoreUserSettings.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityCoreUserSettings.swift new file mode 100644 index 000000000..3e62d0945 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityCoreUserSettings.swift @@ -0,0 +1,70 @@ +// +// AccessibilityCoreUserSettings.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#if canImport(UIKit) +import Accessibility +package import OpenSwiftUICore +import UIKit + +extension AccessibilityCore { + package enum UserSettings { + package static func resolve(into environment: inout EnvironmentValues) { + environment.accessibilityDifferentiateWithoutColor = UIAccessibility.shouldDifferentiateWithoutColor + environment.accessibilityReduceTransparency = UIAccessibility.isReduceTransparencyEnabled + environment.accessibilityReduceMotion = UIAccessibility.isReduceMotionEnabled + environment.accessibilityInvertColors = UIAccessibility.isInvertColorsEnabled + environment.accessibilityPrefersCrossFadeTransitions = UIAccessibility.prefersCrossFadeTransitions + environment.accessibilityShowButtonShapes = UIAccessibility.buttonShapesEnabled + environment.accessibilityDimFlashingLights = _AXSPhotosensitiveMitigationEnabled() + environment.accessibilityPlayAnimatedImages = AccessibilitySettings.animatedImagesEnabled + environment.accessibilityEnabledTechnologies = .enabledTechnologies + environment._accessibilityLargeContentViewerEnabled = UILargeContentViewerInteraction.isEnabled + environment._accessibilityQuickActionsEnabled = false + environment.accessibilityPrefersOnOffLabels = _AXSIncreaseButtonLegibility() + } + } +} + +// MARK: - AccessibilityTechnologies + enabledTechnologies + +extension AccessibilityTechnologies { + package static var enabledTechnologies: AccessibilityTechnologies { + let enabled = AccessibilityTechnology.allCases.filter { technology in + switch technology { + case .voiceOver: UIAccessibility.isVoiceOverRunning + case .switchControl: UIAccessibility.isSwitchControlRunning + case .fullKeyboardAccess: _AXSFullKeyboardAccessEnabled() + case .voiceControl: _AXSCommandAndControlEnabled() + case .hoverText: _AXSHoverTextEnabled() + case .assistiveAccess: AXAssistiveAccessEnabled() + } + } + return AccessibilityTechnologies(list: enabled) + } +} + +// MARK: - Private C functions + +@_silgen_name("_AXSPhotosensitiveMitigationEnabled") +private func _AXSPhotosensitiveMitigationEnabled() -> Bool + +@_silgen_name("_AXSIncreaseButtonLegibility") +private func _AXSIncreaseButtonLegibility() -> Bool + +@_silgen_name("_AXSFullKeyboardAccessEnabled") +private func _AXSFullKeyboardAccessEnabled() -> Bool + +@_silgen_name("_AXSCommandAndControlEnabled") +private func _AXSCommandAndControlEnabled() -> Bool + +@_silgen_name("_AXSHoverTextEnabled") +private func _AXSHoverTextEnabled() -> Bool + +@_silgen_name("AXAssistiveAccessEnabled") +private func AXAssistiveAccessEnabled() -> Bool + +#endif diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityEnvironmentAdditions.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityEnvironmentAdditions.swift new file mode 100644 index 000000000..348c581bb --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityEnvironmentAdditions.swift @@ -0,0 +1,88 @@ +// +// AccessibilityEnvironmentAdditions.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: E3F97FE8C846010147E7A62076265464 (SwiftUI) + +import OpenSwiftUICore + +// MARK: - EnabledTechnologiesKey + +private struct EnabledTechnologiesKey: EnvironmentKey { + static var defaultValue: AccessibilityTechnologies { AccessibilityTechnologies() } +} + +// MARK: - EnvironmentValues + Accessbility + +extension EnvironmentValues { + @_spi(Private) + @available(OpenSwiftUI_v3_0, *) + public var accessibilityEnabledTechnologies: AccessibilityTechnologies { + get { self[EnabledTechnologiesKey.self] } + set { self[EnabledTechnologiesKey.self] = newValue } + } + + fileprivate func isEnabled(for technology: AccessibilityTechnology) -> Bool { + let member = AccessibilityTechnologies(list: [technology]) + return accessibilityEnabledTechnologies.contains(member) + } + + fileprivate mutating func setIsEnabled(_ enabled: Bool, for technology: AccessibilityTechnology) { + guard isEnabled(for: technology) != enabled else { return } + let member = AccessibilityTechnologies(list: [technology]) + if enabled { + accessibilityEnabledTechnologies.insert(member) + } else { + accessibilityEnabledTechnologies.remove(member) + } + } + +} + +@available(OpenSwiftUI_v3_0, *) +extension EnvironmentValues { + + /// A Boolean value that indicates whether the VoiceOver screen reader is in use. + /// + /// The state changes as the user turns on or off the VoiceOver screen reader. + public var accessibilityVoiceOverEnabled: Bool { + isEnabled(for: .voiceOver) + } + + /// A Boolean value that indicates whether the Switch Control motor accessibility feature is in use. + /// + /// The state changes as the user turns on or off the Switch Control feature. + public var accessibilitySwitchControlEnabled: Bool { + isEnabled(for: .switchControl) + } +} + +@_spi(Private) +@available(OpenSwiftUI_v4_0, *) +extension EnvironmentValues { + public var accessibilityFullKeyboardAccessEnabled: Bool { + isEnabled(for: .fullKeyboardAccess) + } + + public var accessibilityVoiceControlEnabled: Bool { + isEnabled(for: .voiceControl) + } +} + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +extension EnvironmentValues { + public var accessibilityHoverTextEnabled: Bool { + isEnabled(for: .hoverText) + } +} + +@available(OpenSwiftUI_v6_0, *) +extension EnvironmentValues { + /// A Boolean value that indicates whether Assistive Access is in use. + public var accessibilityAssistiveAccessEnabled: Bool { + isEnabled(for: .assistiveAccess) + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityLargeContentView.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityLargeContentView.swift new file mode 100644 index 000000000..88bdb5d06 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityLargeContentView.swift @@ -0,0 +1,271 @@ +// +// AccessibilityLargeContentView.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: F0D6FE3E66D6447B1F7FC2D6B4BA3CAB (SwiftUI) + +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) +@_spi(Private) +import OpenSwiftUICore +import Foundation + +// MARK: - EnvironmentValues + accessibilityLargeContentViewerEnabled + +extension EnvironmentValues { + + /// Whether the Large Content Viewer is enabled. + /// + /// The system can automatically provide a large content view + /// with ``View/accessibilityShowsLargeContentViewer()`` + /// or you can provide your own with ``View/accessibilityShowsLargeContentViewer(_:)``. + /// + /// While it is not necessary to check this value before adding + /// a large content view, it may be helpful if you need to + /// adjust the behavior of a gesture. For example, a button with + /// a long press handler might increase its long press duration + /// so the user can read the text in the large content viewer first. + @available(OpenSwiftUI_v3_0, *) + public var accessibilityLargeContentViewerEnabled: Bool { + self[AccessibilityLargeContentViewerKey.self] + } + + @available(OpenSwiftUI_v3_0, *) + public var _accessibilityLargeContentViewerEnabled: Bool { + get { self[AccessibilityLargeContentViewerKey.self] } + set { self[AccessibilityLargeContentViewerKey.self] = newValue } + } +} + +// MARK: - View + accessibilityLargeContentViewer [WIP] + +extension View { + + /// Adds a custom large content view to be shown by + /// the large content viewer. + /// + /// Rely on the large content viewer only in situations + /// where items must remain small due to unavoidable + /// design constraints. For example, buttons in a tab bar + /// remain small to leave more room for the main app content. + /// + /// The following example shows how to add a custom large + /// content view: + /// + /// var body: some View { + /// Button(action: newMessage) { + /// Image(systemName: "plus") + /// } + /// .accessibilityShowsLargeContentViewer { + /// Label("New Message", systemImage: "plus") + /// } + /// } + /// + /// Don’t use the large content viewer as a replacement for proper + /// Dynamic Type support. For example, Dynamic Type allows items + /// in a list to grow or shrink vertically to accommodate the user’s preferred + /// font size. Rely on the large content viewer only in situations where + /// items must remain small due to unavoidable design constraints. + /// + /// For example, views that have their Dynamic Type size constrained + /// with ``View/dynamicTypeSize(_:)`` may require a + /// large content view. + @available(OpenSwiftUI_v3_0, *) + nonisolated public func accessibilityShowsLargeContentViewer( + @ViewBuilder _ largeContentView: () -> V + ) -> some View where V: View { + accessibilityShowsLargeContentViewer( + .enabled, + largeContentView: largeContentView + ) + } + + /// Adds a default large content view to be shown by + /// the large content viewer. + /// + /// Rely on the large content viewer only in situations + /// where items must remain small due to unavoidable + /// design constraints. For example, buttons in a tab bar + /// remain small to leave more room for the main app content. + /// + /// The following example shows how to add a custom large + /// content view: + /// + /// var body: some View { + /// Button("New Message", action: newMessage) + /// .accessibilityShowsLargeContentViewer() + /// } + /// + /// Don’t use the large content viewer as a replacement for proper + /// Dynamic Type support. For example, Dynamic Type allows items + /// in a list to grow or shrink vertically to accommodate the user’s preferred + /// font size. Rely on the large content viewer only in situations where + /// items must remain small due to unavoidable design constraints. + /// + /// For example, views that have their Dynamic Type size constrained + /// with ``View/dynamicTypeSize(_:)`` may require a + /// large content view. + @available(OpenSwiftUI_v3_0, *) + nonisolated public func accessibilityShowsLargeContentViewer( + ) -> some View { + accessibilityShowsLargeContentViewer(.enabled) + } + + nonisolated func accessibilityShowsLargeContentViewer( + _ behavior: AccessibilityLargeContentViewBehavior + ) -> some View { + transformPreference(AccessibilityLargeContentViewTree.Key.self) { value in + _openSwiftUIUnimplementedFailure() + } + } + + nonisolated func accessibilityShowsLargeContentViewer( + _ behavior: AccessibilityLargeContentViewBehavior, + @ViewBuilder largeContentView: () -> V + ) -> some View where V: View { + modifier( + AccessibilityLargeContentViewModifier( + behavior: behavior, + largeContentView: largeContentView() + ) + ) + } +} + +// MARK: - AccessibilityLargeContentViewTree + +enum AccessibilityLargeContentViewTree: Equatable { + case leaf(AccessibilityLargeContentViewItem) + case branch([AccessibilityLargeContentViewTree]) + case empty + + func hitTest(at point: CGPoint) -> AccessibilityLargeContentViewItem? { + switch self { + case .leaf(let item): + guard item.behavior == .enabled, item.frame.contains(point) else { + return nil + } + return item + case .branch(let children): + for child in children { + guard let result = child.hitTest(at: point) else { + continue + } + return result + } + return nil + case .empty: + return nil + } + } + + // MARK: - AccessibilityLargeContentViewTree.Key + + struct Key: HostPreferenceKey { + static let defaultValue: AccessibilityLargeContentViewTree = .empty + + static func reduce( + value: inout AccessibilityLargeContentViewTree, + nextValue: () -> AccessibilityLargeContentViewTree + ) { + let newValue = nextValue() + switch (value, newValue) { + case (_, .empty): + break + case (.empty, _): + value = newValue + case (.branch(let oldArray), .branch(let newArray)): + value = .branch(oldArray + newArray) + case (.branch(let oldArray), _): + value = .branch(oldArray + [newValue]) + case (_, .branch(let newArray)): + value = .branch([value] + newArray) + case (_, _): + value = .branch([value, newValue]) + } + } + } +} + +// MARK: - AccessibilityLargeContentViewItem + +struct AccessibilityLargeContentViewItem: Equatable { + var title: String? + var image: Image.Resolved? + var frame: CGRect + var behavior: AccessibilityLargeContentViewBehavior +} + +// MARK: - AccessibilityLargeContentViewModifier [WIP] + +private struct AccessibilityLargeContentViewModifier: MultiViewModifier, PrimitiveViewModifier { + var behavior: AccessibilityLargeContentViewBehavior + var largeContentView: Content + + nonisolated static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + _openSwiftUIUnimplementedFailure() + } +} + +package struct AccessibilityLargeContentViewerKey: EnvironmentKey { + package static var defaultValue: Bool { false } +} + +// MARK: - AccessibilityLargeContentViewHitTestingTransform + +private struct AccessibilityLargeContentViewHitTestingTransform: Rule { + @Attribute var allowsHitTesting: Bool + + var value: (inout AccessibilityLargeContentViewTree) -> Void { + { + guard !allowsHitTesting else { + return + } + $0 = .empty + } + } +} + +// MARK: - AccessibilityLargeContentViewTransform + +private struct AccessibilityLargeContentViewTransform: Rule { + @Attribute var behavior: AccessibilityLargeContentViewBehavior + @Attribute var platformItemList: PlatformItemList + @Attribute var size: ViewSize + @Attribute var position: CGPoint + @Attribute var transform: ViewTransform + + var value: (inout AccessibilityLargeContentViewTree) -> Void { + var viewTransform = transform + viewTransform.appendPosition(position) + var frame = CGRect(origin: .zero, size: size.value) + frame.convert(to: .global, transform: viewTransform) + let mergedContentItems = platformItemList.mergedContentItems + let title = mergedContentItems.text?.string ?? mergedContentItems.label?.string + let image = mergedContentItems.resolvedImage + let behavior = behavior + let item = AccessibilityLargeContentViewItem( + title: title, + image: image, + frame: frame, + behavior: behavior + ) + return { tree in + tree = .leaf(item) + } + } +} + +// MARK: - AccessibilityLargeContentViewBehavior + +enum AccessibilityLargeContentViewBehavior: UInt8, Hashable { + case disabled + case placeholder + case enabled +} diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityQuickAction.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityQuickAction.swift new file mode 100644 index 000000000..1d07732c3 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityQuickAction.swift @@ -0,0 +1,282 @@ +// +// AccessibilityQuickAction.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: TODO +// ID: B8D2E4520F2964BB14185EE65411F685 (SwiftUI) + +#if OPENSWIFTUI_OPENCOMBINE +import OpenCombine +#else +import Combine +#endif +import OpenSwiftUICore + +// MARK: - AccessibilityQuickActionProxy [TODO] + +private class AccessibilityQuickActionProxy { + @Published var state: AccessibilityQuickActionState = .inactive + var label: String? + var isActive: Binding? + var action: (() -> Void)? + var isEnabled: Bool = true + let style: _AccessibilityQuickActionStyle.RawValue + + init(style: _AccessibilityQuickActionStyle.RawValue) { + self.style = style + } +} + +// MARK: - _AccessibilityQuickActionStyle + +@available(OpenSwiftUI_v4_0, *) +public struct _AccessibilityQuickActionStyle { + enum RawValue: Equatable { + case prompt + case outline + } + + let rawValue: RawValue +} + +@available(*, unavailable) +extension _AccessibilityQuickActionStyle: Sendable {} + +// MARK: - AccessibilityQuickActionStyle + +/// A type that describes the presentation style of an +/// accessibility quick action. +@available(OpenSwiftUI_v4_0, *) +@available(iOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(visionOS, unavailable) +public protocol AccessibilityQuickActionStyle { + var _style: _AccessibilityQuickActionStyle { get } +} + +// MARK: - AccessibilityQuickActionPromptStyle + +/// A presentation style that displays a prompt to the user when +/// the accessibility quick action is active. +/// +/// Don't use this type directly. Instead, use ``AccessibilityQuickActionStyle/prompt``. +@available(OpenSwiftUI_v4_0, *) +@available(iOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(visionOS, unavailable) +public struct AccessibilityQuickActionPromptStyle: AccessibilityQuickActionStyle { + public var _style: _AccessibilityQuickActionStyle { + .init(rawValue: .prompt) + } + + @usableFromInline + init() {} +} + +@available(*, unavailable) +extension AccessibilityQuickActionPromptStyle: Sendable {} + +@available(OpenSwiftUI_v4_0, *) +@available(iOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(visionOS, unavailable) +extension AccessibilityQuickActionStyle where Self == AccessibilityQuickActionPromptStyle { + @_alwaysEmitIntoClient + public static var prompt: AccessibilityQuickActionPromptStyle { + AccessibilityQuickActionPromptStyle() + } +} + +// MARK: - AccessibilityQuickActionOutlineStyle + +/// A presentation style that displays a prompt to the user when +/// the accessibility quick action is active. +/// +/// Don't use this type directly. Instead, use ``AccessibilityQuickActionStyle/outline``. +@available(OpenSwiftUI_v4_0, *) +@available(iOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(visionOS, unavailable) +public struct AccessibilityQuickActionOutlineStyle: AccessibilityQuickActionStyle { + public var _style: _AccessibilityQuickActionStyle { + .init(rawValue: .outline) + } + + @usableFromInline + init() {} +} + +@available(OpenSwiftUI_v4_0, *) +@available(iOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(visionOS, unavailable) +extension AccessibilityQuickActionStyle where Self == AccessibilityQuickActionOutlineStyle { + @_alwaysEmitIntoClient + public static var outline: AccessibilityQuickActionOutlineStyle { + AccessibilityQuickActionOutlineStyle() + } +} + +// MARK: - AccessibilityQuickActionState + +enum AccessibilityQuickActionState { + case inactive + case willHint + case willPulse + case willActivate +} + +// MARK: - AccessibilityQuickActionsKey + +private struct AccessibilityQuickActionsKey: EnvironmentKey { + static var defaultValue: Bool { false } +} + +// MARK: - EnvironmentValues + AccessibilityQuickActions + +extension EnvironmentValues { + + /// A Boolean that indicates whether the quick actions feature is enabled. + /// + /// The system uses quick actions to provide users with a + /// fast alternative interaction method. Quick actions can be + /// presented to users with a textual banner at the top of their + /// screen and/or an outline around a view that is already on screen. + @available(OpenSwiftUI_v4_0, *) + public var accessibilityQuickActionsEnabled: Bool { + self[AccessibilityQuickActionsKey.self] + } + + @available(OpenSwiftUI_v4_0, *) + public var _accessibilityQuickActionsEnabled: Bool { + get { self[AccessibilityQuickActionsKey.self] } + set { self[AccessibilityQuickActionsKey.self] = newValue } + } +} + +// MARK: - View + AccessibilityQuickAction + +@available(OpenSwiftUI_v4_0, *) +@available(iOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(visionOS, unavailable) +extension View { + + /// Adds a quick action to be shown by the system when active. + /// + /// The quick action will automatically become active when the + /// view appears. If the view is disabled, the action will defer + /// becoming active until the view is no longer disabled. + /// + /// The following example shows how to add a quick action to + /// pause and resume a workout, with the ``AccessibilityQuickActionStyle/prompt`` style. + /// + /// @State private var isPaused = false + /// + /// var body: some View { + /// WorkoutView(isPaused: $isPaused) + /// .accessibilityQuickAction(style: .prompt) { + /// Button(isPaused ? "Resume" : "Pause") { + /// isPaused.toggle() + /// } + /// } + /// } + /// + /// The following example shows how to add a quick action to + /// play and pause music, with the ``AccessibilityQuickActionStyle/outline`` style. + /// + /// @State private var isPlaying = false + /// + /// var body: some View { + /// PlayButton(isPlaying: $isPlaying) + /// .contentShape(.focusEffect, Circle()) + /// .accessibilityQuickAction(style: .outline) { + /// Button(isPlaying ? "Pause" : "Play") { + /// isPlaying.toggle() + /// } + /// } + /// } + /// + public func accessibilityQuickAction( + style: Style, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + modifier( + AccessibilityQuickActionModifier( + content: content(), + style: style._style.rawValue + ) + ) + } + + /// Adds a quick action to be shown by the system when active. + /// + /// The following example shows how to add a quick action to + /// pause and resume a workout, with the ``AccessibilityQuickActionStyle/prompt`` style. + /// + /// @State private var isPaused = false + /// @State private var isQuickActionActive = false + /// + /// var body: some View { + /// WorkoutView(isPaused: $isPaused) + /// .accessibilityQuickAction(style: .prompt, isActive: $isQuickActionActive) { + /// Button(isPaused ? "Resume" : "Pause") { + /// isPaused.toggle() + /// } + /// } + /// } + /// + /// The following example shows how to add a quick action to + /// play and pause music, with the ``AccessibilityQuickActionStyle/outline`` style. + /// + /// @State private var isPlaying = false + /// @State private var isQuickActionActive = false + /// + /// var body: some View { + /// PlayButton(isPlaying: $isPlaying) + /// .contentShape(.focusEffect, Circle()) + /// .accessibilityQuickAction(style: .outline, isActive: $isQuickActionActive) { + /// Button(isPlaying ? "Pause" : "Play") { + /// isPlaying.toggle() + /// } + /// } + /// } + /// + public func accessibilityQuickAction( + style: Style, + isActive: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + modifier( + AccessibilityQuickActionModifier( + content: content(), + isActive: isActive, + style: style._style.rawValue + ) + ) + } +} + + +// MARK: - AccessibilityQuickActionModifier [WIP] + +private struct AccessibilityQuickActionModifier: MultiViewModifier, PrimitiveViewModifier where Content: View { + var content: Content + var isActive: Binding? + var style: _AccessibilityQuickActionStyle.RawValue + + nonisolated static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + _openSwiftUIPlatformUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityTechnologies.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityTechnologies.swift new file mode 100644 index 000000000..5dac01a54 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityTechnologies.swift @@ -0,0 +1,162 @@ +// +// AccessibilityTechnologies.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 0590C4C51604B1E3DCF5495DDA9D97C1 (SwiftUI) + +// MARK: - AccessibilityTechnologies + +/// Accessibility technologies available to the system. +@available(OpenSwiftUI_v3_0, *) +public struct AccessibilityTechnologies: SetAlgebra, Sendable { + + // MARK: - Static Properties + + /// The value that represents the VoiceOver screen reader, allowing use + /// of the system without seeing the screen visually. + public static let voiceOver: AccessibilityTechnologies = .init(list: [.voiceOver]) + + /// The value that represents a Switch Control, allowing the use of the + /// entire system using controller buttons, a breath-controlled switch or similar hardware. + public static let switchControl: AccessibilityTechnologies = .init(list: [.switchControl]) + + @_spi(Private) + public static let fullKeyboardAccess: AccessibilityTechnologies = .init(list: [.fullKeyboardAccess]) + + @_spi(Private) + @available(OpenSwiftUI_v4_0, *) + public static let voiceControl: AccessibilityTechnologies = .init(list: [.voiceControl]) + + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public static let hoverText: AccessibilityTechnologies = .init(list: [.hoverText]) + + static let focusSupportingTechnologies: AccessibilityTechnologies = .init(technologySet: .focusSupportingTechnologies) + + // MARK: - Initializers + + private var technologySet: AccessibilityTechnologySet + + /// Creates a new accessibility technologies structure with an empy accessibility technology set. + public init() { + technologySet = [] + } + + package init(list: [AccessibilityTechnology]) { + var set = AccessibilityTechnologySet() + for technology in list { + set.insert(.init(rawValue: 1 << technology.rawValue)) + } + technologySet = set + } + + private init(technologySet: AccessibilityTechnologySet) { + self.technologySet = technologySet + } + + // MARK: - SetAlgebra + + public func union(_ other: AccessibilityTechnologies) -> AccessibilityTechnologies { + .init(technologySet: technologySet.union(other.technologySet)) + } + + public mutating func formUnion(_ other: AccessibilityTechnologies) { + technologySet.formUnion(other.technologySet) + } + + public func intersection(_ other: AccessibilityTechnologies) -> AccessibilityTechnologies { + .init(technologySet: technologySet.intersection(other.technologySet)) + } + + public mutating func formIntersection(_ other: AccessibilityTechnologies) { + technologySet.formIntersection(other.technologySet) + } + + public func symmetricDifference(_ other: AccessibilityTechnologies) -> AccessibilityTechnologies { + .init(technologySet: technologySet.symmetricDifference(other.technologySet)) + } + + public mutating func formSymmetricDifference(_ other: AccessibilityTechnologies) { + technologySet.formSymmetricDifference(other.technologySet) + } + + public func contains(_ member: AccessibilityTechnologies) -> Bool { + technologySet.isSuperset(of: member.technologySet) + } + + @discardableResult + public mutating func insert(_ newMember: AccessibilityTechnologies) -> (inserted: Bool, memberAfterInsert: AccessibilityTechnologies) { + let isNew = !technologySet.isSuperset(of: newMember.technologySet) + technologySet.formUnion(newMember.technologySet) + return (isNew, .init(technologySet: newMember.technologySet)) + } + + @discardableResult + public mutating func remove(_ member: AccessibilityTechnologies) -> AccessibilityTechnologies? { + guard technologySet.isSuperset(of: member.technologySet) else { return nil } + technologySet.subtract(member.technologySet) + return member + } + + @discardableResult + public mutating func update(with newMember: AccessibilityTechnologies) -> AccessibilityTechnologies? { + let existing = technologySet.isSuperset(of: newMember.technologySet) ? newMember : nil + technologySet.formUnion(newMember.technologySet) + return existing + } + + // MARK: - Equatable + + public static func == (lhs: AccessibilityTechnologies, rhs: AccessibilityTechnologies) -> Bool { + lhs.technologySet == rhs.technologySet + } +} + +// MARK: - AccessibilityTechnologySet + +private struct AccessibilityTechnologySet: OptionSet, Hashable, Codable { + package var rawValue: UInt16 + + package init(rawValue: UInt16) { + self.rawValue = rawValue + } + + var list: [AccessibilityTechnology] { + AccessibilityTechnology.allCases.filter { technology in + contains(AccessibilityTechnologySet(rawValue: 1 << technology.rawValue)) + } + } + + static let focusSupportingTechnologies: AccessibilityTechnologySet = { + var set = AccessibilityTechnologySet() + for technology in AccessibilityTechnology.focusSupportingTechnologies { + set.insert(.init(rawValue: 1 << technology.rawValue)) + } + return set + }() + + func assertAllSupportFocus() { + for technology in list { + guard technology.rawValue < 2 else { + Log.runtimeIssues("Technology %@ does not support Accessibility focus!", [String(describing: technology)]) + continue + } + } + } +} + +// MARK: - AccessibilityTechnology + +/// An assistive access technology. +package enum AccessibilityTechnology: UInt16, CaseIterable, Hashable { + case voiceOver = 0 + case switchControl = 1 + case fullKeyboardAccess = 2 + case voiceControl = 3 + case hoverText = 4 + case assistiveAccess = 5 + + package static let focusSupportingTechnologies: [AccessibilityTechnology] = allCases.filter { $0.rawValue <= 1 } +} diff --git a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift index 34868bab5..fa00d67d5 100644 --- a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift +++ b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift @@ -3,7 +3,7 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: 005A2BB2D44F4D559B7E508DC5B95FFB (SwiftUI) #if canImport(UIKit) @@ -161,7 +161,7 @@ extension UITraitCollection { result.backgroundMaterial == nil { result.backgroundMaterial = .thick } - _openSwiftUIUnimplementedWarning() + AccessibilityCore.UserSettings.resolve(into: &result) return result } diff --git a/Sources/OpenSwiftUI/Integration/PlatformItemList.swift b/Sources/OpenSwiftUI/Integration/PlatformItemList.swift index d6b7b33ea..0ded29cb1 100644 --- a/Sources/OpenSwiftUI/Integration/PlatformItemList.swift +++ b/Sources/OpenSwiftUI/Integration/PlatformItemList.swift @@ -5,12 +5,34 @@ // Status: Empty // ID: CE84B1BFBEAEAB6361605407E54625A3 (SwiftUI) +import Foundation + // FIXME package struct PlatformItemList { var items: [Item] // FIXME - struct Item {} + struct Item { + var text: NSAttributedString? + var secondaryText: NSAttributedString? + var platformIdentifier: String? + var isExternal: Bool = false + var hierarchicalLevel: Int = 0 + // var imageColorResolver: mageColorResolver? + var isEnabled: Bool = false + var resolvedImage: Image.Resolved? + var namedResolvedImage: Image.NamedResolved? + // TODO + var label: NSAttributedString? + var tooltip: String? + var badge: String? + // TODO + } + + var mergedContentItems: Item { + // FIXME + .init() + } fileprivate struct Key: PreferenceKey { static let defaultValue: PlatformItemList = .init(items: []) diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift index b0220c192..6c7d6271b 100644 --- a/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift @@ -192,7 +192,7 @@ extension EnvironmentValues { /// Whether the setting for playing animations in an animated image is /// on. When this value is false, any presented image that contains /// animation should not play automatically. - public private(set) var accessibilityPlayAnimatedImages: Bool { + public package(set) var accessibilityPlayAnimatedImages: Bool { get { self[AccessibilityPlayAnimatedImagesKey.self] } set { self[AccessibilityPlayAnimatedImagesKey.self] = newValue } }