You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UIColor子类实现状态色:深色模式下Idle颜色不更新问题

UIColor子类深色模式下idle属性不动态更新问题排查与解决

问题背景

我们通过UIColor子类CUIColorVariant实现状态色,包含hovered、pressed、disabled三个公开属性与私有idle属性(作为实例默认颜色)。原本运行正常,但添加深色模式支持后发现:其他状态色可随trait变化自动更新,仅idle颜色无法更新。

有趣的是,将idle设为公开属性后,它能在深色模式下正常更新,推测问题出在子类内部对idle属性的解析逻辑。

相关实现代码

@interface CUIColorVariant : UIColor

@property (nonatomic, readonly) UIColor *hovered;
@property (nonatomic, readonly) UIColor *pressed;
@property (nonatomic, readonly) UIColor *disabled;

- (instancetype)initWithIdle:(UIColor *)idle hovered:(UIColor *)hovered pressed:(UIColor *)pressed disabled:(UIColor *)disabled;

@end


@interface CUIColorVariant ()

@property (nonatomic, strong) UIColor *idle;

@end

@implementation CUIColorVariant

- (instancetype)initWithIdle:(UIColor *)idle hovered:(UIColor *)hovered pressed:(UIColor *)pressed disabled:(UIColor *)disabled {
    self = [super init];
    if (self) {
        _idle = idle;
        _hovered = hovered;
        _pressed = pressed;
        _disabled = disabled;
    }
    return self;
}

#pragma mark - Required UIColor methods

- (CGColorRef)CGColor {
    return self.idle.CGColor;
}

- (CIColor *)CIColor {
    return self.idle.CIColor;
}

- (void)set {
    [self.idle set];
}

- (void)setFill {
    [self.idle setFill];
}

- (void)setStroke {
    [self.idle setStroke];
}

- (UIColor *)colorWithAlphaComponent:(CGFloat)alpha {
    return [self.idle colorWithAlphaComponent:alpha];
}

- (UIColor *)resolvedColorWithTraitCollection:(UITraitCollection *)traitCollection {
    return [self.idle resolvedColorWithTraitCollection:traitCollection];
}

- (BOOL)getWhite:(nullable CGFloat *)white alpha:(nullable CGFloat *)alpha {
    return [self.idle getWhite:white alpha:alpha];
}

- (BOOL)getHue:(nullable CGFloat *)hue saturation:(nullable CGFloat *)saturation brightness:(nullable CGFloat *)brightness alpha:(nullable CGFloat *)alpha {
    return [self.idle getHue:hue saturation:saturation brightness:brightness alpha:alpha];
}

- (BOOL)getRed:(nullable CGFloat *)red green:(nullable CGFloat *)green blue:(nullable CGFloat *)blue alpha:(nullable CGFloat *)alpha {
    return [self.idle getRed:red green:green blue:blue alpha:alpha];
}

#pragma mark - Required NSObject methods

