UIControl: sendActionsForControlEvents omits UIEvent - iphone

I want to implement a custom subclass of UIControl. It works beautifully except for one fatal problem that is making me spit teeth. Whenever I use sendActionsForControlEvents: to send an action message out, it omits to include a UIEvent. For example, if I link it to a method with the following signature:
- (IBAction) controlTouched:(id)sender withEvent:(UIEvent *)event
... the event always comes back as nil! The problem seems to occur within sendActionsForControlEvents:
Now, I need my IBAction to be able to determine the location of the touch. I usually do so by extracting the touches from the event. Surely there must be some way to ensure that the correct event is delivered? It's a pretty fundamental part of using a UIControl!
Anyone know a legal workaround?

I would assume that this is because the sendActionsForControlEvents: method can't know which UIEvent (if any) your control event should be associated with.
You could try to send all the actions separately (replicating what the sendActionsForControlEvents: method does, according to the documentation), so you can specifically associate them with a UIEvent:
UIEvent *event = ...;
UIControlEvents controlEvent = ...;
for (id target in [self allTargets]) {
NSArray *actions = [self actionsForTarget:target forControlEvent:controlEvent];
for (NSString *action in actions) {
[self sendAction:NSSelectorFromString(action) to:target forEvent:event];
}
}

I have ONE possible solution at the moment, but I'm not very happy about it. For others faced with the same problem though, here it is. First, declare a local variable or property for a UIEvent thus:
#property (nonatomic, assign) UIEvent * currentEvent;
Now, in your touch-handling routines, set that local variable to the current UIEvent for that routine before calling [self sendActionsForControlEvents:] like so, replacing UIControlEventTouchDown with whichever action you want to send out of course.
self.currentEvent = event;
[self sendActionsForControlEvents: UIControlEventTouchDown];
Finally, override the following method thus:
- (void) sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
[super sendAction:action to:target forEvent:self.currentEvent];
}
This works, but I am not in the least bit fond of it, so if anybody has an alternative solution that doesn't rely on holding a weak reference to a UIEvent, I will be overjoyed to hear it!

Related

cast UITextView to UITextField

