I have a UIScrollView with 2 pages, and I can scroll horizontally between them. However, on one of my pages, I have a UIDatePicker, and the scroll view is intercepting the vertical touch events so I can no longer manipulate the date picker (except by clicking or tapping). Is there some way to tell the ScrollView to send the vertical touch events to the date picker, but send the horizontal touch events to the scroll view to switch pages?
Actually, there is a much simpler implementation than what Bob suggested. This works perfectly for me. You will need to subclass your UIScrollview if you haven't already, and include this method:-
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result.superview isKindOfClass:[UIPickerView class]])
{
self.canCancelContentTouches = NO;
self.delaysContentTouches = NO;
}
else
{
self.canCancelContentTouches = YES; // (or restore bool from prev value if needed)
self.delaysContentTouches = YES; // (same as above)
}
return result;
}
The reason I use result.superview is that the view which gets the touches will actually be a UIPickerTable, which is a private API.
Cheers
I think there's two parts to this problem. The first is determining the user's intent, and the second is getting the correct control to respond to that intent.
Determining Intent
I think it's important to be clear about what the user intends. Imagine this scenario: The user starts touching the screen and moves his finger far to the left, but also up a little. The user probably intended to scroll the view, and didn't intend to change the date at all. It would be bad to both scroll the view and change the date, especially just as it moves off-screen. So to determine what the user intends I suggest the following algorithm:
When the user starts touching the screen, record the starting position. As the user's finger starts to move away from that position, the controls should not react at all. Once the touch moves past a certain threshold distance from the starting position, determine whether it moved more horizontally or vertically.
If it moved vertically, the user intends to change the date, so ignore the horizontal portion of the movement and only change the date.
If it moved more horizontally, the user intends to scroll the view, so ignore the vertical portion of the movement and only scroll the view.
Implementation
In order to implement this, you need to handle the events before the UIScrollView or date picker do. There's probably a few ways to do this, but one in particular comes to mind: Make a custom UIView called ScrollingDateMediatorView. Set the UIScrollView as a child of this view. Override the ScrollingDateMediatorView's hitTest:withEvent: and pointInside:withEvent: methods. These methods need to perform the same kind of hit testing that would normally occur, but if the result is the date picker, return self instead. This effectively hijacks any touch events that were destined for the date picker, allowing the ScrollingDateMediatorView to handle them first. Then you implement the algorithm described above in the various touches* methods. Specifically:
In the touchesBegan:withEvent method, save the starting position.
In touchesMoved:withEvent, if the user's intent isn't known yet, determine whether the touched has moved far enough away from the starting position. If it has, determine whether the user intends to scroll or change the date, and save that intent.
If the user's intent is already known and it's to change the date, send the date picker the touchedMoved:withEvent message, otherwise send the UIScrollView the touchesMoved:withEvent message.
You'll have to do some simliar work within touchesEnded:withEvent and touchesCancelled:withEvent to make sure the other views get the appropriate messages. Both of these methods should reset the saved values.
Once you have it properly propagating events, you'll probably have to try some user testing to tune the movement threshold.
Awesome help Sam! I used that to create a simple category that swizzles the method (because I was doing this in a UITableViewController and thus would have had to do some really messy stuff to subclass the scroll view).
#import <UIKit/UIKit.h>
#interface UIScrollView (withControls)
+ (void) swizzle;
#end
And the main code:
#import </usr/include/objc/objc-class.h>
#import "UIScrollView+withControls.h"
#define kUIViewBackgroundImageTag 6183746
static BOOL swizzled = NO;
#implementation UIScrollView (withControls)
+ (void)swizzleSelector:(SEL)orig ofClass:(Class)c withSelector:(SEL)new;
{
Method origMethod = class_getInstanceMethod(c, orig);
Method newMethod = class_getInstanceMethod(c, new);
if (class_addMethod(c, orig, method_getImplementation(newMethod),
method_getTypeEncoding(newMethod))) {
class_replaceMethod(c, new, method_getImplementation(origMethod),
method_getTypeEncoding(origMethod));
} else {
method_exchangeImplementations(origMethod, newMethod);
}
}
+ (void) swizzle {
#synchronized(self) {
if (!swizzled) {
[UIScrollView swizzleSelector:#selector(hitTest:withEvent:)
ofClass:[UIScrollView class]
withSelector:#selector(swizzledHitTest:withEvent:)];
swizzled = YES;
}
}
}
- (UIView*)swizzledHitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView* result = [self swizzledHitTest:point withEvent:event]; // actually calling the original hitTest method
if ([result.superview isKindOfClass:[UIPickerView class]]) {
self.canCancelContentTouches = NO;
self.delaysContentTouches = NO;
} else {
self.canCancelContentTouches = YES; // (or restore bool from prev value if needed)
self.delaysContentTouches = YES; // (same as above)
}
return result;
}
#end
Then, in my viewDidLoad method, I just called
[UIScrollView swizzle];
Related
I am attempting to let players drag objects in my game from one part of the screen to another. The problem is, the objects which are to be dragged have layers beneath them that need to receive touches, too. Normally I'd just swallow the touch, but as far as I can tell, that can only be done during ccTouchBegan. I can't tell if the user is attempting to drag an object until after ccTouchMoved is called, so I need a way to explicitly swallow (or otherwise prevent lower layers) from receiving the touch after I've determined that it is a touch I'm interested in (within ccTouchMoved).
I got almost the same problem, but I don't know if my solution would fit here. The main idea was that objects which should be dragged were children on the same CCNode hierarchy with beneath items. The solution consists in the fact that parent disabled children's touch events, then intercepts these events. In case some object was dragged parent sends all event to it, in the other case parent handles the event itself.
Let me try to show what I mean. Create protocol for items which can swallow touches ccTouchMoved:
#protocol SwallowingOnTouchMovedNode
{
-(BOOL) ccTouchMoved:(UITouch*)touch; // not full signature for simpleness (UIEvent should be also here)
}
Then create layer, which will manually handle the touches of its children:
#interface TouchHandler : CCLayer
{
NSMutableArray *draggableChildren_, nonDraggableChildren_, *claimedChildren_;
BOOL isDragging_;
}
#implementation TouchHandler
-(id) init
{
...
self.isTouchEnabled = YES;
draggableChildren_ = [[NSMutableArray alloc] init];
nonDraggableChildren_ = [[NSMutableArray alloc] init];
claimedChildren = [[NSMutableArray alloc] init];
...
}
Create two methods for TouchHandler for adding two types of children - the ones which can be dragged and the others. That methods will disable touches on children so the parent will manually handle them.
-(void) addChild:(CCNode*)child shouldBeDragged:(BOOL)shouldBeDragged
{
NSMutableArray *arrayToAddChild = shouldBeDragged ? draggableChildren_ : nonDraggableChildren_;
[arrayToAddChild addObject:child];
// note, that if the child has its own children, you will need to
// set isTouchEnabled on all of them, as adding all children to array recursively
if ([child respondsToSelector:#selector(setIsTouchEnabled:)]) ((CCLayer*)child).isTouchEnabled = NO;
[self addChild:child]; // maybe you need to call -addChild:zOrder:tag: here
}
Then override touch handles like that:
-(BOOL) ccTouchBegan:(UITouch*)touch
{
for (CCNode *child in draggableChildren)
{
if ([child ccTouchBegin:touch])
{
// this behavior looks like the one in CCTouchDispatcher -
// we claim children which return YES for touchBegin
[claimedChildren addObject:child];
}
}
}
-(void) ccTouchMoved:(UITouch*)touch
{
for (CCNode *child in claimedChildren)
{
if ([(id<SwallowingOnTouchMovedNode>)child ccTouchMoved:touch])
{
isDragging_ = YES;
}
}
// if no one swallowed touches
if (!isDragging_)
{
for (CCNode *child in nonDraggableChildren)
{
// we did not called ccTouchBegan earlier for these items,
// so for consistency we need to call it now
if ([child ccTouchBegin:touch])
{
[child ccTouchMoved:touch];
[claimedChildren addObject:child];
}
}
}
}
-(void) ccTouchEnded:(UITouch*)touch
{
isDragging_ = NO;
for (CCNode *child in claimedChildren)
{
[child ccTouchEnded];
}
}
Do not forget to implement -ccTouchCancelled. This code is pretty concrete, so you may need to make some changes, but I hope I have explained my idea. In general, the TouchHandler may not even be the CCLayer to work like this, just add it as targeted delegate for receiving touches.
There is another way, which seems to be more consistent and correct from OOP point of view, but I am not sure about it. The behavior in ccTouchBegin, ccTouchMoved and ccTouchEnded almost duplicates the one in the CCTouchDispatcher. You can subclass it and override some methods for receiving touch events and implement -(BOOL)ccTouchMoved, as I've done. Also, I don't know if we can replace default CCTouchDispatcher. Hope this will help!
In my app I have 2 transparent UIViewController layers.
the first layer contain UIView object that i am trying to recognize by touch with:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
method.
the problem is,
there is an transparent UIViewController above it.
I have tried to implement touch event on the SeconedStackedViewController and to create an instance of FirstStackedViewController and call the same method from there. the methods are called, but the hit test not.
Code:
FirstStackedViewController *fsvc = [[FirstStackedViewController alloc]init];
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
[fsvc hitTest:point withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
fsvc = [[FirstStackedViewController alloc]init];
[fsvc touchesEnded:touches withEvent:event];
}
How can I override this method to be called on the FirstStackedViewController?
if i will can simulate a touch on the FirstStackedViewController i think it will make the work
My first question is why you are pushing a transparent view controller. Might your needs be better served by layering a transparent view within the same controller?
Without looking at your requirements, it's hard to say for sure, but most likely this is more in accord with the usual separation of logic: you have a need for code that can receive input from one view and pass it to other logic. It has need to know about the internals of both, and therefore is a good candidate for a controller object with the knowledge needed to accomplish your task.
In this case, you would have no trouble at all - just present your transparent view, and pass touches to whatever action you wish.
But there are good reasons to do a VC with a transparent view. To access the VC beneath you, you can as another responder said access the app delegate. I'd keep the app delegate out of it, and just access self.navController.viewControllers. This array represents the stack of VCs. Then iterate downward - the one 'below' you is the one you want to relay the message to.
Most likely rather than using isMemberOfClass you'd want to define a protocol and check whether the VC conformsToProtocol.
for (int i=0; i < navBar.viewControllers.count ; i++) {
UIViewController *current = navBar.viewControllers[i];
if (current == self) {
UIViewController *targetVC = navBar.viewControllers[i+1]; // make sure you're not over bounds - you could do this in the loop def'n.
if ([targetVC class] conformsToProtocol("TransparentPassingActions")]) {
// pass message
}
}
}
You can also just use indexOfObject to get the current VC, I suppose, and then look one lower. That gets rid of the loop.
The pseudo-code in the previous answer confuses UIViews and UIViewControllers, by the way; perhaps this was clear to you. It is not possible to access view controllers by accessing views.
I'd start by getting a refernce to the rootViewController. I'm assuming your hierarchy is root ->1st view controller --> 2nd view controller, then by initing fsvc you are creating a new instance and you are not actually getting the reference from the view hierarchy.
I'd recommend iterating through rootviewcontroller subviews and find your instance for first viewcontroller, then call the method on that instance.
//PSEUDO-Code for class names, but iteration should solve the problem.
if(UIView *view in RootViewController.subviews){
if(view isMemberOfClass: [FirstStackedViewController class]]){
[(FirstStackedViewController *)view touchesEnded:touches withEvent:event];
}
}
//ToolUser was right about mixing up VCs and regular Views, you'd want to do this:
for(UIViewController *vc in self.navigationController.viewControllers){
if(view isMemberOfClass: [FirstStackedViewController class]]){
[(FirstStackedViewController *)vc touchesEnded:touches withEvent:event];
}
}
So I have a UIScrollView with multiple UIImageViews
I had to generate my own class of scroll view in order to tap into it's touchesEnded
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// [super touchesEnded:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
if ((int)(point.x / 100) < [[self items] count] ) {
NSLog(#" ENDED D File Selected is %# %d " , [[self items] objectAtIndex:(int)(point.x / 100) ] , (int)(point.x / 100) );
}
// [[self nextResponder] touchesEnded:touches withEvent:event];
}
items is an NSMutableArray within which I store the name of the file pointed to by each subview so basically subview[0] === item[0] etc...
subview[0] is the image View and item[0] is the fileName of that image
My question is this How can I now "advertise" alert the original caller that file xyz was selected ? As opposed to the NSLog line ?
Thanks in advance
What do you mean by "original caller"? -touchesEnded:withEvent: is called by the framework event handling subsystem.
What you might do (but don't! see below) is to add add some delegate methods of your own to the existing delegate (obviously there is already a UIScrollViewDelegate protocol and corresponding -[UIScrollView delegate] property) and call out to the delegate method you defined in lieu of your NSLog(). I discussed the refrain for doing this in a recent answer.
However, this is all moot because you are really approaching this backward and creating lots of unnecessary work for yourself. I'll preface this by saying that there are certain classes for which subclassing ought to be a trigger that you need to reconsider your design. and UIScrollView is one such class.
You have already acknowledged that you have a collection of UIImageView objects. You should let them handle interaction. The general idea would be:
1) Send each instance something like [imageView setUserInteractionEnabled:YES];. This is the single most commonly overlooked mistake when working with interactive image views.
2) Add an appropriate concrete UIGestureRecognizer instance to each image view and implement the gesture recognizer callbacks. Unless you absolutely must support ancient iOS releases, you should always try to use gesture recognizers in lieu of explicit touch handling.
3) In the gesture recognizer callbacks, add your logic code that takes an appropriate action based on the sending gesture recognizer's -view. (You could, for example, examine the corresponding view's -image or -frame and use the information to decide which image was touched.)
Since the gesture recognizer callback will likely be in your view controller it will have a much easier time "talking" to the rest of your code.
I'm using TTLauncherView as a sort of home screen for my app and I only have one page's worth of icons. How can I make it so the TTLauncherView won't let you drag icons to "the next page"? I want to set a maximum number of pages (in this case one.)
(EDIT: long story short, I subclassed beginEditing, see the answer below.)
I see where why it adds an extra page when beginEditing is called, but I don't want to edit the framework code. (That makes it hard to update to newer versions.) I'd also prefer not to subclass and override that one method, if I have to rely on how it's implemented. (I'm not against subclassing or adding a category if it's clean.)
I tried setting scrollView.scrollEnabled to NO in the callback method launcherViewDidBeginEditing in my TTLauncherViewDelegate, but that doesn't work while it's in editing mode and I don't know why.
I tried adding a blocker UIView to the scrollview to intercept the touch events by setting userInteractionEnabled=NO, which works OK. I still have to disable the dragging of TTLauncherItems to the next page somehow.
I also tried setting the contentSize of the scrollview to it's bounds in launcherViewDidBeginEditing, but that didn't seem to work either.
Is there a better way?
Tried to block gestures:
- (void)setLauncherViewScrollEnabled:(BOOL)scrollEnabled {
if (scrollEnabled) {
[self.scrollViewTouchInterceptor removeFromSuperview];
self.scrollViewTouchInterceptor = nil;
} else {
// iter through the kids to get the scrollview, put a gesturerecognizer view in front of it
UIScrollView *scrollView = [launcherView scrollViewSubview];
self.scrollViewTouchInterceptor = [UIView viewWithFrame:scrollView.bounds]; // property retains it
UIView *blocker = self.scrollViewTouchInterceptor;
[scrollView addSubview:scrollViewTouchInterceptor];
[scrollView sendSubviewToBack:scrollViewTouchInterceptor];
scrollViewTouchInterceptor.userInteractionEnabled = NO;
}
}
For reference: TTLauncherView.m:
- (void)beginEditing {
_editing = YES;
_scrollView.delaysContentTouches = YES;
UIView* prompt = [self viewWithTag:kPromptTag];
[prompt removeFromSuperview];
for (NSArray* buttonPage in _buttons) {
for (TTLauncherButton* button in buttonPage) {
button.editing = YES;
[button.closeButton addTarget:self action:#selector(closeButtonTouchedUpInside:)
forControlEvents:UIControlEventTouchUpInside];
}
}
// Add a page at the end
[_pages addObject:[NSMutableArray array]];
[_buttons addObject:[NSMutableArray array]];
[self updateContentSize:_pages.count];
[self wobble];
if ([_delegate respondsToSelector:#selector(launcherViewDidBeginEditing:)]) {
[_delegate launcherViewDidBeginEditing:self];
}
}
I think overriding beginEditing in TTLauncherView is your best bet. Since you'd only really be touching one method (and only a few lines in that method), upgrading it when the time comes shouldn't be too bad. Since that method explicitly adds the extra page, I'm not sure how you'd get around it w/o editing that specific piece of code
As Andrew Flynn suggested in his answer, I was able to make it work by subclassing and overriding the beginEditing method to remove the extra page TTLauncherView adds when it goes into editing mode.
One problem I'm having is I can't figure out how to remove the warning I get for calling the (private) method updateContentSize on my subclass. I tried casting it to id, but that didn't remove the warning. Is it possible?
edit: I was able to remove the warning by using performSelector to send the message to the private class. (I had previously create a category method performSelector:withInt that wraps NSInvocation so that I can pass primitives via performSelector methods, which makes this very convenient.)
// MyLauncherView.h
#interface MyLauncherView : TTLauncherView {
NSInteger _maxPages;
}
#property (nonatomic) NSInteger maxPages;
#end
// MyLauncherView.m
#implementation MyLauncherView
#synthesize maxPages = _maxPages;
- (void)beginEditing {
[super beginEditing];
// ignore unset or negative number of pages
if (self.maxPages <= 0) {
return;
}
// if we've got the max number of pages, remove the extra "new page" that is added in [super beginEditing]
if ([_pages count] >= self.maxPages ) {
[_pages removeLastObject];
[self updateContentSize:_pages.count]; // this has a compiler warning
// I added this method to NSObject via a category so I can pass primitives with performSelector
// [self performSelector:#selector(updateContentSize:) withInt:_pages.count waitUntilDone:NO];
}
}
Similar to this question I have a custom subclass of UITableViewCell that has a UITextField. Its working fine except the keyboard for doesn't go away when the user touches a different table view cell or something outside the table. I'm trying to figure out the best place to find out when something outside the cell is touched, then I could call resignFirstResponder on the text field.
If the UITableViewCell could receive touch events for touches outside of its view then it could just resignFirstResponder itself but I don't see any way to get those events in the cell.
EDIT: I tried this (below) in my UITableViewCell subclass but it doesn't work, I think because touchesBegan:withEvent: doesn't get called if the event was handled by a control. I think I need to catch the events before they get send down the responder chain somehow.
The solution I'm considering is to add a touchesBegan:withEvent: method to the view controller. There I could send a resignFirstResponder to all tableview cells that are visible except the one that the touch was in (let it get the touch event and handle it itself).
Maybe something like this pseudo code:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint touchPoint = // TBD - may need translate to cell's coordinates
for (UITableViewCell* aCell in [theTableView visibleCells]) {
if (![aCell pointInside:touchPoint withEvent:event]) {
[aCell resignFirstResponder];
}
}
}
I'm not sure if this is the best way to go about this. There doesn't seem to be any way for the tableviewcell itself to receive event notifications for events outside its view.
EDIT2: I thought I had an answer (I even posted it as an answer) using hitTest:withEvent: but that didn't work out. It doesn't always get called. :-(
[Edited: removed previous attempt which didn't always work, this one does]
OK, I finally figured a solution that fully works. I subclassed UITableView and overrode the hitTest:withEvent: method. It gets invoked for all touches anywhere in the table view, the only other possible touches are in the navbar or keyboard and the tableview's hitTest doesn't need to know about those.
This keeps track of the active cell in the table view, and whenever you tap a different cell (or non-cell) it sends a resignFirstResponder to the cell going inactive, which gives it a chance to hide its keyboard (or its datepicker).
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
// check to see if the hit is in this table view
if ([self pointInside:point withEvent:event]) {
UITableViewCell* newCell = nil;
// hit is in this table view, find out
// which cell it is in (if any)
for (UITableViewCell* aCell in self.visibleCells) {
if ([aCell pointInside:[self convertPoint:point toView:aCell] withEvent:nil]) {
newCell = aCell;
break;
}
}
// if it touched a different cell, tell the previous cell to resign
// this gives it a chance to hide the keyboard or date picker or whatever
if (newCell != activeCell) {
[activeCell resignFirstResponder];
self.activeCell = newCell; // may be nil
}
}
// return the super's hitTest result
return [super hitTest:point withEvent:event];
}
In my UITableViewCell subclasses that have a UITextField, I add the following code to get rid of the keyboard (or date picker, which slides up just like the keyboard):
-(BOOL)resignFirstResponder
{
[cTextField resignFirstResponder];
return [super resignFirstResponder];
}
Yay!
I think you're on the right track, but touchesBegan:withEvent: is a UIResponder method, so you'd actually have to override it in a UIView subclass rather than in your UIViewController subclass. Your options are:
If you're already subclassing UITableViewCell, override touchesBegan:withEvent: there.
If you're using a standard UITableViewCell, implement tableView:didSelectRowAtIndexPath in your UITableView's delegate.
That is a very good solution, the best I've found on the net. The only glitch I've discovered is that if you go from one cell with a textfield to another, the keyboard dismisses and reappears resulting in a jerky type animation.