Skip to content
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
105 changes: 105 additions & 0 deletions Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import SwiftUI
import UIKit

private extension UIColor {
convenience init(hex: UInt32) {
self.init(
red: CGFloat((hex >> 16) & 0xFF) / 255.0,
green: CGFloat((hex >> 8) & 0xFF) / 255.0,
blue: CGFloat(hex & 0xFF) / 255.0,
alpha: 1
)
}
}

public struct ColorPalette {
public var colors: [UIColor]
public var fillOpacity: CGFloat
public var strokeOpacity: CGFloat

public init?(
colors: [UIColor],
fillOpacity: CGFloat = 0.3,
strokeOpacity: CGFloat = 0.3
) {
guard !colors.isEmpty else { return nil }
self.colors = colors
self.fillOpacity = fillOpacity
self.strokeOpacity = strokeOpacity
}

private init(
uncheckedColors colors: [UIColor],
fillOpacity: CGFloat,
strokeOpacity: CGFloat
) {
self.colors = colors
self.fillOpacity = fillOpacity
self.strokeOpacity = strokeOpacity
}

public func color(at index: Int) -> UIColor {
colors[index % colors.count]
}

public func color(at index: Int) -> Color {
Color(colors[index % colors.count])
}

public func fillColor(at index: Int) -> UIColor {
color(at: index).withAlphaComponent(fillOpacity)
}

public func strokeColor(at index: Int) -> UIColor {
color(at: index).withAlphaComponent(strokeOpacity)
}

public func fillColor(at index: Int) -> Color {
Color(color(at: index)).opacity(fillOpacity)
}

public func strokeColor(at index: Int) -> Color {
Color(color(at: index)).opacity(strokeOpacity)
}

// MARK: - Presets

public static let legacy = ColorPalette(
uncheckedColors: MarkerColors.defaultColors,

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_17)

'defaultColors' is deprecated: Use ColorPalette.legacy i

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_17)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_17)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_17)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_17)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_17)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_26)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_26)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_26)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_26)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_26)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_26)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_18)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_18)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_18)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_18)

'defaultColors' is deprecated: Use ColorPalette.legacy instead

Check warning on line 68 in Sources/AccessibilitySnapshot/Core/AccessibilityColorPalette.swift

View workflow job for this annotation

GitHub Actions / Tuist Build (iOS_18)

'MarkerColors' is deprecated: Use ColorPalette.legacy instead
fillOpacity: 0.3,
strokeOpacity: 0.3
)

public static let modern = ColorPalette(
uncheckedColors: [
UIColor(hex: 0x8B57CE),
UIColor(hex: 0x248BCF),
UIColor(hex: 0x387F7F),
UIColor(hex: 0xAE4472),
UIColor(hex: 0xDC4F56),
UIColor(hex: 0x804043),
UIColor(hex: 0x499460),
UIColor(hex: 0x4F6292),
UIColor(hex: 0xD986B1),
UIColor(hex: 0x9F8128),
UIColor(hex: 0x549392),
UIColor(hex: 0x3F607E),
UIColor(hex: 0x8B7230),
],
fillOpacity: 0.25,
strokeOpacity: 1.0
)

public static let `default` = modern
}

public typealias AccessibilityColorPalette = ColorPalette

// MARK: - Legacy Support

