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.
Related
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];
}
}
Trying to determine based on a CGPoint inside a window, what (if any) accessibility element is hit. Is this possible? Sort of an Accessibility Element hit test... Should return any accessibility element hit (including System created). Seems like there has to be a way to do this.
I fear that in order to achieve what you want, you’ll have to basically re-implement how e.g. browsers do the capture phase in their event handling in the DOM:
Start at the most general element — in any UIKit-based app, this will be the application’s keyWindow or (if your App is multiscreen) the front most window of the screen which you’ll have to figure out through the windowLevel etc.
Check whether the object in question is an accessibility-container, and
if so, recurse into that, asking for the appropriate thing.
If it's not, check whether it is an accessibility-element, and
if it’s not bail.
If it is, check whether its accessibility-frame contains the point in question.
Now if that happens to be the case, congratulations: You have succeeded in finding the element in question, so you should return it.
If that isn’t so, bail.
By now, we have handled the cases accessibility-element and element inside container.
There is one additional case to cover and that are accessibility-containers, which are themselves accessibility-elements:
We have a container, that did not yield an element for the point in question.
If it is an accessibility-element, check whether its frame contains the point, and
- if so, return self,
- if not, bail.
And by bail I always mean return nil.
Now grab all that logic, turn it into code, and put it in a category on NSObject.
My best answer would be:
Add all the accessibility elements to an array.
In your touchesEnded method, enumerate through the array and check to see if any of the elements have been touched. One way to do this is to check to see if the boundingBox of the element contains the CGPoint.
Then you can write a bunch of code to respond to that touch!
Hope this helped! ^_^
two possible ways to do that. First one, you create a category of UIView (New File, Category, name it "tapped", subclass of "UIView") and put this code in it:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
NSLog(#"%#", self);
}
so far this code will work with all UIViews but not with all subclasses of itself. So you need to create some more subclasses (eg. UIControl, UIWindow, etc.). In those subclasses you don't need to NSLog() anything, just call:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
}
because all those subclasses will call the superclass they will end up calling the NSLog() of UIView. The downside you have to create an immense amount of subclasses for all cases. For example you need UIView for UIControl, then UIControl for UIButton and so on.
.
The other way isn't exactly what you asked for, but is much easier. Simply call subviews recursively.
- (void)logView:(UIView*)v index:(int)i
{
NSMutableString *str = [NSMutableString string];
for (int u = 0; u<i; u++) { [str appendString:#"| "]; }
[str appendFormat:#"<%# %p frame:%#>", v.class, v, NSStringFromCGRect(v.frame)];
// of course you can change it to display your accessibility hint/label
printf("%s\n", [str UTF8String]);
for (UIView *vv in v.subviews) { [self logView:vv index:i+1]; }
}
you call this method once:
[self logView:self.view index:0];
… and it will automatically create a tree as output:
<UITextField 0x6b30010 frame:{{20, 13}, {280, 31}}>
| <UITextFieldRoundedRectBackgroundView 0x6b31e00 frame:{{0, 0}, {280, 31}}>
| | <UIImageView 0x6b31fe0 frame:{{0, 0}, {0, 0}}>
| | <UIImageView 0x6b32070 frame:{{0, 0}, {0, 0}}>
| | <UIImageView 0x6b320e0 frame:{{0, 0}, {0, 0}}>
Currently, it seems like there is no way to do this with the latest API.
Just a small question i'm really having a lot of problems with
Basically, what I'm doing is making a view every time I hit a button which works fine.
When I want to remove all the images I made when I hit the remove from superview it just removes the last one on the stack.
Is there a way i can get rid of all the images I made?
Here is the code
This puts the picture on the screen
- (IBAction)pushBn:(id)sender {
ZeldaView *newZelda = [[ZeldaView alloc]initWithNibName:#"ZeldaView" bundle:nil];
theZeldaView=newZelda;
[self.view insertSubview:theZeldaView.view atIndex:1];
}
this removes it when i touch it
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch =[touches anyObject];
CGPoint location=[touch locationInView:theZeldaView.view];
if (CGRectContainsPoint(theZeldaView.theImage.frame, location)) {
[theZeldaView.view removeFromSuperview];
}
}
Sure, just iterate through the children of your parent view (self.view) and remove any that are ZeldaView elements.
At it's simplest this would be something like:
for (UIView* subView in [self.view.subviews])
{
if ([subView isKindOfClass:[ZeldaView class]])
[subView removeFromSuperview];
}
Though you will probably want to expand on this to not perform the actual removal during the iteration and you may want to use respondsToSelector and a custom method instead of checking the class here so that you can do any cleanup needed from within the ZeldaView class. Hopefully that makes sense to you.
sure, many ways to do this. I would keep an array of ZeldaView objects and when you want to remove them, traverse the array and remove them all.
in your .h:
#property (nonatomic, retain) NSMutableArray *zeldaViews;
when you add a ZeldaView:
// create a newZeldaView and add it to the superview
[self.zeldaViews addObject:newZeldaView];
when you want to remove them all:
for (ZeldaView *zeldaView in self.zeldaViews) {
[zeldaView.view removeFromSuperview];
}
[self.zeldaViews removeAllObjects];
create the mutable array in viewDidLoad and release it in viewDidUnload and dealloc. Modify as approp if your using ARC.
I'm trying to debug some touchesBegan/Moved/Ended related slowdown in my game; I think that some of my touch responders are not unloading properly, and so as the game runs on more and more of them stack up and the touches get less responsive because they have to pass through a larger and larger responder chain.
Is there some way to view/retrieve the path taken by a UITouch as it moves through the chain? Or simply some way to retrieve a list of all active responders?
Thanks,
-S
You can hijack the desired methods on UIResponder to add logging and then call the original method. Here's an example:
#import <objc/runtime.h>
#interface UIResponder (MYHijack)
+ (void)hijack;
#end
#implementation UIResponder (MYHijack)
+ (void)hijackSelector:(SEL)originalSelector withSelector:(SEL)newSelector
{
Class class = [UIResponder class];
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method categoryMethod = class_getInstanceMethod(class, newSelector);
method_exchangeImplementations(originalMethod, categoryMethod);
}
+ (void)hijack
{
[self hijackSelector:#selector(touchesBegan:withEvent:) withSelector:#selector(MYHijack_touchesBegan:withEvent:)];
}
- (void)MYHijack_touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"touches!");
[self MYHijack_touchesBegan:touches withEvent:event]; // Calls the original version of this method
}
#end
Then somewhere in your app (I sometimes put it in main() itself), just call [UIResponder hijack]. As long as the UIResponder subclass calls super at some point, your code will be injected.
method_exchangeImplementations() is a beautiful thing. Be careful with it of course; it's great for debugging, but very confusing if used indiscriminately.
I would look into your memory usage, rather than trying to retrieve all the responders.
Either look into instruments http://www.mobileorchard.com/find-iphone-memory-leaks-a-leaks-tool-tutorial/
or conversely, just throw in a few NSLog(#"responded"); into your touchesBegan methods and see if it gets logged like ten times for each touch.
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];