Reader beware: this post is part of the older "Objective-C era" on Cocoa with Love. I don't keep these articles up-to-date and many contain code that no longer works or is superceded by newer APIs. Many others contain out-of-date information or offer advice and opinions I no longer endorse. Read "A new era for Cocoa with Love" for more.
The iPhone lacks specific methods to create UIEvent and UITouch objects. I'll show you how to add this functionality so you can write programmatically driven user-interfaces.
Update #1: Added a "Device SDK" version that will link correctly outside of the "Simulator SDK".
Update #2: Two bugs have been fixed since the code was originally posted... the objects in the
NSSets and the
UITouchare now retained.
Update #3: changes to the
UIEventcategories as well as
performTouchInView:to support SDK 2.2 changes.
Update #4: Support for SDK 3.0.
A warning before we begin...
The content of this post is for debugging and testing only. Do not submit this code in an application to the App Store. Doing so will likely result in:
- A bad UI experience for your users.
- An app that breaks on every OS update.
- Rejection of your application.
Synthesized touches are never the right way to trigger actions in a real application. The only useful use for this post is in automated user testing (see my later post Automated User Interface Testing on the iPhone).
When running application tests, it is helpful to be able to generate user events to test your user-interface. That way, you can run user-interface tests automatically instead of manually.
On Cocoa Senior (also known as "the Mac") we have methods like the gargantuan:
to generate events.
Cocoa Junior on the iPhone doesn't have any methods like this, so we must work out how to achieve it ourselves.
A basic touch event normally consists of three objects:
- The UITouch object — which will be used for the touch down and touch up
- A first UIEvent to wrap the touch down
- A second UIEvent to wrap the touch up
Lets look first at creating the UITouch object. Since most of the fields in this object are private, we can't sublcass it or set them directly — everything must be done on a category. My category goes something like this:
This method builds a touch in the center of the specified UIView (window coordinates must be used).
You should note that this category includes the changeToPhase: method. This phase (set to UITouchPhaseBegan in the initInView: method) refers to the begin/drag/ended state of the touch operation. We need a method to change the state because the same UITouch object must be used for touch began and touch ended events (otherwise the whole windowing system crashes).
The UIEvent object is mostly handled through an existing private method (
_initWithEvent:touches:). There are two difficulties with this method though:
- We must provide it a
GSEventobject (or something very close to it)
- We must allocate the object as a UITouchesEvent on SDK 3.0 and later but as a UIEvent on earlier versions.
Here's how all that will look:
You can see that most of the setup is simply concerned with filling in the fields of the
GSEventProxy object which is the pretend object that we substitute in place of the actual
GSEvent object (which can't be easily allocated).
The fields and values used are determined simply by staring at real
GSEvent structures in the debugger until the values for fields could be determined.
The definition of this object follows. You'll need to place it before the previous
UIEvent category implemention.
Sending the event
There is no API to route the events to the appropriate view — so we will just invoke the methods directly on the view ourselves.
Using the above categories to create the UITouch and UIEvent objects, dispatching a touch event to a UIView looks like this:
Legality of use and risks
The approach used in this post constitutes using an undisclosed API — it is therefore illegal to submit applications to the App Store that use this approach, according to the iPhone SDK Agreement.
In terms of risks, this type of undisclosed API use has a high probability of breaking on every update to the iPhone OS — yet another reason why this code is for in-house developer use only.
If you use this code, only use it in a separate target for testing purposes only. Do not submit this code to the App Store.
You can download a copy of TouchSynthesis.m as part of the SelfTesting project (from my later post Automated User Interface Testing on the iPhone).
I have only tested this for performing touch events in UITableViewCells in a UINavigationController — navigating a hierarchy to verify that the hierarchy works. Of course, once you've programmatically navigated, you must also read back from the hierarchy to ensure that required features are present — but that's a post for a different time.
Working directly with the fields of a class is always a little risky. I'm sure there are UIViews that won't work well with this type of synthetic touch. Apple is also free to change the meaning of any fields at any time so this code is prone to break frequently.
Finally, remember to keep this type of testing code in a separate target so it isn't included in the application you submit to the App Store. I don't want to see your projects break or be rejected because you're trying to invoke use undisclosed APIs in your final application.