UIScrollView with images slideshow and timers - iphone

i'm trying to implement a slideshow of images with previews horizontaly and vertically, and i want to be able to do some stuff when the user stop scrolling and after 5 seconds do this stuff. I use 2 timers to handle this but i need some synchronization or maybe i'm in the wrong way! Why i use 2 timers : because i want to handle scrolling in the main view (-(void)touchesBegan and -(void)touchesEnded)and scrolling on the scrollView( UIScrollView delegate). my code is below and i hope someone can help :). Thanks.
// This is the slideShow view controller
// It display all the images with preview (right, left, up and down)
#interface ImageSlideShowViewController : UIViewController<UIScrollViewDelegate>
{
UIScrollView *scrollViewForSlideShow;
// Timers
NSTimer *autoscrollTimer;
NSTimer *mainViewTimer;
}
#end
#implementation ImageSlideShowViewController
#pragma mark Touch handling
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event{
....
// Invalidate all timers
[autoscrollTimer invalidate];
autoscrollTimer = nil;
[mainViewTimer invalidate];
mainViewTimer = nil;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
....
assert(mainViewTimer == nil);
mainViewTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(allViewTimerFireMethod:)
userInfo:nil
repeats:NO];
// prev image horizontal
...
// next image horizontal
....
// prev image vertical
...
// next image vertical
....
}
#pragma mark -
#pragma mark UIScrollView delegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
// Invalidate all timers
[autoscrollTimer invalidate];
autoscrollTimer = nil;
[mainViewTimer invalidate];
mainViewTimer = nil;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
//Doing some management stuff here
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (!scrollView.dragging)
{
//Doing some management stuff here
assert(autoscrollTimer == nil);
autoscrollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(timerFireMethod:)
userInfo:nil
repeats:NO];
}
}
#pragma mark -
#pragma mark Timers methods
- (void)timerFireMethod:(NSTimer*)theTimer
{
autoscrollTimer = nil;
// Call a method that dispaly the image
}
- (void)allViewTimerFireMethod:(NSTimer *)theTimer
{
mainViewTimer = nil;
// Call a method that dispaly the image (same as the method called above)
}
#end

I think that you could get rid of the two timers and define an intermediate function, say enableTimerFor:, that you could call from -scrollViewDidEndDecelerating: and -touchesEnded:withEvent: instead of creating there the 2 timers.
enableTimerFor: would basically create the timer and schedule it:
-(void)enableTimerFor:(NSUInteger)callerId {
viewTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(timerFired:)
userInfo:nil
repeats:NO];
}
-(void)timerFired {
// do whatever you need to
}
And it would be called like this:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
....
[self enableTimerFor:kTouchesEnded];
....
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (!scrollView.dragging) {
//Doing some management stuff here
[self enableTimerFor:kScrollEnded];
}
....
}
Like that, you would have no problem in syncing. Notice also that I thought of enableTimerFor: as having a parameter that allows you to identify whether it was called from one function or the other, but you can decide easily to get rid of it in case you don't need to distinguish the two cases.
I am not sure that this suggestion could work well for you, since I don't know all the details of your solution, but still I think this is the most direct suggestion given the information I have.

Related

NSTimer method calls multiple times

