Skip to content

Commit

Permalink
Fix
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriellanata committed Jul 16, 2021
1 parent 433803a commit fbb6569
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension EventGenerator {
/// - parameter locations: The locations where to touch down.
public func fingerDown(_ indices: [FingerIndex?] = .automatic, at locations: [HammerLocatable]) throws {
let indices = try self.fillNextFingerIndices(indices, withExpected: locations.count)
let locations = try locations.map { try $0.screenHitPoint(for: self) }
let locations = try locations.map { try $0.windowHitPoint(for: self) }
try self.checkPointsAreHittable(locations)
try self.sendEvent(hand: HandInfo(fingers: zip(locations, indices).map { location, index in
FingerInfo(fingerIndex: index, location: location, phase: .began,
Expand Down Expand Up @@ -119,7 +119,7 @@ extension EventGenerator {
/// - parameter locations: The new locations of the fingers.
public func fingerMove(_ indices: [FingerIndex?] = .automatic, to locations: [HammerLocatable]) throws {
let indices = try self.fillExistingFingerIndices(indices, withMinimum: locations.count)
let locations = try locations.map { try $0.screenHitPoint(for: self) }
let locations = try locations.map { try $0.windowHitPoint(for: self) }
let fingers = zip(locations, indices).map { location, index in
FingerInfo(fingerIndex: index, location: location, phase: .moved,
pressure: 0, twist: 0, majorRadius: kDefaultRadius, minorRadius: kDefaultRadius)
Expand Down Expand Up @@ -154,7 +154,7 @@ extension EventGenerator {
}

let indices = try self.fillExistingFingerIndices(indices, withMinimum: locations.count)
let locations = try locations.map { try $0.screenHitPoint(for: self) }
let locations = try locations.map { try $0.windowHitPoint(for: self) }
let startLocations = self.activeTouches.fingers(forIndices: indices).map(\.location)

let startTime = Date()
Expand Down Expand Up @@ -217,7 +217,7 @@ extension EventGenerator {
angle radians: CGFloat = 0) throws
{
let indices = try self.fillNextFingerIndices(indices, withExpected: 2)
let location = try (location ?? self.mainView).screenHitPoint(for: self)
let location = try (location ?? self.mainView).windowHitPoint(for: self)
try self.fingerDown(indices, at: location.twoWayOffset(distance, angle: radians))
}

Expand Down Expand Up @@ -245,7 +245,7 @@ extension EventGenerator {
angle radians: CGFloat = 0) throws
{
let indices = try self.fillExistingFingerIndices(indices, withMinimum: 2)
let location = try location.screenHitPoint(for: self)
let location = try location.windowHitPoint(for: self)
try self.fingerMove(indices, to: location.twoWayOffset(distance, angle: radians))
}

Expand All @@ -263,7 +263,7 @@ extension EventGenerator {
withDistance distance: CGFloat = EventGenerator.twoFingerDistance,
angle radians: CGFloat = 0, duration: TimeInterval) throws
{
let location = try location.screenHitPoint(for: self)
let location = try location.windowHitPoint(for: self)
try self.fingerMove(indices, to: location.twoWayOffset(distance, angle: radians), duration: duration)
}

Expand Down Expand Up @@ -300,7 +300,7 @@ extension EventGenerator {
angle radians: CGFloat = 0, duration: TimeInterval) throws
{
let indices = try self.fillNextFingerIndices(indices, withExpected: 2)
let location = try (location ?? self.mainView).screenHitPoint(for: self)
let location = try (location ?? self.mainView).windowHitPoint(for: self)
let startLocations = location.twoWayOffset(startDistance, angle: radians)
let endLocations = location.twoWayOffset(endDistance, angle: radians)
try self.fingerDown(indices, at: startLocations)
Expand Down Expand Up @@ -352,7 +352,7 @@ extension EventGenerator {
angle radians: CGFloat) throws
{
let indices = try self.fillExistingFingerIndices(indices, withMinimum: 1)
let anchor = try anchor.screenHitPoint(for: self)
let anchor = try anchor.windowHitPoint(for: self)
let locations = self.activeTouches.fingers(forIndices: indices).map(\.location)
try self.fingerMove(indices, to: locations.map { $0.pivot(anchor: anchor, angle: radians) })
}
Expand All @@ -376,7 +376,7 @@ extension EventGenerator {
}

let indices = try self.fillExistingFingerIndices(indices, withMinimum: 1)
let anchor = try anchor.screenHitPoint(for: self)
let anchor = try anchor.windowHitPoint(for: self)
let startLocations = self.activeTouches.fingers(forIndices: indices).map(\.location)

let startTime = Date()
Expand Down Expand Up @@ -413,7 +413,7 @@ extension EventGenerator {
duration: TimeInterval) throws
{
let indices = try self.fillNextFingerIndices(indices, withExpected: 2)
let location = try (location ?? self.mainView).screenHitPoint(for: self)
let location = try (location ?? self.mainView).windowHitPoint(for: self)
try self.fingerDown(indices, at: location.twoWayOffset(distance, angle: startRadians))
try self.fingerPivot(indices, aroundAnchor: location, byAngle: endRadians - startRadians,
duration: duration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension EventGenerator {
public func stylusDown(at location: HammerLocatable? = nil,
azimuth: CGFloat = 0, altitude: CGFloat = 0, pressure: CGFloat = 0) throws
{
let location = try (location ?? self.mainView).screenHitPoint(for: self)
let location = try (location ?? self.mainView).windowHitPoint(for: self)
try self.checkPointsAreHittable([location])
try self.sendEvent(stylus: StylusInfo(location: location, phase: .began,
pressure: pressure, twist: 0,
Expand Down Expand Up @@ -110,7 +110,7 @@ extension EventGenerator {
public func stylusMove(to location: HammerLocatable,
azimuth: CGFloat = 0, altitude: CGFloat = 0, pressure: CGFloat = 0) throws
{
let location = try location.screenHitPoint(for: self)
let location = try location.windowHitPoint(for: self)
try self.sendEvent(stylus: StylusInfo(location: location, phase: .moved, pressure: pressure, twist: 0,
altitude: altitude, azimuth: azimuth))
}
Expand All @@ -130,7 +130,7 @@ extension EventGenerator {
throw HammerError.touchForStylusDoesNotExist
}

let location = try location.screenHitPoint(for: self)
let location = try location.windowHitPoint(for: self)

let startLocation = existingStylus.location
let startAzimuth = existingStylus.azimuth
Expand Down
18 changes: 9 additions & 9 deletions Sources/Hammer/EventGenerator/HammerLocatable.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import UIKit

public protocol HammerLocatable {
func screenHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint
func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint
}

extension CGPoint: HammerLocatable {
public func screenHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
public func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return self
}
}

extension CGRect: HammerLocatable {
public func screenHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
public func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return self.center
}
}

extension UIView: HammerLocatable {
public func screenHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return try eventGenerator.screenHitPoint(forView: self)
public func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return try eventGenerator.windowHitPoint(forView: self)
}
}

extension UIViewController: HammerLocatable {
public func screenHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return try self.view.screenHitPoint(for: eventGenerator)
public func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return try self.view.windowHitPoint(for: eventGenerator)
}
}

extension String: HammerLocatable {
public func screenHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return try eventGenerator.viewWithIdentifier(self).screenHitPoint(for: eventGenerator)
public func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint {
return try eventGenerator.viewWithIdentifier(self).windowHitPoint(for: eventGenerator)
}
}
88 changes: 68 additions & 20 deletions Sources/Hammer/Utilties/Subviews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ extension EventGenerator {
}

// Recursive
func viewIsVisible(_ view: UIView, currentView: UIView, visibility: Visibility) -> Bool {
func viewIsVisible(currentView: UIView) -> Bool {
guard !currentView.isHidden && currentView.alpha >= 0.01 else {
return false
}
Expand All @@ -168,20 +168,19 @@ extension EventGenerator {
return currentView == self.window
}

let adjustedRect = view.convert(view.bounds, to: superview)
guard superview.bounds.isVisible(adjustedRect, visibility: visibility) else {
guard superview.bounds.isVisible(view.frame, visibility: visibility) else {
return false
}

return viewIsVisible(view, currentView: superview, visibility: visibility)
return viewIsVisible(currentView: superview)
}

return viewIsVisible(view, currentView: view, visibility: visibility)
return viewIsVisible(currentView: view)
}

/// Returns if the specified rect is visible.
///
/// - parameter rect: The rect in screen coordinates
/// - parameter rect: The rect in window coordinates
/// - parameter visibility: How determine if the view is visible.
///
/// - returns: If the rect is visible
Expand All @@ -191,7 +190,7 @@ extension EventGenerator {

/// Returns if the specified point is visible.
///
/// - parameter point: The point in screen coordinates
/// - parameter point: The point in window coordinates
///
/// - returns: If the point is visible
public func pointIsVisible(_ point: CGPoint) -> Bool {
Expand All @@ -203,28 +202,70 @@ extension EventGenerator {
/// NOTE: This will also return false if the view for the accessibility identifier is not found.
///
/// - parameter accessibilityIdentifier: The identifier to check.
/// - parameter point: A point to check if hittable in the view's coordinate space. If
/// nil, it will use the center of the view's visible area.
///
/// - returns: If the view is hittable
public func viewIsHittable(_ accessibilityIdentifier: String) -> Bool {
public func viewIsHittable(_ accessibilityIdentifier: String, atPoint point: CGPoint? = nil) -> Bool {
guard let view = try? self.viewWithIdentifier(accessibilityIdentifier) else {
return false
}

return self.viewIsHittable(view)
return self.viewIsHittable(view, atPoint: point)
}

/// Returns if the specified view is hittable.
///
/// - parameter view: The view to check.
/// - parameter view: The view to check.
/// - parameter point: A point to check if hittable in the view's coordinate space. If nil, it will use
/// the center of the view's visible area.
///
/// - returns: If the view is hittable
public func viewIsHittable(_ view: UIView) -> Bool {
return (try? self.screenHitPoint(forView: view)) != nil
public func viewIsHittable(_ view: UIView, atPoint point: CGPoint? = nil) -> Bool {
guard self.viewIsVisible(view) else {
return false
}

let point = point ?? {
let windowHitPoint = self.internalWindowHitPoint(forView: view)
return view.convert(windowHitPoint, from: self.window)
}()

// Check if hittable through standard piping
let windowHitPoint = view.convert(point, to: self.window)
let windowHitTest = self.window.hitTest(windowHitPoint, with: nil)
if windowHitTest == view {
// If the hit test returns the target view we know it is hittable
return true
} else if windowHitTest == nil {
// If the hit test returns nil there is no interactive view
return false
}

// Recursive
func viewIsHittable(currentView: UIView) -> Bool {
guard currentView.isUserInteractionEnabled else {
return false
}

let adjustedPoint = currentView.convert(point, from: view)
guard currentView.point(inside: adjustedPoint, with: nil) else {
return false
}

guard let superview = currentView.superview else {
return currentView == self.window
}

return viewIsHittable(currentView: superview)
}

return viewIsHittable(currentView: view)
}

/// Returns if the specified point has a hittable view at that location.
///
/// - parameter point: The point in screen coordinates
/// - parameter point: The point in window coordinates
///
/// - returns: If the point is hittable
public func pointIsHittable(_ point: CGPoint) -> Bool {
Expand All @@ -233,7 +274,7 @@ extension EventGenerator {

/// Checks if the specified points have a hittable view at that location.
///
/// - parameter points: The points in screen coordinates
/// - parameter points: The points in window coordinates
///
/// - throws: If one of the points is not hittable
func checkPointsAreHittable(_ points: [CGPoint]) throws {
Expand All @@ -251,7 +292,7 @@ extension EventGenerator {
/// - throws: And error if the view is not in the same hierarchy, not visible or not hittable.
///
/// - returns: If the view is hittable
public func screenHitPoint(forView view: UIView) throws -> CGPoint {
public func windowHitPoint(forView view: UIView) throws -> CGPoint {
guard view.isDescendant(of: self.window) else {
throw HammerError.viewIsNotInHierarchy(view)
}
Expand All @@ -260,13 +301,20 @@ extension EventGenerator {
throw HammerError.viewIsNotVisible(view)
}

let viewFrame = view.convert(view.bounds, to: self.window)
let screenHitPoint = self.window.bounds.intersection(viewFrame).center
let viewHitPoint = view.convert(screenHitPoint, from: self.window)
guard view.isUserInteractionEnabled && view.point(inside: viewHitPoint, with: nil) else {
guard self.viewIsHittable(view) else {
throw HammerError.viewIsNotHittable(view)
}

return screenHitPoint
return self.internalWindowHitPoint(forView: view)
}

/// Returns a possible hittable point in the specified view without any validation.
///
/// - parameter view: The view to hit
///
/// - returns: A possible hittable point
private func internalWindowHitPoint(forView view: UIView) -> CGPoint {
let viewBounds = view.convert(view.bounds, to: self.window)
return self.window.bounds.intersection(viewBounds).center
}
}
29 changes: 28 additions & 1 deletion Tests/HammerTests/HandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ final class HandTests: XCTestCase {
XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: 1), .completed)
}

func testButtonTapOnNonInteractiveSuperview() throws {
let view = UIButton(frame: CGRect(x: 10, y: 10, width: 50, height: 50))
view.setContentHuggingPriority(.required, for: .vertical)
view.setContentHuggingPriority(.required, for: .horizontal)
view.accessibilityIdentifier = "my_button"
view.backgroundColor = .green

let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 70, height: 70))
containerView.setContentHuggingPriority(.required, for: .vertical)
containerView.setContentHuggingPriority(.required, for: .horizontal)
containerView.backgroundColor = .blue
containerView.isUserInteractionEnabled = false
containerView.addSubview(view)

let eventGenerator = try EventGenerator(view: containerView)
try eventGenerator.wait(0.5)

do {
try eventGenerator.fingerTap(at: "my_button")
XCTFail("Button should not be tappable")
} catch HammerError.viewIsNotHittable {
// Success
} catch {
throw error
}
}

func testButtonHighlight() throws {
let view = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
view.setContentHuggingPriority(.required, for: .vertical)
Expand Down Expand Up @@ -258,8 +285,8 @@ final class HandTests: XCTestCase {
try eventGenerator.fingerPinchOpen(duration: 1)
try eventGenerator.wait(0.3)
XCTAssertEqual(view.zoomScale, 6.9, accuracy: 1)
try eventGenerator.wait(0.3)
try eventGenerator.fingerPinchClose(duration: 1)
try eventGenerator.wait(0.3)
XCTAssertEqual(view.zoomScale, 1, accuracy: 0.1)
}
}

0 comments on commit fbb6569

Please sign in to comment.