0

I am trying to create a long press gesture that presents a second view controller, once the press has been held for 3 seconds. However, I only want the second view controller presented if the device is in a certain accelerometer orientation for the ENTIRE 3 seconds. That is, if the gesture is not held long enough or the device is tilted too much, the gesture is dismissed and the user must try again.

// In FirstViewController.h

#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>

@interface FirstViewController : UIViewController

@property (nonatomic, strong) CMMotionManager *motionManager;

@end

// In FirstViewController.m

#import "FirstViewController"
#import "SecondViewController"

@implementation motionManager;

- (void) viewDidLoad
{
    [super viewDidLoad];
    self.motionManager = [[CMMotionManager alloc]init];
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(handleLongPress:)];
    longPress.minimumPressDuration = 3.0;
    [self.view addGestureRecognizer:longPress];
}

- (void) handleLongPress: (UILongPressGestureRecognizer *)sender
{
    // Not sure what to do here
}

I have previously tried chunks of code in the last method but it looks obnoxious and just is not correct. Instead, I have listed several lines of code below that I know work individually, but I need assistance in making them all work together.

// Accelerometer

if ([self.motionManager isAccelerometerAvailable])
{
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [self.motionManager startAccelerometerUpdatesToQueue:queue withHandler:^(CMAccelerometerData *accelerometerData,NSError *error)
    {
        if (ABS(accelerometerData.acceleration.x) < 0.3 && ABS(accelerometerData.acceleration.y) < 0.30 && ABS(accelerometerData.acceleration.z) > 0.70) // Phone is flat and screen faces up
        { 
            NSLog(@"Correct Orientation!!!");
            [self.motionManager stopAccelerometerUpdates];
        }
        else
        {
            NSLog(@"Incorrect orientation!!!");
            [self.motionManager stopAccelerometerUpdates];
        }];
}

else
{
    NSLog(@"Accelerometer is not available.");
}

// Go to second view controller

if (sender.state == UIGestureRecognizerStateBegan)
{
    SecondViewController *svc = [self.storyboard instantiateViewControllerWithIdentifier:@"SecondViewController"];
    [self presentViewController:svc animated:YES completion:nil];
}

Any ideas? Or even a more general way to cancel the gesture unless a condition is met would be very helpful.

Wooble
  • 87,717
  • 12
  • 108
  • 131

3 Answers3

0

You may be able to do what you want by subclassing UIGestureRecognizer to make your own gesture recognizer similar to UILongPressGestureRecognizer that listens for accelerometer data in addition to presses of at least 3 second duration.

Community
  • 1
  • 1
Jake Spencer
  • 1,117
  • 7
  • 13
0

I'd probably override the onTouchesBegan, and onTouchesEnded methods, rather than using a gesture recogniser.

I'd then create a NSTimer object, an NSTimeInterval var and a BOOL in your view controller; for the sake of it, I'll call them, touchTimer, initialTouchTimeStamp, and touchValid.

for the sake of complexity, it's an assumption that the viewControllers view is not multi-touch.

Assume the repeatTime = 0.25f; longPressTimeRequired = 3;

The timer selector would incorporate your accelerometer method, if the data inside your accelerometer method is invalid, I'd set a touchValid to false (and invalidate the timer), otherwise I'd set it to true After checking the accelerometer, I'd check if my initialTouchTimeStamp var is longPressTimeRequired or more seconds prior to [touchTimer fireDate] - repeatTime , if it is, and the touchValid is true, then I would go to my second controller.

onTouchesBegan, I'd invalidate touchTimer and create a new one that would repeat every repearTime seconds, and lasts for y seconds. Set touchValid to NO, and set initialTouchTimeStamp to touch.timestamp.

onTouchesEnded, I'd invalidate touchTimer, and check if my initialTouchTimeStamp var is longPressTimeRequired or more seconds prior to [touchTimer fireDate] - repeatTime , if it is, and the touchValid is true, then I would go to my second controller.

there are many common elements in here, and it's probably not the most elegant way of doing things, but it should work. Hope this helps.

user352891
  • 1,181
  • 1
  • 8
  • 14
  • Thanks for the response. I was using a similar method like this before but I got stuck so I tried using the LongPressGesture. The problem I was getting was once the fingers were released (after 3 seconds), only the final orientation mattered. I will try your suggestion on the boolean so that for the entire press, the phone must be in the correct orientation. Thanks – user2480376 Jul 31 '13 at 20:18
  • Okay I got a little confused. In the .h file, I added NSTimer *touchTimer, NSTimeInterval initialTouchTimeStamp, int longPressTimeRequired, and - (BOOL) touchValid. In the .m, in viewDidLoad I set longPressTimeRequired to 3. I guess where I start to become confused is with the repeatTime. Am I trying to use initWithFireDate:interval:target:selector:userInfo:repeats:? – user2480376 Aug 01 '13 at 01:03
  • I'd probably go for something like: `self.touchTimer = [NSTimer timerWithTimeInterval:repeatTime target:self selector:@selector(mySelector:) userInfo:nil repeats:YES]; [touchTimer fire] [[NSRunLoop mainRunLoop] addTimer:self.touchTimer forMode:NSDefaultRunLoopMode];` We would want the fire before we add it to the run loop, because otherwise we have to wait for the repeatTime for the timer to fire for the first time. – user352891 Aug 01 '13 at 07:48
  • Perhaps you could help fill in the blanks of my code. In viewDidLoad, I set longPressedTimeRequired to 3 and repeatTime to 0.25. In touchesBegan, I have the code you add in your last comment. In the mySelector: (id)sender method, I have the accelerometer code I first posted with a few additions. Under NSLog(@"Correct Orient..."), I have self.touchValid = YES. Under NSLog(@"Incorrect..."), I have self.touchValid = NO and [touchTimer invalidate]. I then have - (BOOL) touchValid { if (touchValid == YES) { // code to present secondVC }}. I know I am still missing some things but I am stuck. – user2480376 Aug 01 '13 at 14:21
  • To make the timer simpler, I posted another question that does not deal with touches. Please take a look at this (http://stackoverflow.com/questions/17999375/accelerometer-must-be-in-a-specific-orientation-for-a-given-time-interval-before) and perhaps if I get this to work, then I could incorporate the touches later. Thanks. – user2480376 Aug 01 '13 at 16:20
0

I hope the code is verbose enough for you to read, it's pretty self explanatory - I've stuck with your accelerometer code, and used the same variable names.

#import "ViewController.h"
#import <CoreMotion/CoreMotion.h>

@interface ViewController () {

    NSOperationQueue    *motionQueue;
    NSTimer             *touchTimer;
    NSTimeInterval      initialTimeStamp;
    BOOL                touchValid;

    float                timerPollInSeconds;
    float                longPressTimeRequired;
}

@property (strong, nonatomic)            NSTimer             *touchTimer;
@property (assign, nonatomic)            NSTimeInterval      initialTimeStamp;
@property (assign, nonatomic)            BOOL                touchValid;
@property (assign, nonatomic)            float               timerPollInSeconds;
@property (assign, nonatomic)            float               longPressTimeRequired;
@property (strong, nonatomic)            CMMotionManager     *motionManager;
@property (strong, nonatomic)            NSOperationQueue    *motionQueue;


@end

@implementation ViewController

@synthesize touchTimer = _touchTimer, initialTimeStamp, touchValid, motionQueue = _motionQueue;
@synthesize timerPollInSeconds, longPressTimeRequired, motionManager = _motionManager;

- (void)viewDidLoad
{
    self.timerPollInSeconds = 0.25f;
    self.longPressTimeRequired = 3.0f;
    self.touchTimer = nil;
    self.touchValid = NO;
    self.initialTimeStamp = NSTimeIntervalSince1970;
    self.motionManager = [[CMMotionManager alloc] init];
    self.motionQueue = [[NSOperationQueue alloc] init];
    [_motionQueue setName:@"MotionQueue"];
    [_motionQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];

    [super viewDidLoad];

    self.view.multipleTouchEnabled = NO;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Operations

-(void) startLongPressMonitorWithTimeStamp:(NSTimeInterval) timeStamp {
    NSLog(@"Starting monitoring - %g", timeStamp);
    if( self.touchTimer ) {
        if( [_touchTimer isValid] ) {
            [_touchTimer invalidate];
        }
    }

    self.touchTimer = [NSTimer timerWithTimeInterval:self.timerPollInSeconds target:self selector:@selector(timerPolled:) userInfo:nil repeats:YES];

    if( [_motionManager isAccelerometerAvailable] ) {
        NSLog(@"Accelerometer Available");
        if( ![_motionManager isAccelerometerActive] ) {
            NSLog(@"Starting Accelerometer");
            [_motionManager startAccelerometerUpdatesToQueue:self.motionQueue withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {

                if (ABS(accelerometerData.acceleration.x) < 0.3 && ABS(accelerometerData.acceleration.y) < 0.30 && ABS(accelerometerData.acceleration.z) > 0.70) // Phone is flat and screen faces up
                {
                    dispatch_sync(dispatch_get_main_queue(), ^{
                        self.touchValid = YES;
                    });
                }
                else
                {
                    dispatch_sync(dispatch_get_main_queue(), ^{
                        self.touchValid = NO;
                        [self stopLongPressMonitoring:YES];
                    });
                };
            }];
        }
        else {
            NSLog(@"Accelerometer already active");
        }
    }
    else {
        NSLog(@"Accelerometer not available");
    }

    self.initialTimeStamp = timeStamp;

    self.touchValid = YES;
    [_touchTimer fire];

    [[NSRunLoop mainRunLoop] addTimer:self.touchTimer forMode:NSRunLoopCommonModes];
}



-(void) stopLongPressMonitoring:(BOOL) touchSuccessful {
    [_motionManager stopAccelerometerUpdates];
    [_touchTimer invalidate];
    self.touchValid = NO;

    if( touchSuccessful ) {
        NSLog(@"Yes");
    }
    else {
         NSLog(@"No");
    }
}

#pragma mark - User Interaction
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    //We're using the current times, interval since the touches timestamp refers to system boot up
    // it is more than feasible to use this boot up time, but for simplicity, I'm just using this
    NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate];
    [self startLongPressMonitorWithTimeStamp:timestamp];
}

-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if( self.touchValid && [NSDate timeIntervalSinceReferenceDate] - self.initialTimeStamp == self.longPressTimeRequired ) {
        [self stopLongPressMonitoring:YES];
    }
    else {
        [self stopLongPressMonitoring:NO];
    }
}



#pragma mark - Timer Call back
-(void) timerPolled:(NSTimer *) timer {
    NSTimeInterval firedTimeStamp = [NSDate timeIntervalSinceReferenceDate];
    NSLog(@"Timer polled - %g", firedTimeStamp);
    if( self.touchValid ) {
        NSLog(@"Time elapsed: %d", (int)(firedTimeStamp - self.initialTimeStamp));

        if( firedTimeStamp - self.initialTimeStamp >= self.longPressTimeRequired ) {
            NSLog(@"Required time has elapsed");
            [self stopLongPressMonitoring:YES];
        }
    }
    else {
        NSLog(@"Touch invalidated");
        [self stopLongPressMonitoring:NO];
    }
}


@end
user352891
  • 1,181
  • 1
  • 8
  • 14
  • One last question. I added the lines of code to switch to the secondVC. Is there any reason why the touch methods are still active? – user2480376 Aug 01 '13 at 18:28
  • The onTouchesBegan, and onTouchesEnded shouldn't be active, unless the view is still visible - that or the touches are being passed up the responder chain (I don't think that can happen without you explicitly telling it to). The appropriate place to push the secondVC is in stopLongPressMonitoring:, where the NSLog(@"Yes") is – user352891 Aug 01 '13 at 21:08
  • I tried pushing the secondVC here and what happens is when I am not in the correct accelerometer orientation and release my finger, the secondVC is presented (not what I am looking for). When I called the secondVC under NSLog(@"Required time has elapsed"), the next VC gets called at the correct time but the touches are still active in the next view. Both placements continue the methods of the firstVC (weird) and I get the error: Warning: Attempt to present on whose view is not in the window hierarchy! – user2480376 Aug 02 '13 at 13:26
  • Hi, Apologies, you could safely remove the parameter from stopLongPressMonitoring, and as you say perform the push in the timer. There's also a bug with one of the stopLongPressMonitoring calls (sends YES rather than NO). I would push the secondVC after the stopLongPressMonitoring is called just to be safe (it invalidates the timer). I've just tried pushing a view controller on top modally, and on the nav stack, and I get no touches - could you provide the block where you're pushing the view controller? – user352891 Aug 03 '13 at 01:30
  • Here it is in timerPolled: if( firedTimeStamp - self.initialTimeStamp >= self.longPressTimeRequired ) { NSLog(@"Required time has elapsed"); [self stopLongPressMonitoring:YES]; SecondViewController *svc = [self.storyboard instantiateViewControllerWithIdentifier:@"SecondViewController"]; [self presentViewController:svc animated:YES completion:nil]; – user2480376 Aug 05 '13 at 13:53