I am developing one application,in that i have one requirement.onTyping method calls on textfield text change.when i enter first letter on textfield ontying method get called and timer starts &pause method called after 10 sec,pause method get called on continue on typing.But i want call pause method on last letter typing.how can i achieve this one
-(IBAction)onTyping:(id)sender
{
// [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:#selector(pause) userInfo:nil repeats:NO];
if(timer==nil)
{
timer=[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:#selector(pause) userInfo:nil repeats:NO];
}
[self fadeIn];
}
-(void)pause
{
[self fadeOut];
}
- (void)fadeIn
{
NSlog(#"fadeIn");
}
- (void)fadeOut
{
NSlog(#"fadeOut");
}
call that method textFieldShouldEndEditing: method of UITextFieldDelegate like bellow...
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
[self pause];
return YES;
}

Displaying the image for particular time interval

I have view controller with UIImageView . In view did load i want to set image on image view for particular time interval.After that image view should be cleared and application should switch to next screen. I have tried following code which is not working:
welcomeImage.image=[UIImage imageNamed:#"abc.png"];
sleep(5);
homeScreenController *controller=[[homeScreenController alloc]initWithNibName:#"homeScreenController" bundle:nil];
controller.modalTransitionStyle=UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:controller animated:YES];
In this case it is sleeping before view did load and it is not going to the next screen also. So what is wrong with the code?
Use the below code/..
- (void)viewDidLoad
{
[super viewDidLoad];
[self performSelector:#selector(goToNextView) withObject:nil afterDelay:5.0];
}
- (void)goToNextView
{
homeScreenController *controller=[[homeScreenController alloc]initWithNibName:#"homeScreenController" bundle:nil];
controller.modalTransitionStyle=UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:controller animated:YES];
}
The easiest solution is to fire a event after a time. I would recommend using performSelector.
- (void) hide {
//For example.
[self dismissModalViewController];
}
- (void) viewDidLoad {
//Setup my image.
[self performSelector:#selector(hide) withObject:nil afterDelay:3];
}
you can use NSTimer
something like this
- (void) viewDidLoad {
.......
NSTimer *timer = [[NSTimer scheduledTimerWithTimeInterval:(2.5)
target:self selector:#selector(hideImage)
userInfo:nil repeats:NO] retain];
....
}
- (void)hideImage
{
yourImage.hidden = YES;
}
Also, you made a sleep(5) inside the Main Thread, that's bad.
try something like
[self performSelectorOnBackground:#selector(hide) withObject:nil]
and do the thing you want (and the sleep(5)) inside the method -(void)hide like
-(void) hide {
sleep(5)
// ...
}
Good luck ;)

Help changing NSTimer for textLabel. Code included

want to change my NSTimer code to to use applicationSignificantTimeChange
This is what I have right now.
Basically I want to change the timer, instead of changing every 5 seconds for example I want these textLabels to change at 12am every night anyone help me out?
//h file
NSTimer *timer;
IBOutlet UILabel *textLabel;
//m file
- (void)onTimer {
static int i = 0;
if ( i == 0 ) {
textLabel.text = #"iphone app";
}
else if ( i == 1 ) {
textLabel.text = #" Great App!";
}
else if ( i == 2 ) {
textLabel.text = #" WOW!";
}
else {
textLabel.text = #" great application again!!";
i = -1;
}
i++;
}
timer =[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(onTimer) userInfo:nil repeats:YES];
There are two things you can do to respond to a applicationSignificantTimeChange:
(1) Just implement applicationSignificantTimeChange: in your app delegate if you have a connection to the view controller that should be updated.
- (void)applicationSignificantTimeChange:(UIApplication *)application {
yourViewController.textLabel.text = #"random text";
}
(2) Subscribe to the UIApplicationSignificantTimeChangeNotification notification in the view controller that should get the update. You could perhaps put that code in viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(onSignificantTimeChange:)
name:UIApplicationSignificantTimeChangeNotification
object:nil];
}
- (void)onSignificantTimeChange:(NSNotification *)notification {
self.textLabel.text = #"random text";
}
You also need to call
[[NSNotificationCenter defaultCenter] removeObserver:self];
somewhere when your view controller is not longer needed.
the selector for the timer should be:
-(void) onTimer:(NSTimer *) timer
{
// yourcode
}
and called with:
[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(onTimer:) userInfo:nil repeats:YES];
you need the "onTimer*:*"
[EDIT]
for calling a function that will go off at 12:am, for low-performance (i.e. not 12:00:00:0000) you could do the following for like an app that sits in the background:
NSTimeInterval minute = 60.0f
[NSTimer scheduledTimerWithTimeInterval:minute target:self selector:#selector(onTimer:) userInfo:nil repeats:YES]
-(void) onTimer:(NSTimer *) timer
{
NSDate *midnight = [NSDate dateWithNaturalLanguageString:#"today at 12:00 am"]; // unsure of this line
if ([midnight timeIntervalSinceDate:[NSDate date]] < 30.0f)
// set off alarm.
}
Seems pretty straightforward to me -- just create an NSDate for midnight, calculate the time interval of 24 hours in seconds, and create the NSTimer with
- (id)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

viewDidUnload - stopping a method when the view is changed

I'm looking to stop an audio file from playing when the view is changed.
I'm using a tabController and I would like the audio that's playing to stop when the user moves to a different view. I'm not sure where and how I would do this. in the viewDidUnload perhaps?
here's the method I'm using to play the audio file:
-(void)startPlaying {
[NSTimer scheduledTimerWithTimeInterval:15 target:self selector:#selector(startPlaying) userInfo:nil repeats:NO];
NSString *audioSoundPath = [[ NSBundle mainBundle] pathForResource:#"audio_file" ofType:#"caf"];
CFURLRef audioURL = (CFURLRef) [NSURL fileURLWithPath:audioSoundPath];
AudioServicesCreateSystemSoundID(audioURL, &audioID);
AudioServicesPlaySystemSound(audioID);
}
thanks for any help
Something like this in your view controller (untested):
- (void)viewDidLoad
{
[super viewDidLoad];
// Load sample
NSString *audioSoundPath = [[NSBundle mainBundle] pathForResource:#"audio_file"
ofType:#"caf"];
CFURLRef audioURL = (CFURLRef)[NSURL fileURLWithPath:audioSoundPath];
AudioServicesCreateSystemSoundID(audioURL, &audioID)
}
- (void)viewDidUnload
{
// Dispose sample when view is unloaded
AudioServicesDisposeSystemSoundID(audioID);
[super viewDidUnload];
}
// Lets play when view is visible (could also be changed to viewWillAppear:)
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self startPlaying];
}
// Stop audio when view is gone (could also be changed to viewDidDisappear:)
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if([self.audioTimer isValid]) {
[self.audioTimer invalidate];
}
self.timer = nil;
}
// Start playing sound and reschedule in 15 seconds.
-(void)startPlaying
{
self.audioTimer = [NSTimer scheduledTimerWithTimeInterval:15 target:self
selector:#selector(startPlaying)
userInfo:nil
repeats:NO];
AudioServicesPlaySystemSound(audioID);
}
Missing:
Error checking
Property or ivar for audioTimer.
Could also keep the same timer, with repeats YES.
Freeing resources in dealloc.
Testing

Scrolling with two fingers with a UIScrollView

I have an app where my main view accepts both touchesBegan and touchesMoved, and therefore takes in single finger touches, and drags. I want to implement a UIScrollView, and I have it working, but it overrides the drags, and therefore my contentView never receives them. I'd like to implement a UIScrollview, where a two finger drag indicates a scroll, and a one finger drag event gets passed to my content view, so it performs normally. Do I need create my own subclass of UIScrollView?
Here's my code from my appDelegate where I implement the UIScrollView.
#implementation MusicGridAppDelegate
#synthesize window;
#synthesize viewController;
#synthesize scrollView;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Override point for customization after app launch
//[application setStatusBarHidden:YES animated:NO];
//[window addSubview:viewController.view];
scrollView.contentSize = CGSizeMake(720, 480);
scrollView.showsHorizontalScrollIndicator = YES;
scrollView.showsVerticalScrollIndicator = YES;
scrollView.delegate = self;
[scrollView addSubview:viewController.view];
[window makeKeyAndVisible];
}
- (void)dealloc {
[viewController release];
[scrollView release];
[window release];
[super dealloc];
}
In SDK 3.2 the touch handling for UIScrollView is handled using Gesture Recognizers.
If you want to do two-finger panning instead of the default one-finger panning, you can use the following code:
for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
}
}
For iOS 5+, setting this property has the same effect as the answer by Mike Laurence:
self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;
One finger dragging is ignored by panGestureRecognizer and so the one finger drag event gets passed to the content view.
In iOS 3.2+ you can now achieve two-finger scrolling quite easily. Just add a pan gesture recognizer to the scroll view and set its maximumNumberOfTouches to 1. It will claim all single-finger scrolls, but allow 2+ finger scrolls to pass up the chain to the scroll view's built-in pan gesture recognizer (and thus allow normal scrolling behavior).
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
You need to subclass UIScrollView (of course!). Then you need to:
make single-finger events to go to your content view (easy), and
make two-finger events scroll the scroll view (may be easy, may be hard, may be impossible).
Patrick's suggestion is generally fine: let your UIScrollView subclass know about your content view, then in touch event handlers check the number of fingers and forward the event accordingly. Just be sure that (1) the events you send to content view don't bubble back to UIScrollView through the responder chain (i.e. make sure to handle them all), (2) respect the usual flow of touch events (i.e. touchesBegan, than some number of {touchesBegan, touchesMoved, touchesEnded}, finished with touchesEnded or touchesCancelled), especially when dealing with UIScrollView. #2 can be tricky.
If you decide the event is for UIScrollView, another trick is to make UIScrollView believe your two-finger gesture is actually a one-finger gesture (because UIScrollView cannot be scrolled with two fingers). Try passing only the data for one finger to super (by filtering the (NSSet *)touches argument — note that it only contains the changed touches — and ignoring events for the wrong finger altogether).
If that does not work, you are in trouble. Theoretically you can try to create artificial touches to feed to UIScrollView by creating a class that looks similar to UITouch. Underlying C code does not check types, so maybe casting (YourTouch *) into (UITouch *) will work, and you will be able to trick UIScrollView into handling the touches that did not really happen.
You probably want to read my article on advanced UIScrollView tricks (and see some totally unrelated UIScrollView sample code there).
Of course, if you can't get it to work, there's always an option of either controlling UIScrollView's movement manually, or use an entirely custom-written scroll view. There's TTScrollView class in Three20 library; it does not feel good to the user, but does feel good to programmer.
This answers are a mess since you can only find the correct answer by reading all the other answers and the comments (closest answer got the question backwards). The accepted answer is too vague to be useful, and suggests a different method.
Synthesizing, this works
// makes it so that only two finger scrolls go
for (id gestureRecognizer in self.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *panGR = gestureRecognizer;
panGR.minimumNumberOfTouches = 2;
panGR.maximumNumberOfTouches = 2;
}
}
This requires two fingers for a scroll. I've done this in a subclass, but if not, just replace self.gestureRecognizers with myScrollView.gestureRecognizers and you're good to go.
The only thing that I added is using id to avoid an ugly cast :)
This works but can get quite messy if you want your UIScrollView to do zoom too... the gestures don't work correctly, since pinch-to-zoom and scroll fight it out. I'll update this if I find a suitable answer.
we managed to implement similar functionality in our iPhone drawing app by subclassing UIScrollView and filtering events depending on number of touches in simple and rude way:
//OCRScroller.h
#interface OCRUIScrollView: UIScrollView
{
double pass2scroller;
}
#end
//OCRScroller.mm
#implementation OCRUIScrollView
- (id)initWithFrame:(CGRect)aRect {
pass2scroller = 0;
UIScrollView* newv = [super initWithFrame:aRect];
return newv;
}
- (void)setupPassOnEvent:(UIEvent *)event {
int touch_cnt = [[event allTouches] count];
if(touch_cnt<=1){
pass2scroller = 0;
}else{
double timems = double(CACurrentMediaTime()*1000);
pass2scroller = timems+200;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self setupPassOnEvent:event];
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
pass2scroller = 0;
[super touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
return YES;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
double timems = double(CACurrentMediaTime()*1000);
if (pass2scroller == 0 || timems> pass2scroller){
return NO;
}
return YES;
}
#end
ScrollView setuped as follows:
scroll_view = [[OCRUIScrollView alloc] initWithFrame:rect];
scroll_view.contentSize = img_size;
scroll_view.contentOffset = CGPointMake(0,0);
scroll_view.canCancelContentTouches = YES;
scroll_view.delaysContentTouches = NO;
scroll_view.scrollEnabled = YES;
scroll_view.bounces = NO;
scroll_view.bouncesZoom = YES;
scroll_view.maximumZoomScale = 10.0f;
scroll_view.minimumZoomScale = 0.1f;
scroll_view.delegate = self;
self.view = scroll_view;
simple tap does nothing (you can handle it in the way you need), tap with two fingers scrolls/zooms view as expected. no GestureRecognizer is used, so works from iOS 3.1
I've got a further improvement to the code above. The problem was, that even after we set setCanCancelContentTouches:NO We have the problem, that a zoom gesture will interrupt with the content. It won't cancel the content touch but allow zooming in the meantime. TO prevent this i lock the zooming by setting the minimumZoomScale and maximumZoomScale to the same values everytime, the timer fires.
A quite strange behavior is that when a one finger event gets canceled by a two finger gesture within the allowed time period, the timer will be delayed. It gets fired after the touchCanceled Event gets called. So we have the problem, that we try to lock the zooming although the event is already canceled and therefore disable zooming for the next event.
To handle this behavior the timer callback method checks against if touchesCanceled was called before.
#implementation JWTwoFingerScrollView
#pragma mark -
#pragma mark Event Passing
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
}
timerWasDelayed = NO;
}
}
return self;
}
-(void)lockZoomScale {
zoomScale[0] = self.minimumZoomScale;
zoomScale[1] = self.maximumZoomScale;
[self setMinimumZoomScale:self.zoomScale];
[self setMaximumZoomScale:self.zoomScale];
NSLog(#"locked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
-(void)unlockZoomScale {
if (zoomScale[0] != -1 && zoomScale[1] != -1) {
[self setMinimumZoomScale:zoomScale[0]];
[self setMaximumZoomScale:zoomScale[1]];
zoomScale[0] = -1.0;
zoomScale[1] = -1.0;
NSLog(#"unlocked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"began %i",[event allTouches].count);
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
//if one finger touch gets canceled by two finger touch, this timer gets delayed
// so we can! use this method to disable zooming, because it doesnt get called when two finger touch events are wanted; otherwise we would disable zooming while zooming
-(void)firstTouchTimerFired:(NSTimer*)timer {
NSLog(#"fired");
[self setCanCancelContentTouches:NO];
//if already locked: unlock
//this happens because two finger gesture delays timer until touch event finishes.. then we dont want to lock!
if (timerWasDelayed) {
[self unlockZoomScale];
}
else {
[self lockZoomScale];
}
timerWasDelayed = NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// NSLog(#"moved %i",[event allTouches].count);
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"ended %i",[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
[self unlockZoomScale];
}
//[self setCanCancelContentTouches:NO];
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"canceled %i",[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
[self unlockZoomScale];
timerWasDelayed = YES;
}
#end
Bad news: iPhone SDK 3.0 and up, don't pass touches to -touchesBegan: and -touchesEnded: **UIScrollview**subclass methods anymore. You can use the touchesShouldBegin and touchesShouldCancelInContentView methods that is not the same.
If you really want to get this touches, have one hack that allow this.
In your subclass of UIScrollView override the hitTest method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
This will pass to you subclass this touches, however you can't cancel the touches to UIScrollView super class.
What I do is have my view controller set up the scroll view:
[scrollView setCanCancelContentTouches:NO];
[scrollView setDelaysContentTouches:NO];
And in my child view I have a timer because two-finger touches usually start out as one finger followed quickly by two fingers.:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Hand tool or two or more touches means a pan or zoom gesture.
if ((selectedTool == kHandToolIndex) || (event.allTouches.count > 1)) {
[[self parentScrollView] setCanCancelContentTouches:YES];
[firstTouchTimer invalidate];
firstTouchTimer = nil;
return;
}
// Use a timer to delay first touch because two-finger touches usually start with one touch followed by a second touch.
[[self parentScrollView] setCanCancelContentTouches:NO];
anchorPoint = [[touches anyObject] locationInView:self];
firstTouchTimer = [NSTimer scheduledTimerWithTimeInterval:kFirstTouchTimeInterval target:self selector:#selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
firstTouchTimeStamp = event.timestamp;
}
If a second touchesBegan: event comes in with more than one finger, the scroll view is allowed to cancel touches. So if the user pans using two fingers, this view would get a touchesCanceled: message.
This seems to be the best resource for this question on the internet. Another close solution can be found here.
I have solved this issue in a very satisfactory manner in a different way, essentially by supplanting my own gesture recognizer into the equation. I strongly recommend that anyone who is trying to achieve the effect requested by the original poster consider this alternative over aggressive subclassing of UIScrollView.
The following process will provide:
A UIScrollView containing your custom view
Zoom and Pan with two fingers (via UIPinchGestureRecognizer)
Your view's event processing for all other touches
First, let's assume you have a view controller and its view. In IB, make the view a subview of a scrollView and adjust the resize rules of your view so that it does not resize. In the attributes of the scrollview, turn on anything that says "bounce" and turn off "delaysContentTouches". Also you must set the zoom min and max to other than the default of 1.0 for, as Apple's docs say, this is required for zooming to work.
Create a custom subclass of UIScrollView, and make this scrollview that custom subclass. Add an outlet to your view controller for the scrollview and connect them up. You're now totally configured.
You will need to add the following code to the UIScrollView subclass so that it transparently passes touch events (I suspect this could be done more elegantly, perhaps even bypassing the subclass altogether):
#pragma mark -
#pragma mark Event Passing
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return NO;
}
Add this code to your view controller:
- (void)setupGestures {
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinchGesture:)];
[self.view addGestureRecognizer:pinchGesture];
[pinchGesture release];
}
- (IBAction)handlePinchGesture:(UIPinchGestureRecognizer *)sender {
if ( sender.state == UIGestureRecognizerStateBegan ) {
//Hold values
previousLocation = [sender locationInView:self.view];
previousOffset = self.scrollView.contentOffset;
previousScale = self.scrollView.zoomScale;
} else if ( sender.state == UIGestureRecognizerStateChanged ) {
//Zoom
[self.scrollView setZoomScale:previousScale*sender.scale animated:NO];
//Move
location = [sender locationInView:self.view];
CGPoint offset = CGPointMake(previousOffset.x+(previousLocation.x-location.x), previousOffset.y+(previousLocation.y-location.y));
[self.scrollView setContentOffset:offset animated:NO];
} else {
if ( previousScale*sender.scale < 1.15 && previousScale*sender.scale > .85 )
[self.scrollView setZoomScale:1.0 animated:YES];
}
}
Please note that in this method there are references to a number of properties you must define in your view controller's class files:
CGFloat previousScale;
CGPoint previousOffset;
CGPoint previousLocation;
CGPoint location;
Ok that's it!
Unfortunately I could not get the scrollView to show its scrollers during the gesture. I tried all of these strategies:
//Scroll indicators
self.scrollView.showsVerticalScrollIndicator = YES;
self.scrollView.showsVerticalScrollIndicator = YES;
[self.scrollView flashScrollIndicators];
[self.scrollView setNeedsDisplay];
One thing I really enjoyed is if you'll look at the last line you'll note that it grabs any final zooming that's around 100% and just rounds it to that. You can adjust your tolerance level; I had seen this in Pages' zoom behavior and thought it would be a nice touch.
I put this in the viewDidLoad method and this accomplishes the scroll view handling the two touch pan behavior and another pan gesture handler handling the one touch pan behavior -->
scrollView.panGestureRecognizer.minimumNumberOfTouches = 2
let panGR = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGR.minimumNumberOfTouches = 1
panGR.maximumNumberOfTouches = 1
scrollView.gestureRecognizers?.append(panGR)
and in the handlePan method which is a function attached to the ViewController there is simply a print statement to verify that the method is being entered -->
#IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)")
}
HTH
Check out my solution:
#import “JWTwoFingerScrollView.h”
#implementation JWTwoFingerScrollView
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
for (UIGestureRecognizer* r in self.gestureRecognizers) {
NSLog(#“%#”,[r class]);
if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
[((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
[((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
}
}
}
return self;
}
-(void)firstTouchTimerFired:(NSTimer*)timer {
[self setCanCancelContentTouches:NO];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setCanCancelContentTouches:YES];
if ([event allTouches].count == 1){
touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(firstTouchTimerFired:) userInfo: nil repeats:NO];
[touchesBeganTimer retain];
[touchFilter touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[touchFilter touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#“ended %i”,[event allTouches].count);
[touchFilter touchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#“canceled %i”,[event allTouches].count);
[touchFilter touchesCancelled:touches withEvent:event];
}
#end
It does not delays the first touch and does not stop when the user touches with two fingers after using one. Still it allows to cancel a just started one touch event using a timer.
Yes, you'll need to subclass UIScrollView and override its -touchesBegan: and -touchesEnded: methods to pass touches "up". This will probably also involve the subclass having a UIView member variable so that it knows what it's meant to pass the touches up to.
Kenshi's answer in Swift 4
for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
if (gestureRecognizer is UIPanGestureRecognizer) {
let panGR = gestureRecognizer as? UIPanGestureRecognizer
panGR?.minimumNumberOfTouches = 2
}
}