Understanding UIButton Control States and Custom State Extensions in iOS

The article explains UIButton’s built‑in UIControlState bitmask, shows how to set titles and colors for normal, highlighted, disabled and selected states, demonstrates combining states, and details creating custom application‑specific states (e.g., follow, mutual, loading) by extending the state enum and overriding state‑related methods.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Understanding UIButton Control States and Custom State Extensions in iOS

For iOS developers, UIButton is the most frequently used control for handling user tap actions. This article introduces the built‑in UIControlState options, shows how they are defined with NS_OPTIONS, and explains how to configure a button for different states.

typedef NS_OPTIONS(NSUInteger, UIControlState) {
    UIControlStateNormal       = 0,
    UIControlStateHighlighted  = 1 << 0, // used when UIControl isHighlighted is set
    UIControlStateDisabled     = 1 << 1,
    UIControlStateSelected     = 1 << 2, // flag usable by app (see below)
    UIControlStateFocused API_AVAILABLE(ios(9.0)) = 1 << 3, // applicable only when the screen supports focus
    UIControlStateApplication  = 0x00FF0000, // additional flags available for application use
    UIControlStateReserved     = 0xFF000000 // flags reserved for internal framework use
};

Typical usage sets the button’s title or title color for each state, for example:

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
// Normal state
[button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
// Highlighted (pressed) state
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[button setBackgroundImage:[UIImage imageNamed:@"btn_highlighted"] forState:UIControlStateHighlighted];
// Disabled state
[button setTitleColor:[UIColor grayColor] forState:UIControlStateDisabled];
// Change enabled flag based on login status
if (/* user not logged in */) {
    button.enabled = NO;
} else {
    button.enabled = YES;
}

Only four states are not sufficient for many real‑world scenarios. Because UIControlState is an options bitmask, any combination of the three low‑order bits yields up to eight distinct states. A quick demo shows that some combinations produce identical visual results because later bits override earlier ones. UIControlStateHighlighted and UIControlStateHighlighted | UIControlStateDisabled behave the same. UIControlStateSelected | UIControlStateHighlighted and

UIControlStateSelected | UIControlStateHighlighted | UIControlStateDisabled

also produce the same effect.

When a particular state has no explicit style, iOS falls back to the Normal style, so it is advisable to define styles for every state you intend to use.

In a real project (the "音街" app), a follow button needed more than the default states: unfollowed , following , followed , and mutual follow . To avoid tangled logic, custom states were created using the UIControlStateApplication range.

enum {
    NKControlStateFollowed  = UIControlStateSelected,
    NKControlStateMutual    = 1 << 16 | UIControlStateSelected,
    NKControlStateLoading   = 1 << 17 | UIControlStateDisabled,
};

@interface NKLoadingButton : UIButton
@property (nonatomic, getter=isLoading) BOOL loading;
@property (nonatomic) UIActivityIndicatorView *spinnerView;
@end

@interface NKFollowButton : NKLoadingButton
@property (nonatomic, getter=isMutual) BOOL mutual;
@end

The custom states are built by shifting bits into the 0x00FF0000 region reserved for application‑specific flags. loading combines with disabled so the button cannot be tapped while loading, and mutual always includes selected because a mutual follow implies the user is already followed.

@implementation NKLoadingButton
- (UIControlState)state {
    UIControlState state = [super state];
    if (self.isLoading) {
        state |= NKControlStateLoading;
    }
    return state;
}
- (void)setEnabled:(BOOL)enabled {
    super.enabled = !_loading && enabled;
}
- (void)setLoading:(BOOL)loading {
    if (_loading != loading) {
        _loading = loading;
        super.enabled = !loading;
        if (loading) {
            [self.spinnerView startAnimating];
        } else {
            [self.spinnerView stopAnimating];
        }
        [self setNeedsLayout];
        [self invalidateIntrinsicContentSize];
    }
}
@end

@implementation NKFollowButton
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setTitle:@"关注" forState:UIControlStateNormal];
        [self setTitle:@"已关注" forState:UIControlStateSelected];
        [self setTitle:@"已关注" forState:UIControlStateSelected | UIControlStateHighlighted];
        [self setTitle:@"互相关注" forState:NKControlStateMutual];
        [self setTitle:@"互相关注" forState:NKControlStateMutual | UIControlStateHighlighted];
        [self setTitle:@"" forState:NKControlStateLoading];
        [self setTitle:@"" forState:NKControlStateLoading | UIControlStateSelected];
        [self setTitle:@"" forState:NKControlStateMutual | NKControlStateLoading];
        // color settings omitted for brevity
    }
    return self;
}
- (UIControlState)state {
    UIControlState state = [super state];
    if (self.isMutual) {
        state |= NKControlStateMutual;
    }
    return state;
}
- (void)setSelected:(BOOL)selected {
    super.selected = selected;
    if (!selected) {
        self.mutual = NO;
    }
}
- (void)setMutual:(BOOL)mutual {
    if (_mutual != mutual) {
        _mutual = mutual;
        if (mutual) {
            self.selected = YES;
        }
        [self setNeedsLayout];
        [self invalidateIntrinsicContentSize];
    }
}
@end

By overriding -state, -setSelected:, and -setMutual:, the button reports a complete and correct state value while keeping style configuration centralized. This approach allows the UI to be driven solely by data (e.g., server responses) without repeatedly setting styles.

In summary, the article walks through the progression from single states to combined states and finally to custom states for UIButton, demonstrating how to extend the built‑in API responsibly and how to think about Apple’s design intentions when using system‑provided enums.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

iOSObjective‑CControlStateCustomStateUIButton
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.