I have written a subclass of UIViewController which creates a view programmatically, instead of loading it from a NIB file.
It has a simple loadView
method:
- (void)loadView
{
UIScrollView *mainScrollView =
[[UIScrollView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view = mainScrollView;
[mainScrollView release];
}
...then I do the bulk of my initialization in viewDidLoad
, as documented. It all works, and I can see the scroll view with my custom views in it.
I need a UIViewController to own the view because it's part of a UINavigationBar workflow. Since I have a controller object, I'd rather it do the controller stuff.
The problem, then, is my view controller does not seem to be in the responder chain. touchesBegan:withEvent:
is invoked if I define it in the root view or a subview, but not if it's in the view controller itself.
Apple event handling documentation glibly mentions the view controller should be in the responder chain. UIViewController documentation says nothing about extra steps needed beyond assigning the root view to the self.view
property, as I've done above. UIResponder documentation claims a UIView should figure out if it has a controller and pass the event to it. UIScrollView documentation says nothing at all.
I've also experimented with various settings of userInteractionEnabled:
for all views and subviews, with no luck.
What am I missing?
EricB, touches are sent down the responder chain only if they have not been handled. UIScrollView obviously handles all the touch events, so it does not send anything to it's nextResponder. Makes perfect sense to me.
What you actually want is to “filter” touch events before they are handled by the scrolling logic of UIScrollView. But note that the responder chain is a wrong tool for this job, exactly because it does not allow to intercept the events before they get handled.
Probably the best solution in your case is to subclass UIScrollView, override the touch methods (touchesBegan etc) and manually send the events to the delegate before calling [super touchesXxx]
.
I think the problem is that UIScrollView has a lot of bugs, and workarounds to the bugs, and bugs in the workarounds to the bugs :).
It's complicated by the fact that subclassing UIScrollView is usually a bad idea in the long run - you'll often have to subclass it for other reasons that cannot be solved any other way, so if you want to re-use your code, try not to subclass it until you absolutely have to.
Simplest solution I've found that works consistently is to make your content view a tiny UIView subclass - bearing in mind your content view itself is REQUIRED by UIScrollView to never change (e.g. if you enable zooming - Apple has some bugs up to at least iOS 6 that kick in when you change the content view of an existing scrollview), so it's a good idea to keep it trivial, and put your custom views inside it as subviews.
NB: This has always worked for me, on multiple shipped apps. If there's a problem, I haven't seen it yet. I have no idea why Apple doesn't implement this simple change themselves, unless there's a subtle problem with it that I haven't run into yet!
Usage:
/** Inside your UIViewController, wherever you set the root content view
of your UIScrollView, you have to also tell the custom class "I am the nextResponder!"
*/
-(void)viewDidLoad
{
self.scrollView.contentSize = self.viewEmbeddedInScrollview.frame.size;
self.viewEmbeddedInScrollview.nextResponderHeyAppleWhyDidYouStealThis = self;
}
Interface file + class file:
#import <UIKit/UIKit.h>
@interface UIViewThatNeverLosesItsNextResponder : UIView
@property(nonatomic,retain) UIResponder* nextResponderHeyAppleWhyDidYouStealThis;
@end
#import "UIViewThatNeverLosesItsNextResponder.h"
@implementation UIViewThatNeverLosesItsNextResponder
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
@synthesize nextResponderHeyAppleWhyDidYouStealThis;
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[nextResponderHeyAppleWhyDidYouStealThis touchesBegan:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[nextResponderHeyAppleWhyDidYouStealThis touchesCancelled:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[nextResponderHeyAppleWhyDidYouStealThis touchesEnded:touches withEvent:event];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[nextResponderHeyAppleWhyDidYouStealThis touchesMoved:touches withEvent:event];
}
@end
UIScrollView will delay content touches by default; have you looked at -delaysContentTouches to make sure the touches will pass through.