This may seem like a very simple question, however I haven't been able to discover a simple option yet...
I have a series of UITextFields followed by a UITextView. How can I incorporated the TextFields and the TextView in the same method below.
-(BOOL) textFieldDidBeginEditing:(UITextField *) textField{
textField = activeField;
if([self.textField1 isFirstResponder]){activeField = textField1;}
else if([self.textField2 isFirstResponder]){activeField = textField2;}
else if([self.textField3 isFirstResponder]){activeField = textField3;}
else if([self.textView1 isFirstResponder]){ activeField = textView1;}
[scrollView1 scrollRectToVisible:[activeField frame] animated:YES];
return NO;
}
The last line causes a warning of:
Incompatible pointer types assigning to 'UITextField *_strong' from 'UITextView *_strong'
This is due (I'm sure) to the obvious fact that the UITextField and the UITextView are different Objects... which is fine however is there a way to get around this as I wish to be able to advance through the textFields and TextViews with a next and previous button.
as per this method
-(void) nextTextField:(id)sender{
if([self.textField1 isFirstResponder]){activeField = textField2;}
else if([self.textField2 isFirstResponder]){activeField = textField3;}
else if([self.textField3 isFirstResponder]){activeField = textView1;}
else if([self.textView1 isFirstResponder]){ activeField = textField1;}
}
I was hoping for a casting sort of option however I am a little confused as to how to cast in objective C... This might sound dumb however
activeField = ((UITextField) textView1);
is how I'd have cast in Java but it seems I just can't seem to get he syntax right.
Should I cast to a UIView as they both inherit from that?
Thank you in advance
Okay, so you don't need to store activeField, for this method, so the only relevant code would be your nextTextField: method. Try changing it to:
-(void) nextTextField:(id)sender{
if([self.textField1 isFirstResponder]) {[textField2 becomeFirstResponder];}
else if([self.textField2 isFirstResponder]) {[textField3 becomeFirstResponder];}
else if([self.textField3 isFirstResponder]) {[textView1 becomeFirstResponder];}
else if([self.textView1 isFirstResponder]) {[textField1 becomeFirstResponder];}
}
You don't need to cast to a different type or anything.
I think you have a few issues with your thinking. You will need to go down two levels to get to a common super type. Both UITextField and UITextView are "visible" components so they inherit from UIView. They can both be cast to UIView but not to each other. But this won't help you with your question. I think you may be struggling with delegation also.
The method textFieldDidBeginEditing: is a delegate call and it only works with UITextField. This is why the class that contains your method above should implement UITextFieldDelegate. When you set focus to the UITextField, UITextField first checks to see that the delegate is not nil. If the delegate property holds a class then UITextField checks to see if it implements the textFieldDidBeginEditing: method explicitly. If the method is implemented in the delegate then the UITextField calls the method.
It is no different with the UITextView. However UITextView doesn't even know about the textFieldDidBeginEditing: method. It has its own delegate and its own method that performs the same general function as textFieldDidBeginEditing:. The UITextView version of this method is called textViewDidBeginEditing:. Like the UITextfield the UITextView checks that the delegate is not nil and that it implements textViewDidBeginEditing:. If these requirements are met then UITextView will textViewDidBeginEditing. However UITextView will never call textFieldDidBeginEditing:.
Finally objects cannot be cast into something they're not. They can only be cast as its own type or any of its ancestors.
You will need to set up both each components' method (textFieldDidBeginEditing and textViewDidBeginEditing) for this to work.
Hope this helps.

Setting a delegate using blocks in iPhone

On a view controller I have multiple textfields, which all use the same delegate. Now in the delegate the code gets really ugly since I have to differentiate between all the textfields (bunch of if/else-if or a switch statement). I came a cross this article:
Blocks in textfield delegates
But from this I still don't understand how this solves the problem? Doesn't this basically call one method and pass it the text and the method has no idea what textfield gave the string? You would still need to differentiate between the textfields, but this time inside the block (with the usual if(textfield == bazTextField)...).
I don't know that it exactly solves the problem so much as shifts it (and into viewDidLoad, which usually gets a bit of mush-mash in it anyway).
However in that example the block itself was being passed in the textfield to run comparisons with and "remembers" the values of all the instance variables as well (if it refers to them), so that's how it knows what text and text field is being dealt with.
I don't see how that code exactly is supposed to help though, since it assigns one block to the single delegate class to be used with all text field delegates - unless perhaps you were supposed to have one per text field, each with a different block. Then you have way more code than you'd have had with the if statements!
The article doesn't make it clear, but I believe the idea is to create one of these blocks (and block delegate objects) for each UITextField that you wish to have respond to textFieldShouldReturn.
hm, maybe I didn't completely understand the article, but I don't see the advantage of using blocks instead of selectors in that concrete example.
you could achieve something similar like this
#interface AlternativeTextFieldDelegate : NSObject <UITextFieldDelegate>
{
SEL selectorToCall;
id objectToCall;
}
- (void) setObjectToCall:(id)obj selector:(SEL)selector;
#end
#implementation AlternativeTextFieldDelegate
- (void) setObjectToCall:(id)obj selector:(SEL)selector
{
objectToCall = obj;
selectorToCall = selector;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[objectToCall performSelector:selectorToCall];
return YES;
}
#end
and the view controller
#interface ViewWithTextFieldsController : UIViewController
{
UITextField *tf1;
AlternativeTextFieldDelegate *delegateForTF1;
UITextField *tf2;
AlternativeTextFieldDelegate *delegateForTF2;
}
// ...IBOutlets and all that...
- (void) tf1ShouldReturn; // handles shouldReturn for tf1
- (void) tf2ShouldReturn; // handles shouldReturn for tf2
#end
#implementation ViewWithTextFieldsController
- (void) viewDidLoad // or wherever
{
delegateForTF1 = [[AlternativeTextFieldDelegate alloc] init];
[delegateForTF1 setObjectToCall:self selector:#selector(tf1ShouldReturn)];
tf1.delegate = delegateForTF1;
delegateForTF2 = [[AlternativeTextFieldDelegate alloc] init];
[delegateForTF2 setObjectToCall:self selector:#selector(tf2ShouldReturn)];
tf2.delegate = delegateForTF2;
}
// ...
#end
don't really know if that's any better than chaining if-elses though.
it seems to me that this complicates things more than the problem it solves.

UIControl subclass is unable to take a target?

I've subclasses UIControl and in it I am sending:
[self sendActionsForControlEvents:UIControlEventValueChanged];
When I create an instance of the object, I add a target as follows:
[starView addTarget:self action:#selector(starRatingChanged:) forControlEvents:UIControlEventValueChanged];
The view shows up fine, and without the target being there the functionality works well. But with adding the target, it crashes. Any ideas why?
My class is declared with:
#interface RMStarRating : UIControl {...}
For what it is worth, I set up my view in - (void)layoutSubviews. Is there another method that I need to subclass in order for the targets to be saved properly or for the targets to be sent the right actions? I thought UIControl handled saving the targets and actions for you.
UPDATE: trying to provide more information
I set the object up as follows:
RMStarRating *starView = [[RMStarRating alloc] initWithFrame:CGRectMake(10, 70, 23*5, 30)];
[starView addTarget:self action:#selector(starRatingChanged:) forControlEvents:UIControlEventValueChanged];
....
[self.view addSubview:starView];
My sendAction, according to Jordan's suggestion:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
NSLog(#"send action");
[super sendAction:action to:target forEvent:event];
}
My function that calls sendActionsForControlEvents:
- (void)updateValue:(UITouch *)touch {
....
NSLog(#"sendActionsForControlEvents");
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
And the function that should be called (and it is in the header too):
- (void)starRatingChanged:(id)sender {
NSLog(#"star rating changed");
}
And the log just spits out:
2010-10-22 09:45:41.348 MyApp[72164:207] sendActionsForControlEvents
2010-10-22 09:45:41.350 MyApp[72164:207] send action
The debugger has:
Have you tried implementing
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
instead? A good example is located here:
Can I override the UIControlEventTouchUpInside for a UISegmentedControl?
Ok, I figured out what it was. I was releasing my parent class too soon, so there was no object for the message to be sent back to, even though it was showing on screen.
And I ended up not needing the sendAction:to:forEvent.
Jordan, thanks you for your help.

Is there a way to retrieve every responder that has handled a UITouch?

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.

delegates and multiple methods

I have a problem that I solved using delegates, but now I am thinking I may have made a mistake.
This is what I want to do.
I have a class that runs on a delay. When it is done it has a finished delegate that it calls.
Now I have the main class that creates two of these delay classes.
I don't want them to both be handled by the same isfinished method in the main class. I want to use two different ones.
However I believe with the protocol method of creating delegates that this will not work for me.
Is there a way around this?
delayclass setdelegates MainclassFunction1
delayclass setdelegates MainclassFunction2
If I understand you correctly, take a look at the NSTableViewDelegate protocol. There, each delegate method's first argument is the NSTableView instance sending the message.
You can solve your issue by changing your delegate methods to have your delegating object send itself as an argument. Then, in your delegate, you'd do something like this:
if (theDelegator == objectA)
{
// Do something
}
if (theDelegator == objectB)
{
// Do something else
}
This way, you've got one cleanly-implemented delegate method that can handle multiple objects delegating to it.
Using delegates doesn't seem like the correct approach to me; they're generally used for augmenting behavior. What sounds most appropriate here is the target/selector pattern, like NSTimer.
#interface MyObject : NSObject {
#private
id target;
SEL selector;
}
#property(assign) id target;
#property SEL selector; /* The selector must return void and accept one argument, which is the MyObject instance that invoked the method. */
#end
#implementation MyObject
- (void)notifyTarget {
[[self target] performSelector:[self selector] withObject:self];
}
#synthesize target;
#synthesize selector;
#end
This is generally the cleanest approach since the delegate callback doesn't need to disambiguate the sender. Using notifications seems like too much overhead for a problem in this domain.
As mentioned, commonly delegate methods would include the object initiating the callback, so you can differentiate that way. Alternately you could have the object post a notification instead, which will also make the originator available.
Why are you not just using NSTimer, adding different timers and having them call whatever selectors you like in the class you are using as a delegate now?
Something like:
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:#selector(myMethod1:) userInfo:nil repeats:YES];
NSTimer *timer2 = [NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:#selector(myMethod2:) userInfo:nil repeats:YES];
Where your methods are:
- (void) myMethod1:(NSTimer*)theTimer
{
// do Something
}
- (void) myMethod2:(NSTimer*)theTimer
{
// do Something different
}
You want to save off and retain both timer1/timer2 references, so that you can stop the timers in dealloc ([timer1 invalidate]).
Short note: Generally, it's bad style to have "if" statements that switch on an object. We all do it occasionally for getting that second list w/o needing a new controller, but switching is what method calls do internally, so ideally you'd just let the ObjC runtime take care of doing the right thing. Several options:
-(void) tableViewSelectionDidChange: (NSTableView*)theView
{
SEL theAction = NSSelectorFromString( [NSString stringWithFormat: #"tableView%#SelectionDidChange:", [theView autosaveName]] );
[self performSelector: theAction withObject: theView];
}
-(void) tableViewUKSourceListSelectionDidChange: (NSTableView*)theView
{
// UKSourceList-table-specific stuff here.
}
-(void) tableViewUKUsersListSelectionDidChange: (NSTableView*)theView
{
// UKUsersList-table-specific stuff here.
}
This works best when you have a non-localized string label, like the autoSave name, but can also use the tag, although that makes the code less readable (which one is "table 1"?). Sometimes it's better to just write a subclass that has a special string for that purpose, or even has methods where you can specify selector names to forward the delegate methods to.
Caleb's suggestion is also good, it's also called "target/action" in case you want to google for it. I have several (Mac) classes that have a regular "action" for clicks, a "doubleAction" for double clicks etc.