How can I simulate a swipe gesture programmatically?

5.2k views Asked by At

I'm currently trying to write some acceptance tests for our new iOS application using frank (and in turn UISpec). Whilst the framework supports touches as a basic way to interact with views, it doesn't currently support any more involved gestures (eg, pinch, swipe etc). I need to add support for swiping at least because this is core to the functionality of our application and our tests will be pretty well useless without it.

Implementing this should be fairly straightforward if I can find a way to simulate the events in Cocoa. It is possible to send swipe gestures if you use Apple's UIAutomation framework (see here) so theres an example of something that generates these events externally. I've searched around on the web but not found any examples of people doing this (though there was a thread where someone was asking for something similar to this before...).

Many thanks in advance for your help/ideas...

1

There are 1 answers

1
jkp On

I spent yesterday trying to get this working at ended up at a solution of sorts. I'm not entirely happy with it but it was the best I could do for now - if anyone can suggest any improvements or a working alternative I'd welcome them...

Anyway, for anyone else trying to do something similar. I based my solution on the API detailed in this post - I recorded the sequence of events I wanted to simulate, then played them back. The only hitch is that I couldn't get the built-in playback API to work (I got the same crash mentioned in the comments mentioned at the bottom). After some time digging around in ASM land, I ended up writing my own version.

@implementation UIApplication (EventReplay)

///
/// - replayEventsFromFile:
///
- (void)replayEventsFromFile:(NSString *)filename 
{
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
  NSString *filePath = [[paths objectAtIndex:0] stringByAppendingPathComponent:filename];
  NSArray* eventList = [[NSArray arrayWithContentsOfFile:filePath] retain];
  [self replayEvents:eventList];
}

///
/// - replayEvents:
///
- (void)replayEvents:(NSArray *)events
{
  if (!events.count)
    return;

  NSDictionary *eventDict = [events objectAtIndex:0U];
  GSEventRef thisEvent = GSEventCreateWithPlist((CFDictionaryRef)eventDict);

  uint64_t eventTime = thisEvent->record.timestamp;
  thisEvent->record.timestamp = mach_absolute_time();

  mach_port_t appPort = GSCopyPurpleNamedPort([[[NSBundle mainBundle] bundleIdentifier] UTF8String]);
  GSSendEvent(&thisEvent->record, appPort);
  mach_port_deallocate(mach_task_self(), appPort); 

  if (events.count <= 1)
    return;

  NSIndexSet *remainderIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, events.count - 1)];
  NSArray *remainingEvents = [events objectsAtIndexes:remainderIndexes];

  GSEventRef nextEvent = GSEventCreateWithPlist((CFDictionaryRef)[remainingEvents objectAtIndex:0U]);
  NSTimeInterval nextEventDelay = GetTimeDelta(nextEvent->record.timestamp, eventTime);

  if (nextEventDelay > 0.05)
    [self performSelector:@selector(replayEvents:) withObject:remainingEvents afterDelay:nextEventDelay];
  else
    [self replayEvents:remainingEvents];

  CFRelease(nextEvent);
  CFRelease(thisEvent);
}

@end

The snippet above shows how I'm playing back the events. My implementation is fairly crufty - you'll see I had to fudge around the fact that if I blindly use a timer to schedule the next event sometimes it doesn't fire - seems to be when the delay is too small. The horrible hack you see seems to make things work OK

Anyway, hope this helps someone else in some way.