Understanding UIScrollView’s Event Handling
=====================================
As a mobile app developer, you’ve likely encountered the UIScrollView control in your projects. This powerful view allows users to scroll through content that exceeds the device’s screen size. However, getting UIScrollView to respond to events, such as touch and pinch/zoom gestures, can be challenging. In this article, we’ll delve into the world of event handling for UIScrollView, exploring how to pass events from a subview (like an UIImageView) to the parent view.
The Problem with Subviews
When you add a subview to another view, like in our example:
mainView -> overlaidImage
The subview receives all the touch events. If we try to pass these events to the parent view using delegates or blocks, it’s not straightforward. The issue lies in how UIScrollView handles its own events.
How UIScrollView Handles Events
To understand what happens when you add a subview to UIScrollView, let’s take a closer look at its event handling process:
- When a user touches the screen within the bounds of the
UIScrollView, it receives the touch event. - The scroll view then checks if the touch occurred outside its own bounds. If so, it passes the touch event to its child views (subviews).
- However, when a subview has an overlay or other view on top of it that is not a direct child of the
UIScrollView, things become more complicated.
Why the Overlay Image Doesn’t Work
In our example, we have an overlaidImage on top of an imageView and both are children of the mainView. When we add an event handler to the overlay image and then pass events to the imageView, it works as expected for most touch events. However, when it comes to pinch/zoom gestures within the overlay image, things don’t work correctly.
This is because the overlay image does not capture all the touch events from the underlying views (like our overlaidImage). When you perform a pinch gesture on the overlay image, it doesn’t report this event to its parent view.
Solution: Use the touchesShouldCancel Method
To solve the problem of passing events from an overlay image to a UIScrollView, we can use the touchesShouldCancel method. This method is called when a touch event occurs on the UIScrollView. We can override this method in the overlaidImage subclass and return NO if the touch should not be canceled.
Here’s how you might do it:
// In your overlaid image class
- (BOOL)touchesShouldCancel:(NSSet *)touches withEvent:(UIEvent *)event {
// If you want to pass events from your overlaidImage to the imageView,
// return NO. This will allow the touch event to be passed on to the imageView.
return NO;
}
// Then, set a block or delegate in your view controller
- (void)viewDidLoad {
[super viewDidLoad];
self.overlaidImage.touchesShouldCancel = ^{
// If you want to cancel the touch event and prevent it from being passed
// to the scrollview, return YES.
return YES;
};
}
Pass Events to the UIScrollView
To pass events from your overlaidImage to the scrollView, you can use the following approach:
- Set a block in the
touchesShouldCancelmethod that cancels the touch event for theoverlaidImage. - In the
touchesShouldCancelmethod, set the delegate of thescrollViewto be an object that will handle all touches.
Here’s how you might do it:
// In your overlaid image class
- (BOOL)touchesShouldCancel:(NSSet *)touches withEvent:(UIEvent *)event {
// Set a block in touchesShouldCancel that cancels the touch event for this view.
return ^{
// Cancel the touch event to prevent it from being passed on to the imageView.
return YES;
};
}
// In your scrollview class
- (void)setNeedsUpdateInTarget:(id)aTarget {
[super setNeedsUpdateInTarget:aTarget];
// Set a new block in touchesShouldCancel that will handle all touch events
// for this view. This will pass any remaining touch events to the imageView.
_touchesShouldCancel = ^{
self.touchesShouldCancel = nil;
// Call this method on the target of the touchesShouldCancel.
[_target touchesShouldCancel:self.touchesWithEvent:nil];
// Allow the touch event to continue for other views in the view hierarchy.
return NO;
};
}
- (BOOL)touchesShouldCancel:(NSSet *)touches withEvent:(UIEvent *)event {
// Pass any remaining touch events on to the imageView using touchesWithEvent:
// Set a new block in touchesWithEvent that will handle all touch events
// for this view.
return ^{
self.touchesWithEvent = nil;
// Call this method on the target of the touchesWithEvent.
[_target touchesWithEvent:self.event];
// Allow the touch event to continue for other views in the view hierarchy.
return NO;
};
}
Example Code
To make things more concrete, let’s create a simple example that demonstrates how to pass events from an overlaidImage to a UIScrollView.
// overlaid_image.m
#import <UIKit/UIKit.h>
@interface OverlaidImage : UIView
@property (nonatomic, strong) id touchDelegate;
- (void)setTouchDelegate:(id)delegate;
- (BOOL)touchesShouldCancel:(NSSet *)touches withEvent:(UIEvent *)event;
@end
@implementation OverlaidImage
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Override the touchesShouldCancel method.
[self setNeedsUpdateInTarget:nil];
}
return self;
}
- (void)setTouchDelegate:(id)aTarget {
_touchDelegate = aTarget;
}
- (BOOL)touchesShouldCancel:(NSSet *)touches withEvent:(UIEvent *)event {
// If we want to cancel the touch event and pass it on to the scrollview,
// set this block.
return ^{
// Cancel the touch event for this view.
return YES;
};
}
@end
// scroller.m
#import <UIKit/UIKit.h>
@interface Scroller : UIScrollView
@property (nonatomic, strong) id touchDelegate;
- (void)setTouchDelegate:(id)aTarget;
@end
@implementation Scroller
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the touchesShouldCancel method for this view.
[self setNeedsUpdateInTarget:nil];
}
return self;
}
- (void)setTouchDelegate:(id)aTarget {
_touchDelegate = aTarget;
}
- (BOOL)touchesShouldCancel:(NSSet *)touches withEvent:(UIEvent *)event {
// Set the touchesWithEvent method for this view.
return ^{
// Call this method on the target of the touchesWithEvent.
[_target touchesWithEvent:self.event];
// Allow the touch event to continue for other views in the view hierarchy.
return NO;
};
}
@end
// main_view.m
#import <UIKit/UIKit.h>
@interface MainView : UIView
@property (nonatomic, strong) OverlaidImage *overlaidImage;
@property (nonatomic, strong) Scroller *scroller;
- (void)viewDidLoad {
[super viewDidLoad];
// Set the overlaid image.
self.overlaidImage = [[OverlaidImage alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.bounds.size.width, self.bounds.size.height)];
[self.view addSubview:self.overlaidImage];
// Create a new scroller view.
Scroller *scrollerView = [[Scroller alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.bounds.size.width, self.bounds.size.height)];
[self.view addSubview:scrollerView];
// Set the scroller as the touch delegate for this view.
self.overlaidImage.touchDelegate = self.scroller;
}
@end
// main_view_delegate.m
#import <UIKit/UIKit.h>
@interface MainView (delegate)
@property (nonatomic, strong) OverlaidImage *overlaidImage;
- (void)touchesShouldCancel:(NSSet *)touches withEvent:(UIEvent *)event {
// Handle all touch events for this view.
return YES;
}
@end
Conclusion
In conclusion, passing events from an overlaidImage to a UIScrollView requires careful consideration of how the event handling processes work. We can achieve our goal by overriding the touchesShouldCancel method in the overlaidImage subclass and using this method to cancel any touch events that should be passed on to other views in the view hierarchy.
By setting the touchesWithEvent: block for the scrollView, we allow any remaining touch events to be passed on to the imageView. This ensures that our UIScrollView handles all touch events correctly, even when there is an overlay image on top of it.
Last modified on 2025-01-01