/// The original color palette type used before `ColorPalette` was introduced.
@available(*, deprecated, message: "Use ColorPalette.legacy instead")
public enum MarkerColors {
@available(*, deprecated, message: "Use ColorPalette.legacy instead")
public static let defaultColors: [UIColor] = [.cyan, .magenta, .green, .blue, .yellow, .purple, .orange]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import AccessibilitySnapshotParser
import UIKit

// MARK: - Parsed Data

/// Result type passed to subclasses for overlay creation.
public struct ParsedAccessibilityData {
/// The rendered snapshot image of the contained view.
public let image: UIImage

/// The parsed accessibility markers.
public let markers: [AccessibilityMarker]

/// The bounds size of the contained view.
public let containedViewBounds: CGSize
}

// MARK: - Base View

/// Base class that handles the shared capture and parse logic for accessibility snapshots.
///
/// Subclasses implement `render(data:)` to generate layout-engine-specific visuals.
open class AccessibilitySnapshotBaseView: SnapshotAndLegendView {
// MARK: - Public Properties

/// The configuration for snapshot rendering.
public let snapshotConfiguration: AccessibilitySnapshotConfiguration

// MARK: - Internal Properties

/// The view that will be snapshotted.
let containedView: UIView

// MARK: - Life Cycle

/// Initializes a new snapshot container view.
///
/// - parameter containedView: The view that should be snapshotted, and for which the accessibility markers should
/// be generated.
/// - parameter snapshotConfiguration: The configuration for the visual effects and markers applied to the snapshots.
public init(
containedView: UIView,
snapshotConfiguration: AccessibilitySnapshotConfiguration
) {
self.containedView = containedView
self.snapshotConfiguration = snapshotConfiguration

super.init(frame: containedView.bounds)

backgroundColor = .init(white: 0.9, alpha: 1.0)
}

@available(*, unavailable)
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Public Methods

/// Parse the `containedView`'s accessibility and add appropriate visual elements to represent it.
///
/// This must be called _after_ the view is in the view hierarchy.
///
/// - Throws: Throws a `RenderError` when the view fails to render a snapshot of the `containedView`.
public func parseAccessibility() throws {
cleanup()

let viewController = containedView.next as? UIViewController
let originalParent = viewController?.parent
let originalSuperviewAndIndex = containedView.superviewWithSubviewIndex()

viewController?.removeFromParent()
addSubview(containedView)

defer {
containedView.removeFromSuperview()

if let (originalSuperview, originalSubviewIndex) = originalSuperviewAndIndex {
originalSuperview.insertSubview(containedView, at: originalSubviewIndex)
}

if let viewController = viewController, let originalParent = originalParent {
originalParent.addChild(viewController)
}
}

containedView.setNeedsLayout()
containedView.layoutIfNeeded()

let image = try containedView.renderToImage(
configuration: snapshotConfiguration.rendering
)

snapshotView.image = image
snapshotView.bounds.size = containedView.bounds.size

containedView.layoutIfNeeded()

let parser = AccessibilityHierarchyParser()
let markers = parser.parseAccessibilityHierarchy(
in: containedView,
rotorResultLimit: snapshotConfiguration.rotors.resultLimit
).flattenToElements()

let parsedData = ParsedAccessibilityData(
image: image,
markers: markers,
containedViewBounds: containedView.bounds.size
)

render(data: parsedData)
}

// MARK: - Methods for Subclasses to Override

/// Cleans up any previously created overlay views.
open func cleanup() {}

/// Renders the accessibility overlays and legend.
///
/// - Parameter data: The parsed accessibility data including snapshot image and markers.
open func render(data: ParsedAccessibilityData) {
fatalError("Subclasses must implement render(data:)")
}
}

// MARK: - Helper Extension

extension UIView {
/// Returns the superview and the index of this view within the superview's subviews array.
func superviewWithSubviewIndex() -> (UIView, Int)? {
guard let superview = superview else {
return nil
}

guard let index = superview.subviews.firstIndex(of: self) else {
fatalError("Internal inconsistency error: view has a superview, but is not a subview of the superview")
}

return (superview, index)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public struct AccessibilitySnapshotConfiguration {
/// Controls when to show elements' accessibility user input labels (used by Voice Control). Defaults to `.whenOverridden`.
public let inputLabelDisplayMode: AccessibilityContentDisplayMode

/// Whether to show unspoken accessibility traits (keyboardKey, playsSound, etc.) in the legend. Defaults to `true`.
public let showsUnspokenTraits: Bool

// MARK: - Initialization

/// Creates a new accessibility snapshot configuration.
Expand All @@ -73,20 +76,23 @@ public struct AccessibilitySnapshotConfiguration {
/// - includesInputLabels: When to show accessibility user input labels. Defaults to `.whenOverridden`.
/// - includesCustomRotors: When to show accessibility custom rotors and their contents. Defaults to `.whenOverridden`.
/// - rotorResultLimit: Maximum number of rotor results to collect in each direction. Defaults to `10`.
/// - showsUnspokenTraits: Whether to show unspoken traits in the legend. Defaults to `true`.
public init(
viewRenderingMode: ViewRenderingMode,
colorRenderingMode: ColorRenderingMode = .monochrome,
overlayColors: [UIColor] = MarkerColors.defaultColors,
overlayColors: [UIColor] = [],
activationPointDisplay: AccessibilityContentDisplayMode = .whenOverridden,
includesInputLabels: AccessibilityContentDisplayMode = .whenOverridden,
includesCustomRotors: AccessibilityContentDisplayMode = .whenOverridden,
rotorResultLimit: Int = AccessibilityMarker.defaultRotorResultLimit
rotorResultLimit: Int = AccessibilityMarker.defaultRotorResultLimit,
showsUnspokenTraits: Bool = true
) {
rendering = Rendering(renderMode: viewRenderingMode, colorMode: colorRenderingMode)
rotors = Rotors(displayMode: includesCustomRotors, resultLimit: rotorResultLimit)
markerColors = overlayColors.isEmpty ? MarkerColors.defaultColors : overlayColors
markerColors = overlayColors
activationPointDisplayMode = activationPointDisplay
inputLabelDisplayMode = includesInputLabels
self.showsUnspokenTraits = showsUnspokenTraits
}
}

Expand Down
Loading
Loading