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的动态解析机制直接相关:
- UIColor动态解析依赖属性暴露与KVO:系统对动态颜色(如
UIDynamicCatalogColor)的trait变化更新,依赖于对颜色属性的KVO监听。当idle是私有属性时,系统无法通过常规路径监听其变化,导致动态颜色无法被正确解析更新。 - 子类解析方法的错误实现:当前
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
相关产品推荐
相关产品推荐

