UIControl subclass is unable to take a target? - iphone

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.

Related

Apply PanGesture to uiview in another class with action

I have a ViewController iDragHomeViewController
and another
NSObject class iDrag
"iDragHomeViewController.m"
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *dragView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 200, 200)];
[dragView setBackgroundColor:[UIColor greenColor]];
[self.view addSubview:dragView];
iDrag *drag = [[iDrag alloc]init];
[drag makeDraggableView:dragView];
}
"iDrag.m"
-(void)makeDraggableView: (UIView *)dragView {
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(cellPan:)];
[dragView addGestureRecognizer:panRecognizer];
}
- (void)cellPan:(UIPanGestureRecognizer *)iRecognizer {
UIView *viewToDrag = [[UIView alloc]init];
viewToDrag = iRecognizer.view;
CGPoint translation = [iRecognizer translationInView:[viewToDrag superview]];
viewToDrag.center = CGPointMake(iRecognizer.view.center.x + translation.x,
iRecognizer.view.center.y + translation.y);
[iRecognizer setTranslation:CGPointMake(0, 0) inView:[viewToDrag superview]];
}
Now what I am trying here is to make this "dragView"(belongs to iDragHomeViewController) draggable in iDrag class by applying it PanGesture.
But the code is crashing.
I know some people will suggest me to use NSNotification to handle Pan action in another class but I dont want to write a single line in iDragHomeViewController and handle everything in iDrag Class only.
Is it Possible ??
Please Help.
To be sure I need to know the error output but a guess...
From UIGestureRecognizer doc:
- (id)initWithTarget:(id)target action:(SEL)action
target parameter:
An object that is the recipient of action messages sent by the receiver when it recognizes a gesture. nil is not a valid value.
Thats the reason why your app crashes. The drag object is already released while the recognizer tries to call the cellPan: method.
You initialize the iDrag object in viewDidLoad and is not retained. (It is not a member variable and not used anywhere else....). End of the viewDidLoad the iDrag object is released by ARC.
I would not make any other object responsible for handling pan gestures, unless I had a good reason for that. And would make the view controller responsible for creating gesture recognizer and handling the events.
I assume you have really good reason for that, like the handling is used by multiple views, etc... If it is the case then a better approach would be making iDrag object singleton(shared instance).
Got the answer
Just need to declare iDrag object as a property
#property(nonatomic,strong) iDrag *drag;

UIControl: sendActionsForControlEvents omits UIEvent

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!

Passing any control as an argument of function

I want to make a common function which takes a control as an argument (like UITextField, UIButton etc.)
Its working fine if I do like this
- (void) myFunction : (UITextField*) : control
{
}
//But I want to make it common for any control
- (void) myFunction : (`I don't know what to write here`) : control
{
//suppose if control is UITextField, I can set its font and its size.
//something like this
[control setFont:[UIFont fontWithName:#"Verdana" size:12]];
}
Is this possible?
You can also go like this
- (void) myFunction : (id) control
{
if([control isKindOfClass:[UITextField class]]){
// Your textfield condition
}
}
You should pass UIControl, since it's the super class for the controls.
Then in your code, you should use methods like respondsToSelector: to determine whether or not the control passed in can do what you need it to do.
You could check its class type using isKindOfClass: or isMemberOfClass as well.
Once you know which object you're dealing with, you could type cast it to save on some typing and remove any warnings about not responding to selectors, like this:
// decided that it's a UITextField after using `respondsToSelector:` or `isKindOfClass:`
UITextField *aTextField = (UITextField *)control;
This method is known as "duck-typing" - since it's similar to saying "If it walks like a duck and sounds like a duck, it'll probably be a duck".
UIControl is the superclass of UITextField, UIButton, and other controls, so this is what you want:
- (void) myFunction:(UIControl *) control
Yes, It is possible. You can pass UIView as an argument like Below
- (void) myFunction:(UIView*)customView{
if([customView isKindOfClass:[UIImageView class]]){
// This is UIImageview
}
}
Hope this Help.
use generic ID then cast your control and ask if it's a UITextField (or subclass of...)
- (void) myFunction : (id) control
{
//suppose if control is UITextField, I can set its font and its size.
//something like this
(UITextField*)aText = (UITextField*)control;
if ([aText.class isSubclassOfClass:[UITextField class]]) {
[aText setFont:[UIFont fontWithName:#"Verdana" size:12]];
}
}

Programmatically adding switch to scrollview throws exception

I am using storyboards and have dragged a scrollview window onto a view. In my code I am programmatically creating a switch object that is somehow not being initialized correctly. The switch appears on the view correctly but whenever I click the switch, an exception is thrown saying
"unrecognized selector sent to instance 0x6a786f0'"
I have also attempted to edit the On/Off text to Yes/No and accessing the switch also throws the same exception. Clearly I have missed something in creating my switch and setting the correct delegates or whatever.
My code to create the switch is..
UISwitch *switchControl = [[UISwitch alloc] initWithFrame:CGRectMake(x, y, 60, 20)];
[switchControl addTarget:inputsView action:#selector(actionSwitch:) forControlEvents:UIControlEventTouchUpInside];
[switchControl setBackgroundColor:[UIColor clearColor]];
//[(UILabel *)[[[[[[switchControl subviews] lastObject] subviews]
// objectAtIndex:1] subviews] objectAtIndex:0] setText:#"Yes"];
//[(UILabel *)[[[[[[switchControl subviews] lastObject] subviews]
// objectAtIndex:1] subviews] objectAtIndex:1] setText:#"No"];
[inputsView addSubview:switchControl];
inputsView is the name of my UIScrollView that I created in my .h file.
I should note, when the exception is called on clicking the switch, in the error the 'reason' is reason: '-[UIScrollView actionSwitch:]. When the error is called by trying to adjust the text, the 'reason' is reason: '-[UIImageView setText:]
Any help on what I am missing would be great.
Thanks
The exception is correct, UIScrolView does not have a method actionSwitch:. The target parameter in addTarget: is the object you want to receive the selector: argument.
If your posted code is in the class that has the actionSwitch: method then you would use self as the target, like so:
[switchControl addTarget:self action:#selector(actionSwitch:) forControlEvents:UIControlEventTouchUpInside];
And as a side note. For a UISwitch you generally want your method called for UIControlEventValueChanged, that way if the user just touches the switch but doesn't "switch" it your method won't be called.
Edit in response to: "I just tried changing to 'self' for the UISwitch and the error still occurs. I haven't created an actionSwitch method."
Yes, your application would still crash because whatever you pass in as the target must implement the selector/method passed in as the selector.
The view controller is the ideal place to implement this method. A very standard implementation of this event target would look like:
-(void)actionSwitch:(UISwitch *)theSwitch{
if (theSwitch.isOn){
// Switch was switched on respond accordingly
}
else {
// Switch was switched off respond accordingly
}
}

How can I set a maximum on the number of pages in a TTLauncherView?

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];
}
}