25

I'm extending UIButton with generic functionality to change certain appearance attributes based on the displayed title.

In order to do this, I need to detect and respond to changes in the "state" property. This is so I make sure the appearance is adjusted properly if the user has set different titles for different states. I assumed I would need to use some sort of KVO like the following:

[self addObserver:self 
       forKeyPath:@"state" 
          options:NSKeyValueObservingOptionNew 
          context:nil];

But this does not seem to fire the observeValueForKeyPath:... method for @"state" or @"currentTitle". I assume this is because UIButton does not implement the KVO pattern for those properties.

I do not want to just listen for clicks. Those events cause a state change, but are not the only potential causes.

Does anyone know a way to listen to and respond to state changes of a UIButton?

Thanks


UPDATE

Just a note since I've learned a few things in the last couple years ;).

I've since talked with some Apple folks who know, and the reason KVO doesn't work on the state property owes to the fact that NONE of UIKit is guaranteed to be KVO compliant. Thought that was worth repeating here--if you are trying to listen to any property of a UIKit framework class, be aware that it may work but is not officially supported and could break on different iOS versions.

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
DougW
  • 28,776
  • 18
  • 79
  • 107
  • Sorry to dig this thread out of the grave. I was wondering since the UIKit is not KVO compliant is it cause of denial on the appstore validation ? – MaxouMask Nov 28 '14 at 19:41
  • @Maskime - I don't expect they would deny the app unless your usage resulted in noticeable bugs in the application. – DougW Dec 01 '14 at 16:03
  • Ehm, as I know you can't set KVO on `state`, because `state` is not changed, it just returns private variable of `UIControl`. – dimpiax Aug 17 '18 at 16:48

4 Answers4

11

Alright I figured out a solution that works. You can listen to the text property of the button's titleLabel.

[self.titleLabel addObserver:self 
                  forKeyPath:@"text" 
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 
                     context:nil];

It seems to get fired twice per change, so you should check to make sure that the values of @"old" and @"new" in the passed change dictionary are different.

NOTE: Don't use @"old" and @"new" directly. The constants are NSKeyValueChangeOldKey and NSKeyValueChangeNewKey respectively.

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
DougW
  • 28,776
  • 18
  • 79
  • 107
  • What if the text is the same for the different states? Even if this property still gets re-set to the same thing, I don't think you can guarantee that behaviour across different iOS versions as the implementation may not set the text in some state-change circumstances. I wrote a class and posted it in another answer - hope this is useful to someone :) – jhabbott Jun 22 '12 at 22:13
  • @jhabbott - Well sure, but then you're changing the question I asked. I'm specifically trying to change the appearance based on the title text. If the title text doesn't actually change, that has no impact. That said, yes, if someone is concerned with state changes of any kind your answer is probably the way to go--just a bit too heavyweight for my needs. – DougW Jun 23 '12 at 01:15
  • 2
    This confuses me :( I can see in the title of the question `How listen for UIButton state change?` but the correct answer says, `You can listen to the text property`.. well.. cool! But in my particular case it's not possible to rely on the title being changed :( If it's not possible to listen to it with KVO, then i'd like to see a corresponding answer, or at least the one bellow making much more sense – igrek Sep 23 '16 at 14:35
9

I needed this today, so I wrote this class which does the job:

MyButton.h

#import <UIKit/UIKit.h>

// UIControlEventStateChanged uses the first bit from the UIControlEventApplicationReserved group
#define UIControlEventStateChanged  (1 << 24)

@interface MyButton : UIButton
@end

MyButton.m

#import "MyButton.h"

#pragma mark - Private interface
@interface MyButton ()
- (void)checkStateChangedAndSendActions;
@end

#pragma mark - Main class
@implementation MyButton
{
    // Prior state is used to compare the state before
    // and after calls that are likely to change the
    // state. It is an ivar rather than a local in each
    // method so that if one of the methods calls another,
    // the state-changed actions only get called once.
    UIControlState  _priorState;
}

- (void)setEnabled:(BOOL)enabled
{
    _priorState = self.state;
    [super setEnabled:enabled];
    [self checkStateChangedAndSendActions];
}

- (void)setSelected:(BOOL)selected
{
    _priorState = self.state;
    [super setSelected:selected];
    [self checkStateChangedAndSendActions];
}

- (void)setHighlighted:(BOOL)highlighted
{
    _priorState = self.state;
    [super setHighlighted:highlighted];
    [self checkStateChangedAndSendActions];
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
    _priorState = self.state;
    [super touchesBegan:touches withEvent:event];
    [self checkStateChangedAndSendActions];
}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
    _priorState = self.state;
    [super touchesMoved:touches withEvent:event];
    [self checkStateChangedAndSendActions];
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    _priorState = self.state;
    [super touchesEnded:touches withEvent:event];
    [self checkStateChangedAndSendActions];
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
{
    _priorState = self.state;
    [super touchesCancelled:touches withEvent:event];
    [self checkStateChangedAndSendActions];
}

#pragma mark - Private interface implementation
- (void)checkStateChangedAndSendActions
{
    if(self.state != _priorState)
    {
        _priorState = self.state;
        [self sendActionsForControlEvents:UIControlEventStateChanged];
    }
}

@end

You can create it programatically using a UIButton init method, or use it from Interface Builder by adding a normal UIButton to your view and changing the class to MyButton, but you must listen for the UIControlEventStateChanged event programatically. For example from viewDidLoad in your controller class like this:

[self.myButton addTarget:self 
                  action:@selector(myButtonStateChanged:) 
        forControlEvents:UIControlEventStateChanged];
Alex Cio
  • 6,014
  • 5
  • 44
  • 74
jhabbott
  • 18,461
  • 9
  • 58
  • 95
  • 7
    I like the solution, but naming a custom event `UIControlEventStateChanged` as if it's part of the framework is confusing. – Aleks N. Apr 13 '15 at 12:02
  • This is more of a hack. – pronebird Nov 20 '15 at 09:40
  • 1
    Yeah - it is a hack. Sometimes you have to do hacky things to work around deficiencies in the supplied frameworks. It makes me sad. If there is a non-hacky way to achieve this please post an answer, I'd be interested to see it :) – jhabbott Nov 20 '15 at 18:01
2
[self addObserver:self 
       forKeyPath:@"state" 
          options:NSKeyValueObservingOptionNew 
          context:nil];

Works fine if you check inside observer 'selected' property

-(void)observeValueForKeyPath:(NSString *)keyPath  
                     ofObject:(id)object 
                       change:(NSDictionary *)change 
                      context:(void *)context
{
    if ([keyPath isEqualToString:@"selected"])
    {
        [self.img setImage:self.selected ? self.activeImg : self.inactiveImg];
    }
    else
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
}
Alex Cio
  • 6,014
  • 5
  • 44
  • 74
-2

Subclass UIButton, override setState: is what works for me. This is probably not the best way, but I have done it successfully.

Apologies for the above answer, it was wrong. Should have actually looked at my code. In my case, I only needed to change the state based on highlight, so I overrode -setHighlight: to change whatever values I needed. YMMV.

Steven Canfield
  • 7,312
  • 5
  • 35
  • 28
  • 5
    Just tried this and it does not work. The documentation for UIControl concurs: "This attribute is read only—there is no corresponding setter method." – DougW Mar 23 '10 at 21:33
  • You should not override methods that you're not supposed to override. – pronebird Nov 20 '15 at 09:40