If you can't touch this, it's Hammer time!
Table of Contents
Hammer is a touch, mouse, stylus and keyboard synthesis library for emulating user interaction events. It enables better ways of triggering UI actions in unit tests, replicating a real world environment as much as possible.
This fork requires Swift 5.9 and iOS 15.0 or macOS 12.0 or later.
With SwiftPM
.package(url: "https://github.com/OpenSwiftUIProject/Hammer.git", branch: "main")Tuist generates an Xcode workspace that runs iOS and macOS tests in
TestHost through the shared Hammer scheme. Project generation
requires macOS 15 or later and Xcode 26 or later. Run make generate
to install the Tuist version in mise.toml and generate Hammer.xcworkspace.
The workspace uses HammerTests.xcodeproj so it can coexist with a project
generated by another workspace that uses Hammer as a local Swift package.
All targets use automatic signing. Set TUIST_DEVELOPMENT_TEAM when generating
the workspace to select a signing team for device builds:
TUIST_DEVELOPMENT_TEAM=YOUR_TEAM_ID make generateTo use Hammer in another Xcode project, select your test target and choose a host application in the General tab. This can be your main application or an empty wrapper like TestHost. SwiftPM does not create the host application.
GitHub Actions runs the hosted tests on macOS, iPhone, and iPad for pushes to
main and pull requests targeting main. The test workflow uses Xcode 26.6
and retains test logs and result bundles for seven days.
Call Hammer APIs from the main actor on both iOS and macOS. Mark test suites or methods with @MainActor.
Hammer allows you to simulate fingers, stylus and keyboard events. It also provides various convenience methods to simulate higher level user interactions.
To be able to send events to a view you must first create an EventGenerator:
// Initialize for an existing UIWindow, ensure that the window is key and visible.
let eventGenerator = EventGenerator(window: myWindow)
// Initialize for a UIView, automatically wrapping it in a temporary window.
let eventGenerator = EventGenerator(view: myView)
// Initialize for a UIViewController, automatically wrapping it in a temporary window.
let eventGenerator = EventGenerator(viewController: myViewController)When simulating finger or stylus touches, there are multiple ways of specifying a touch location:
- Default: If you don't specify a location it will use the center of the screen.
- Point: A CGPoint in screen coordinates.
- View: A reference to a UIView or UIViewController, the location will be the center of the visible part of the view.
- Identifier: An accessibility identifier string of a view, the location will be the center of the visible part of the view.
By default, Hammer will display simulated touches over the view. You can change this behavior for your event generator.
eventGenerator.showTouches = falseFingers are the most common method of user interaction on iOS. Hammer supports handling multiple fingers on the screen simultaneously, up to the limit on the device. You can specify the specific finger index you would like to use, if unspecified it will choose the most appropriate one automatically.
Primitive events are the basic building blocks of user interactions, they can be combined together to create full gestures. Some methods will allow you to specify a duration and will interpolate the changes during that time.
try eventGenerator.fingerDown(at: CGPoint(x: 10, y: 10))
try eventGenerator.fingerMove(to: CGPoint(x: 20, y: 10), duration: 0.5)
try eventGenerator.fingerUp()For convenience, Hammer provides many higher level gestures. If you don't specify a location it will automatically default to the center of the view.
try eventGenerator.fingerTap()
try eventGenerator.fingerDoubleTap()
try eventGenerator.fingerLongPress()
try eventGenerator.twoFingerTap()Many advanced gestures are also available.
try eventGenerator.fingerDrag(from: CGPoint(x: 10, y: 10), to: CGPoint(x: 20, y: 10), duration: 0.5)
try eventGenerator.fingerPinch(fromDistance: 100, toDistance: 50, duration: 0.5)
try eventGenerator.fingerRotate(angle: .pi, duration: 0.5)Stylus is available when running on an iPad. It allows for additional properties like pressure, altitude and azimuth to be specified.
Similar to fingers, primitive events are the basic building blocks of stylus interactions.
try eventGenerator.stylusDown(at: CGPoint(x: 10, y: 10), azimuth: 0, altitude: 0, pressure: 0.5)
try eventGenerator.stylusMove(to: CGPoint(x: 20, y: 10), duration: 0.5)
try eventGenerator.stylusUp()Hammer also provides many higher level gestures for Stylus. If you don't specify a location it will automatically default to the center of the view.
try eventGenerator.stylusTap()
try eventGenerator.stylusDoubleTap()
try eventGenerator.stylusLongPress()Keyboard methods take an explicit KeyboardKey object or a Character. Characters will be mapped to their closest keyboard key, you must wrap them with a shift key modifier if needed. This means that specifying a lowercase "a" character is equivalent to specifying an uppercase "A", this is also true for keys with symbols.
// Explicit `KeyboardKey`
try eventGenerator.keyDown(.letterA)
try eventGenerator.keyUp(.letterA)
// Automatic `Character` mapping
try eventGenerator.keyDown("a")
try eventGenerator.keyUp("a")
// Convenience key down and up events
try eventGenerator.keyPress(.letterA)
try eventGenerator.keyPress("a")To type characters or longer strings and get automatic shift wrapping you can use the keyType() methods.
try eventGenerator.keyType("This will type the string as specified, including symbols!")The AppKit backend sends left mouse events through NSApplication. It supports
clicks, double clicks, long presses, and drags, including controls that run a
nested mouse tracking loop. Trackpad touch streams are not synthesized.
Create the generator from an existing NSView, NSViewController, or NSWindow.
The caller owns the window and its content. EventGenerator does not create a
window or change application focus. Keep AppKit tests on the main actor and run
them serially.
For isolated tests, create one HammerWindow(size:) for the suite or test host
and replace its content between tests. It stays outside all screens and reports
key state locally, so AppKit can process clicks while the application is in the
background. It does not become NSApplication.keyWindow. Use a normal window
when testing application activation or keyboard focus.
Set HAMMER_SHOW_TEST_WINDOW=1 in the test scheme's environment, or pass
showWindow: true, to show the test window while debugging. Release any held
mouse button and remove the content after each test. Close the window when the
suite or host no longer needs it.
@MainActor
func testClick(in view: NSView) async throws {
let events = try EventGenerator(view: view)
try await events.waitUntilWindowIsReady()
try await events.mouseClick()
try await events.mouseDoubleClick()
try await events.mouseLongPress(duration: 0.5)
try await events.mouseDrag(
from: RelativeLocation(location: view, x: 0.25, y: 0.5),
to: RelativeLocation(location: view, x: 0.75, y: 0.5),
duration: 0.3
)
}AppKit operations are asynchronous so the application can process events while
the test is suspended. High-level interactions release the mouse if they throw
or are cancelled. When using mouseDown, mouseMove, and mouseUp directly,
the caller must release the mouse on error.
Locations accept window-coordinate points, rectangles, views, view controllers,
and view accessibility identifiers. RelativeLocation uses top-left fractions
for both flipped and unflipped views. OffsetLocation uses window coordinates,
where positive y moves upward. waitUntil(_:timeout:) provides a bounded async
wait for observable results.
Run the AppKit regression tests with make test-macOS.
The shared HammerTests target selects tests with #if os(macOS) and #if os(iOS).
When running on a full screen app or testing navigation, specifying a CGPoint in screen coordinates can be difficult. For this, Hammer provides convenience methods to find views in the hierarchy by their accessibility identifier.
let myButton = try eventGenerator.viewWithIdentifier("my_button", ofType: UIButton.self)
try eventGenerator.fingerTap(at: myButton)This method will throw an error if the view was not found in the hierarchy. If you're testing navigation or screen changes and you need to wait until the view appears, you can add a timeout. This will wait until the hierarchy has updated and return the view.
let myButton = try eventGenerator.viewWithIdentifier("my_button", ofType: UIButton.self, timeout: 1)
try eventGenerator.fingerTap(at: myButton)You can also pass accessibility identifiers directly to the event methods.
try eventGenerator.fingerDown(at: "my_draggable_object")
try eventGenerator.fingerMove(to: "drop_target", duration: 0.5)
try eventGenerator.fingerUp()You will often need to wait for the simulator to finish displaying something on the screen or for an animation to end. Hammer provides multiple methods to wait until a view is visible on screen or if a control is hittable
try eventGenerator.waitUntilVisible("my_label", timeout: 1)
try eventGenerator.waitUntilHittable("my_button", timeout: 1)- The app or window is not ready for interaction
Make sure you are running your unit tests in a host application (setup instructions). To interact with a view, it must be visible on the screen and the application must have finished presenting. You can test this by adding a delay to your testing and verifying that your view is appearing on screen.
- View is not in hirarchy / Unable to find view
Make sure the view you specified is in the same hierarchy as the view that was used to create the EventGenerator. If you used an accessibility identifier, check that it was spelled correctly.
- View is not visible
This means that the view is in the hierarchy but is not currently visible on screen, so it's not possible to generate touches for it. Make sure that the view is within visible bounds, not covered by other views, not hidden, and with alpha greater than 0.01.
- View is not hittable
This means that the view is in the hierarchy and visible on screen but is not currently able to receive touches. Make sure that the view reponds to hit test in its center coordinate and user interaction is enabled.
Hammer is released under the Apache License. See LICENSE

