diff --git a/README.md b/README.md index 7de0e36..0b5b5da 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Holding a scroll key scrolls continuously at 600 pt/sec. ```swift dependencies: [ - .package(url: "https://github.com/takumatt/vimulator", from: "0.1.0") + .package(url: "https://github.com/takumatt/vimulator", from: "0.3.0") ] ``` @@ -140,7 +140,7 @@ Vimulator.shared.appearAnimation = .fade(duration: 0.3) ## How it works - **Keyboard monitoring** — swizzles `UIApplication.sendEvent` to intercept hardware key events globally -- **Hint mode** — walks the UIAccessibility tree to collect interactive elements, renders a transparent `UIWindow` overlay with hint labels +- **Hint mode** — walks the UIAccessibility tree to collect interactive elements and other named elements that opt into accessibility user interaction, then renders a transparent `UIWindow` overlay with hint labels - **Activation** — calls `accessibilityActivate()` on the target element; falls back to `becomeFirstResponder()` for text inputs and `UIControl.sendActions` for controls - **Scrolling** — uses `CADisplayLink` to move `contentOffset` at a fixed velocity per frame, with `adjustedContentInset`-aware clamping diff --git a/Sources/Vimulator/AccessibilityScanner.swift b/Sources/Vimulator/AccessibilityScanner.swift index 906f88d..5933215 100644 --- a/Sources/Vimulator/AccessibilityScanner.swift +++ b/Sources/Vimulator/AccessibilityScanner.swift @@ -40,7 +40,17 @@ enum AccessibilityScanner { private static func isInteractive(_ element: NSObject) -> Bool { if let control = element as? UIControl { return control.isEnabled } - return !element.accessibilityTraits.intersection(interactiveTraits).isEmpty + if !element.accessibilityTraits.intersection(interactiveTraits).isEmpty { + return true + } + return element.accessibilityRespondsToUserInteraction && hasAccessibleName(element) + } + + private static func hasAccessibleName(_ element: NSObject) -> Bool { + guard let label = element.accessibilityLabel else { + return false + } + return !label.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } #endif diff --git a/Tests/VimulatorTests/AccessibilityScannerTests.swift b/Tests/VimulatorTests/AccessibilityScannerTests.swift index 486763f..523e68a 100644 --- a/Tests/VimulatorTests/AccessibilityScannerTests.swift +++ b/Tests/VimulatorTests/AccessibilityScannerTests.swift @@ -47,6 +47,34 @@ final class AccessibilityScannerTests: XCTestCase { XCTAssertEqual(results.count, 1) } + func testFindsElementsWithCustomAccessibilityActivation() { + let root = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 812)) + let activatable = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 44)) + activatable.isAccessibilityElement = true + activatable.accessibilityTraits = .staticText + activatable.accessibilityLabel = "Search" + activatable.accessibilityRespondsToUserInteraction = true + root.addSubview(activatable) + + let results = AccessibilityScanner.scan(in: root) + + XCTAssertEqual(results.count, 1) + XCTAssertTrue(results.first === activatable) + } + + func testIgnoresCustomAccessibilityActivationWithoutLabel() { + let root = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 812)) + let activatable = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 44)) + activatable.isAccessibilityElement = true + activatable.accessibilityTraits = .staticText + activatable.accessibilityRespondsToUserInteraction = true + root.addSubview(activatable) + + let results = AccessibilityScanner.scan(in: root) + + XCTAssertTrue(results.isEmpty) + } + // MARK: - Performance func testPerformanceShallowHierarchy() {