杨锴
2025-04-16 09a372bc45fde16fd42257ab6f78b8deeecf720b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/**
 * Tencent is pleased to support the open source community by making QMUI_iOS available.
 * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://opensource.org/licenses/MIT
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */
//
//  UIColor+QMUITheme.m
//  QMUIKit
//
//  Created by MoLice on 2019/J/20.
//
 
#import "UIColor+QMUITheme.h"
#import "QMUIThemeManager.h"
#import "QMUICore.h"
#import "NSMethodSignature+QMUI.h"
#import "UIColor+QMUI.h"
#import "QMUIThemePrivate.h"
#import "QMUIThemeManagerCenter.h"
 
@implementation QMUIThemeColor
 
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 随着 iOS 版本的迭代,需要不断检查 UIDynamicColor 对比 UIColor 多出来的方法是哪些,然后在 QMUIThemeColor 里补齐,否则可能出现”unrecognized selector sent to instance“的 crash
        // https://github.com/Tencent/QMUI_iOS/issues/791
#ifdef DEBUG
        Class dynamicColorClass = NSClassFromString(@"UIDynamicColor");
        NSMutableSet<NSString *> *unrecognizedSelectors = NSMutableSet.new;
        NSDictionary<NSString *, NSMutableSet<NSString *> *> *methods = @{
            NSStringFromClass(UIColor.class): NSMutableSet.new,
            NSStringFromClass(dynamicColorClass): NSMutableSet.new,
            NSStringFromClass(self): NSMutableSet.new
        };
        [methods enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull classString, NSMutableSet<NSString *> * _Nonnull methods, BOOL * _Nonnull stop) {
            [NSObject qmui_enumrateInstanceMethodsOfClass:NSClassFromString(classString) includingInherited:NO usingBlock:^(Method  _Nonnull method, SEL  _Nonnull selector) {
                [methods addObject:NSStringFromSelector(selector)];
            }];
        }];
        [methods[NSStringFromClass(UIColor.class)] enumerateObjectsUsingBlock:^(NSString * _Nonnull selectorString, BOOL * _Nonnull stop) {
            if ([methods[NSStringFromClass(dynamicColorClass)] containsObject:selectorString]) {
                [methods[NSStringFromClass(dynamicColorClass)] removeObject:selectorString];
            }
        }];
        [methods[NSStringFromClass(dynamicColorClass)] enumerateObjectsUsingBlock:^(NSString * _Nonnull selectorString, BOOL * _Nonnull stop) {
            if (![methods[NSStringFromClass(self)] containsObject:selectorString]) {
                [unrecognizedSelectors addObject:selectorString];
            }
        }];
        if (unrecognizedSelectors.count > 0) {
            QMUILogWarn(NSStringFromClass(self), @"%@ 还需要实现以下方法:%@", NSStringFromClass(self), unrecognizedSelectors);
        }
#endif
    });
}
 
#pragma mark - Override
 
- (void)set {
    [self.qmui_rawColor set];
}
 
- (void)setFill {
    [self.qmui_rawColor setFill];
}
 
- (void)setStroke {
    [self.qmui_rawColor setStroke];
}
 
- (BOOL)getWhite:(CGFloat *)white alpha:(CGFloat *)alpha {
    return [self.qmui_rawColor getWhite:white alpha:alpha];
}
 
- (BOOL)getHue:(CGFloat *)hue saturation:(CGFloat *)saturation brightness:(CGFloat *)brightness alpha:(CGFloat *)alpha {
    return [self.qmui_rawColor getHue:hue saturation:saturation brightness:brightness alpha:alpha];
}
 
- (BOOL)getRed:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha {
    return [self.qmui_rawColor getRed:red green:green blue:blue alpha:alpha];
}
 
- (UIColor *)colorWithAlphaComponent:(CGFloat)alpha {
    return [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject<NSCopying> * _Nullable identifier, __kindof NSObject * _Nullable theme) {
        return [self.themeProvider(manager, identifier, theme) colorWithAlphaComponent:alpha];
    }];
}
 
- (CGFloat)alphaComponent {
    return self.qmui_rawColor.qmui_alpha;
}
 
- (CGColorRef)CGColor {
    // 这个 UIColor 对象,以前是直接拿 self.qmui_rawColor,但某些场景(具体是什么场景不知道了,看 git commit 是 2019 年的提交)这样有问题,所以才改为先用 self.qmui_rawColor.CGColor 生成一个 UIColor。
    UIColor *rawColor = [UIColor colorWithCGColor:self.qmui_rawColor.CGColor];
    
    // CGColor 必须通过 CGColorCreate 创建。UIColor.CGColor 返回的是一个多对象复用的 CGColor 值(例如,如果 QMUIThemeA.light 值和 UIColorB 的值刚好相同,那么他们的 CGColor 可能也是同一个对象,所以 UIColorB.CGColor 可能会错误地使用了原本仅属于 QMUIThemeColorA 的 bindObject)
    // 经测试,qmui_red 系列接口适用于不同的 ColorSpace,应该是能放心使用的😜
    // https://github.com/Tencent/QMUI_iOS/issues/1463
    CGColorSpaceRef spaceRef = CGColorSpaceCreateDeviceRGB();
    CGColorRef cgColor = CGColorCreate(spaceRef, (CGFloat[]){rawColor.qmui_red, rawColor.qmui_green, rawColor.qmui_blue, rawColor.qmui_alpha});
    CGColorSpaceRelease(spaceRef);
    
    [(__bridge id)(cgColor) qmui_bindObject:self forKey:QMUICGColorOriginalColorBindKey];
    return (CGColorRef)CFAutorelease(cgColor);
}
 
