30

Is there a way to set a custom states -- not one of the existing UIControlState values -- for a UIControl?

In the UIControlSate enum, there are 16 bits that can be used for custom control states:

UIControlStateApplication  = 0x00FF0000,              // additional flags available for application use

The problem is that UIControl's state property is readonly.

I want to set different background images to my UIButton for custom states.

jscs
  • 63,694
  • 13
  • 151
  • 195
Mayosse
  • 696
  • 1
  • 7
  • 16

4 Answers4

37

You can make use of the custom states in a subclass of UIControl.

  • Create a variable called customState in which you will manage your custom states.
  • If you need to set a state, do your flag operations against this variable, and call [self stateWasUpdated].
  • Override the state property to return [super state] bitwise OR'd against your customState
  • Override the enabled, selected and highlighted setters so that they call [self stateWasUpdated]. This will allow you to respond to any changes in state, not just changes to customState
  • Implement stateWasUpdated with logic to respond to changes in state

In the header:

#define kUIControlStateCustomState (1 << 16)

@interface MyControl : UIControl {
    UIControlState customState;
}

In the implementation:

@implementation MyControl

-(void)setCustomState {
    customState |= kUIControlStateCustomState;
    [self stateWasUpdated];
}

-(void)unsetCustomState {
    customState &= ~kUIControlStateCustomState;
    [self stateWasUpdated];
}

- (UIControlState)state {
    return [super state] | customState;
}

- (void)setSelected:(BOOL)newSelected {
    [super setSelected:newSelected];
    [self stateWasUpdated];
}

- (void)setHighlighted:(BOOL)newHighlighted {
    [super setHighlighted:newHighlighted];
    [self stateWasUpdated];
}

- (void)setEnabled:(BOOL)newEnabled {
    [super setEnabled:newEnabled];
    [self stateWasUpdated];
}

- (void)stateWasUpdated {
    // Add your custom code here to respond to the change in state
}

@end
Nick Street
  • 4,117
  • 1
  • 27
  • 17
  • 4
    The UIControlState enum specifies that app control states use the mask 0x00FF0000. That means 1<<16 to 1<<23. You use 1<<3 is this valid? Could it possibly conflict with future control states apple might add? – Adam Ritenauer Feb 01 '13 at 21:39
  • 2
    It should also be noted; If you plan on using custom states to control custom resources in UIButton such as the title, background image, image, titleShadow, or attributedTitle. You must call setNeedsLayout after changing your custom state. Otherwise the button will only update it's appearance after it is tapped again. – Adam Ritenauer Feb 01 '13 at 21:57
  • Definitely don't use the 1 << 3, as Adam states that will conflict in future versions of the OS. Use a number in the bit mask range 0x00FF0000. – christophercotton Mar 06 '13 at 21:53
  • A nice supportive explanation is in another question: [Whats the use of UIControlState “application” of UIButton?](https://stackoverflow.com/a/43760213) – denkeni Jul 24 '18 at 01:12
6

Based on @Nick answer I have implemented a simpler version. This subclass exposes a BOOL outlined property that is similar in function to selected, highlighted and enabled.

Doing things like [customButtton setImage:[UIImage imageNamed:@"MyOutlinedButton.png"] forState:UIControlStateOutlined] makes it automagically work when you update the outlined property.

More of these state + property could be added if needed.


UICustomButton.h

extern const UIControlState UIControlStateOutlined;

@interface UICustomButton : UIButton
@property (nonatomic) BOOL outlined;
@end

UICustomButton.m

const UIControlState UIControlStateOutlined = (1 << 16);

@interface OEButton ()
@property UIControlState customState;
@end

@implementation OEButton

- (void)setOutlined:(BOOL)outlined
{
    if (outlined)
    {
        self.customState |= UIControlStateOutlined;
    }
    else
    {
        self.customState &= ~UIControlStateOutlined;
    }
    [self stateWasUpdated];
}

- (BOOL)outlined
{
    return ( self.customState & UIControlStateOutlined ) == UIControlStateOutlined;
}

- (UIControlState)state {
    return [super state] | self.customState;
}

- (void)stateWasUpdated
{
    [self setNeedsLayout];
}

// These are only needed if you have additional code on -(void)stateWasUpdated
// - (void)setSelected:(BOOL)newSelected
// {
//     [super setSelected:newSelected];
//     [self stateWasUpdated];
// }
//
// - (void)setHighlighted:(BOOL)newHighlighted
// {
//     [super setHighlighted:newHighlighted];
//     [self stateWasUpdated];
// }
//
// - (void)setEnabled:(BOOL)newEnabled
// {
//     [super setEnabled:newEnabled];
//     [self stateWasUpdated];
// }

@end
Ricardo Sanchez-Saez
  • 9,466
  • 8
  • 53
  • 92
3

I'd like to offer a slight refinement to this strategy. See this stackoverflow question:

Overriding isHighlighted still changes UIControlState - why?

It turns out that Apple's state implementation is actually a computed property based off the other properties, isSelected, isHighlighted, isEnabled, etc.

So there is actually no need for a custom state bit mask on top of UIControlState (well, it's not that there is no need, it's just that it's adding complexity where there need/ought not be).

If you wanted to be congruent with Apple's implementation, you would just override the state property and check your custom states in the getter.

extension UIControlState {
     static let myState = UIControlState(rawValue: 1 << 16)
} 

class MyControl: UIControl {

      override var state: UIControlState {
          var state = super.state
          if self.isMyCustomState {
               state.insert(UIControlState.myState)
          }
          return state
      }

      var isMyCustomState: Bool = false
 }

It's actually a smart way to go; as per the link above, if you override the property and don't change the state you will get inconsistent results. Making state always a computed property ensures consistency between the properties that state represents.

MH175
  • 2,234
  • 1
  • 19
  • 35
  • 1
    I don't think it makes sense to inject yourself as an extra bit into the existing `state` bitmask. That has "fragile" written all over it. It's one thing to derive a completely different way of expressing the state, but to _assume_ that bit 16 of Apple's own `state` is and always will be free for your hack is wrong. – matt Aug 13 '18 at 18:55
  • I agree, to an extent, so I upvoted. However, I think calling it wrong is a little excessive. We all write code based on assumptions, and I think this is likely to be OK (or at least easy to change later with a simple bit shift). The hack works and it's saved me lots of grief. For example, when assigning a custom color based on the state, it saves you from creating a massive if/else pyramid of doom. i.e. `if (isSelected && isHighlighted) && (!isEnabled || isMyCustomState)`. I'd appreciate any links to alternative strategies for managing this situation. – MH175 Aug 14 '18 at 00:13
  • I would make my own OptionSet. I don't see any of the strategies on this page doing that. – matt Aug 14 '18 at 00:59
2

Swift 3 version of Nick's answer:

extension UIControlState {
    static let myState = UIControlState(rawValue: 1 << 16)
}

class CustomControl: UIControl {

    private var _customState: UInt = 0

    override var state: UIControlState {
       return UIControlState(rawValue: super.state.rawValue | self._customState)
    }

    var isMyCustomState: Bool {
        get { 
            return self._customState & UIControlState.myState.rawValue == UIControlState.myState.rawValue 
        } set {
            if newValue == true {
                self._customState |= UIControlState.myState.rawValue
            } else {
                self._customState &= ~UIControlState.myState.rawValue
            }
        }
    }
}
MH175
  • 2,234
  • 1
  • 19
  • 35