- (BOOL)isEqual:(id)object {
    if ([object isKindOfClass:CUIColorVariant.class]) {
        return [self.idle isEqual:[(CUIColorVariant*)object idle]]
        && [self.hovered isEqual:[(CUIColorVariant*)object hovered]]
        && [self.pressed isEqual:[(CUIColorVariant*)object pressed]]
        && [self.disabled isEqual:[(CUIColorVariant*)object disabled]];
    } else {
        return NO;
    }
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL aSelector = [invocation selector];

    if ([self.idle respondsToSelector:aSelector]) {
        [invocation invokeWithTarget:self.idle];
    } else {
        [super forwardInvocation:invocation];
    }
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([self.idle respondsToSelector:aSelector]) {
        return [[self idle] methodSignatureForSelector:aSelector];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

- (NSUInteger)hash {
    return self.idle.hash ^ self.hovered.hash ^ self.pressed.hash ^ self.disabled.hash;
}

- (id)copy {
    return [[CUIColorVariant alloc] initWithIdle:[_idle copy] hovered:[_hovered copy] pressed:[_pressed copy] disabled:[_disabled copy]];
}

- (id)copyWithZone:(NSZone *)zone {
    return [[CUIColorVariant allocWithZone:zone] initWithIdle:[_idle copyWithZone:zone]
                                                      hovered:[_hovered copyWithZone:zone]
                                                      pressed:[_pressed copyWithZone:zone]
                                                     disabled:[_disabled copyWithZone:zone]];
}
@end

关键观察点

  • 将idle设为公开属性时,可在深色模式下正常更新;
  • hovered、pressed、disabled等状态色更新正常;
  • idle属性虽动态初始化,但作为子类默认颜色时似乎静态解析;
  • hovered、pressed、disabled为UIDynamicCatalogColor实例,idle则不是(除非直接通过属性访问)。

问题根源

确实和UIColor的动态解析机制直接相关:

  1. UIColor动态解析依赖属性暴露与KVO:系统对动态颜色(如UIDynamicCatalogColor)的trait变化更新,依赖于对颜色属性的KVO监听。当idle是私有属性时,系统无法通过常规路径监听其变化,导致动态颜色无法被正确解析更新。
  2. 子类解析方法的错误实现:当前resolvedColorWithTraitCollection:直接返回self.idle的解析结果,相当于丢弃了子类的状态包装,系统无法将子类实例与动态颜色的trait更新逻辑关联。当idle变为公开属性后,系统能识别到这是一个动态颜色属性,从而自动处理trait变化时的更新。

解决方案

无需将idle设为公开属性,通过以下调整即可修复:

1. 修正resolvedColorWithTraitCollection:方法

当前实现直接返回idle的解析颜色,正确做法是返回一个新的CUIColorVariant实例,确保所有状态色都用传入的traitCollection解析:

- (UIColor *)resolvedColorWithTraitCollection:(UITraitCollection *)traitCollection {
    UIColor *resolvedIdle = [self.idle resolvedColorWithTraitCollection:traitCollection];
    UIColor *resolvedHovered = [self.hovered resolvedColorWithTraitCollection:traitCollection];
    UIColor *resolvedPressed = [self.pressed resolvedColorWithTraitCollection:traitCollection];
    UIColor *resolvedDisabled = [self.disabled resolvedColorWithTraitCollection:traitCollection];
    return [[CUIColorVariant alloc] initWithIdle:resolvedIdle hovered:resolvedHovered pressed:resolvedPressed disabled:resolvedDisabled];
}

2. 调整方法转发逻辑

当前的方法转发直接绑定私有idle,导致系统误将子类实例当作idle实例处理。调整为使用当前trait解析后的idle进行转发:

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL aSelector = [invocation selector];
    UIColor *resolvedIdle = [self.idle resolvedColorWithTraitCollection:[UITraitCollection currentTraitCollection]];
    if ([resolvedIdle respondsToSelector:aSelector]) {
        [invocation invokeWithTarget:resolvedIdle];
    } else {
        [super forwardInvocation:invocation];
    }
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    UIColor *resolvedIdle = [self.idle resolvedColorWithTraitCollection:[UITraitCollection currentTraitCollection]];
    if ([resolvedIdle respondsToSelector:aSelector]) {
        return [resolvedIdle methodSignatureForSelector:aSelector];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

3. 添加trait变化回调

实现traitCollectionDidChange:方法,在trait变化时手动更新所有状态色:

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    [super traitCollectionDidChange:previousTraitCollection];
    if (![self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
        return;
    }
    _idle = [self.idle resolvedColorWithTraitCollection:self.traitCollection];
    _hovered = [self.hovered resolvedColorWithTraitCollection:self.traitCollection];
    _pressed = [self.pressed resolvedColorWithTraitCollection:self.traitCollection];
    _disabled = [self.disabled resolvedColorWithTraitCollection:self.traitCollection];
}

核心原理总结

  • UIColor的动态更新依赖系统对颜色属性的KVO监听和traitCollectionDidChange:回调;
  • 子类必须正确实现resolvedColorWithTraitCollection:,返回自身的解析实例,而非直接返回内部颜色的结果;
  • 私有属性无法被系统自动监听,需在子类内部手动处理trait变化时的颜色解析更新。

内容的提问来源于stack exchange,提问作者Marcel Voss

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 20:04:52