- (NSString *)colorSpaceName {
    return [((QMUIThemeColor *)self.qmui_rawColor) colorSpaceName];
}
 
- (id)copyWithZone:(NSZone *)zone {
    QMUIThemeColor *color = [[[self class] allocWithZone:zone] init];
    color.name = self.name;
    color.managerName = self.managerName;
    color.themeProvider = self.themeProvider;
    return color;
}
 
- (BOOL)isEqual:(id)object {
    return self == object;// 例如在 UIView setTintColor: 时会比较两个 color 是否相等,如果相等,则不会触发 tintColor 的更新。由于 dynamicColor 实际的返回色值随时可能变化,所以即便当前的 qmui_rawColor 值相等,也不应该认为两个 dynamicColor 相等(有可能 themeProvider block 内的逻辑不一致,只是其中的某个条件下 return 的 qmui_rawColor 恰好相同而已),所以这里直接返回 NO。
}
 
- (NSUInteger)hash {
    return (NSUInteger)self.themeProvider;// 与 UIDynamicProviderColor 相同
}
 
- (NSString *)description {
    return [NSString stringWithFormat:@"%@,%@qmui_rawColor = %@", [super description], self.name.length ? [NSString stringWithFormat:@" name = %@, ", self.name] : @" ", self.qmui_rawColor];
}
 
- (UIColor *)_highContrastDynamicColor {
    return self;
}
 
- (UIColor *)_resolvedColorWithTraitCollection:(UITraitCollection *)traitCollection {
    return self.qmui_rawColor;
}
 
#pragma mark - <QMUIDynamicColorProtocol>
 
@dynamic qmui_isDynamicColor;
 
- (NSString *)qmui_name {
    return self.name;
}
 
- (UIColor *)qmui_rawColor {
    QMUIThemeManager *manager = [QMUIThemeManagerCenter themeManagerWithName:self.managerName];
    UIColor *color = self.themeProvider(manager, manager.currentThemeIdentifier, manager.currentTheme);
    UIColor *result = color.qmui_rawColor;
    return result;
}
 
- (BOOL)qmui_isQMUIDynamicColor {
    return YES;
}
 
// _isDynamic 是系统私有的方法,实现它有两个作用:
// 1. 在某些方法里(例如 UIView.backgroundColor),系统会判断当前的 color 是否为 _isDynamic,如果是,则返回 color 本身,如果否,则返回 color 的 CGColor,因此如果 QMUIThemeColor 不实现 _isDynamic 的话,`a.backgroundColor = b.backgroundColor`这种写法就会出错,因为从 `b.backgroundColor` 获取到的 color 已经是用 CGColor 重新创建的系统 UIColor,而非 QMUIThemeColor 了。
// 2. 当 iOS 13 系统设置里的 Dark Mode 发生切换时,系统会自动刷新带有 _isDynamic 方法的 color 对象,当然这个对 QMUI 而言作用不大,因为 QMUIThemeManager 有自己一套刷新逻辑,且很少有人会用 QMUIThemeColor 但却只依赖于 iOS 13 系统来刷新界面。
// 注意,QMUIThemeColor 是 UIColor 的直接子类,只有这种关系才能这样直接定义并重写,不能在 UIColor Category 里定义,否则可能污染 UIDynamicColor 里的 _isDynamic 的实现
- (BOOL)_isDynamic {
    return !!self.themeProvider;
}
 
@end
 
@implementation UIColor (QMUITheme)
 
+ (instancetype)qmui_colorWithThemeProvider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject<NSCopying> * _Nullable, __kindof NSObject * _Nullable))provider {
    return [self qmui_colorWithName:nil themeManagerName:QMUIThemeManagerNameDefault provider:provider];
}
 
+ (UIColor *)qmui_colorWithName:(NSString *)name themeProvider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject<NSCopying> * _Nullable, __kindof NSObject * _Nullable))provider {
    return [self qmui_colorWithName:name themeManagerName:QMUIThemeManagerNameDefault provider:provider];
}
 
+ (UIColor *)qmui_colorWithThemeManagerName:(__kindof NSObject<NSCopying> *)managerName provider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject<NSCopying> * _Nullable, __kindof NSObject * _Nullable))provider {
    return [self qmui_colorWithName:nil themeManagerName:managerName provider:provider];
}
 
+ (UIColor *)qmui_colorWithName:(NSString *)name themeManagerName:(__kindof NSObject<NSCopying> *)managerName provider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject<NSCopying> * _Nullable, __kindof NSObject * _Nullable))provider {
    QMUIThemeColor *color = QMUIThemeColor.new;
    color.name = name;
    color.managerName = managerName;
    color.themeProvider = provider;
    return color;
}
 
@end