-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathUIButton+QMUI.m
235 lines (194 loc) · 11.3 KB
/
UIButton+QMUI.m
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/**
* 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.
*/
//
// UIButton+QMUI.m
// qmui
//
// Created by QMUI Team on 15/7/20.
//
#import "UIButton+QMUI.h"
#import "QMUICore.h"
#import "UIImage+QMUI.h"
@interface UIButton ()
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSDictionary<NSAttributedStringKey, id> *> *qbt_titleAttributes;
@property(nonatomic, strong) NSMutableSet<NSNumber *> *qbt_statesWithTitle;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *qbt_imageTintColors;
@property(nonatomic, strong) NSMutableSet<NSNumber *> *qbt_statesWithImageTintColor;
@end
@implementation UIButton (QMUI)
QMUISynthesizeIdStrongProperty(qbt_titleAttributes, setQbt_titleAttributes)
QMUISynthesizeIdStrongProperty(qbt_statesWithTitle, setQbt_statesWithTitle)
QMUISynthesizeIdStrongProperty(qbt_imageTintColors, setQbt_imageTintColors)
QMUISynthesizeIdStrongProperty(qbt_statesWithImageTintColor, setQbt_statesWithImageTintColor)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
OverrideImplementation([UIButton class], @selector(setTitle:forState:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UIButton *selfObject, NSString *title, UIControlState state) {
if (title.length) {
if (!selfObject.qbt_statesWithTitle) {
selfObject.qbt_statesWithTitle = [[NSMutableSet alloc] init];
}
if (state == UIControlStateNormal) {
[selfObject.qbt_statesWithTitle addObject:@(state)];
} else {
NSString *normalTitle = [selfObject titleForState:UIControlStateNormal] ?: [selfObject attributedTitleForState:UIControlStateNormal].string;
if (![title isEqualToString:normalTitle]) {
[selfObject.qbt_statesWithTitle addObject:@(state)];
} else {
[selfObject.qbt_statesWithTitle removeObject:@(state)];
}
}
} else {
[selfObject.qbt_statesWithTitle removeObject:@(state)];
}
// call super
void (*originSelectorIMP)(id, SEL, NSString *, UIControlState);
originSelectorIMP = (void (*)(id, SEL, NSString *, UIControlState))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, title, state);
[selfObject qbt_syncTitleByStates];
};
});
ExtendImplementationOfVoidMethodWithoutArguments([UIButton class], @selector(layoutSubviews), ^(UIButton *selfObject) {
// 临时解决 iOS 13 开启了粗体文本(Bold Text)导致 UIButton Title 显示不完整 https://github.com/Tencent/QMUI_iOS/issues/620
if (UIAccessibilityIsBoldTextEnabled()) {
[selfObject.titleLabel sizeToFit];
}
});
});
}
- (instancetype)qmui_initWithImage:(UIImage *)image title:(NSString *)title {
// 非 init 开头的方法,无法给 self 赋值,所以无法像常规的写法一样 self = [self init]
BeginIgnoreClangWarning(-Wunused-value)
[self init];
EndIgnoreClangWarning
[self setImage:image forState:UIControlStateNormal];
[self setTitle:title forState:UIControlStateNormal];
return self;
}
- (void)qmui_calculateHeightAfterSetAppearance {
[self setTitle:@"测" forState:UIControlStateNormal];
[self sizeToFit];
[self setTitle:nil forState:UIControlStateNormal];
}
#pragma mark - TitleAttributes
- (void)qmui_setTitleAttributes:(NSDictionary<NSAttributedStringKey,id> *)attributes forState:(UIControlState)state {
if (!attributes && self.qbt_titleAttributes) {
[self.qbt_titleAttributes removeObjectForKey:@(state)];
return;
}
[UIButton qbt_swizzleForTitleAttributesIfNeeded];
if (!self.qbt_titleAttributes) {
self.qbt_titleAttributes = [[NSMutableDictionary alloc] init];
}
// 从 Normal 同步样式到其他 state
if (state != UIControlStateNormal && self.qbt_titleAttributes[@(UIControlStateNormal)]) {
NSMutableDictionary<NSAttributedStringKey, id> *temp = attributes.mutableCopy;
NSDictionary<NSAttributedStringKey, id> *normalAttributes = self.qbt_titleAttributes[@(UIControlStateNormal)];
for (NSAttributedStringKey key in normalAttributes.allKeys) {
if (!temp[key]) {
temp[key] = normalAttributes[key];
}
}
attributes = temp.copy;
}
self.qbt_titleAttributes[@(state)] = attributes;
// 确保调用此方法设置 attributes 之前已经通过 setTitle:forState: 设置的文字也能应用上新的 attributes
[self qbt_syncTitleByStates];
// 一个系统的不好的特性(bug?):如果你给 UIControlStateHighlighted(或者 normal 之外的任何 state)设置了包含 NSFont/NSKern/NSUnderlineAttributeName 之类的 attributedString ,但又仅用 setTitle:forState: 给 UIControlStateNormal 设置了普通的 string ,则按钮从 highlighted 切换回 normal 状态时,font 之类的属性依然会停留在 highlighted 时的状态
// 为了解决这个问题,我们要确保一旦有 normal 之外的 state 通过设置 qbt_titleAttributes 属性而导致使用了 attributedString,则 normal 也必须使用 attributedString
if (self.qbt_titleAttributes.count && !self.qbt_titleAttributes[@(UIControlStateNormal)]) {
[self qmui_setTitleAttributes:@{} forState:UIControlStateNormal];
}
}
// 如果 normal 用了 attributedTitle,那么其他的 state 都必须用 attributedTitle,否则就无法展示出来
- (void)qbt_syncTitleByStates {
if (!self.qbt_titleAttributes.count) return;
for (NSNumber *stateValue in self.qbt_statesWithTitle) {
UIControlState state = stateValue.unsignedIntegerValue;
NSString *title = [self titleForState:state];
NSDictionary<NSAttributedStringKey, id> *attributes = self.qbt_titleAttributes[stateValue] ?: self.qbt_titleAttributes[@(UIControlStateNormal)];
NSAttributedString *string = [[NSAttributedString alloc] initWithString:title attributes:attributes];
string = [UIButton qbt_attributedStringByRemovingLastKern:string];
[self setAttributedTitle:string forState:state];
}
}
+ (void)qbt_swizzleForTitleAttributesIfNeeded {
[QMUIHelper executeBlock:^{
// 如果之前已经设置了此 state 下的文字颜色,则覆盖掉之前的颜色
OverrideImplementation([UIButton class], @selector(setTitleColor:forState:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UIButton *selfObject, UIColor *color, UIControlState state) {
// call super
void (*originSelectorIMP)(id, SEL, UIColor *, UIControlState);
originSelectorIMP = (void (*)(id, SEL, UIColor *, UIControlState))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, color, state);
NSDictionary *attributes = selfObject.qbt_titleAttributes[@(state)];
if (attributes) {
NSMutableDictionary<NSAttributedStringKey, id> *newAttributes = attributes.mutableCopy;
newAttributes[NSForegroundColorAttributeName] = color;
[selfObject qmui_setTitleAttributes:newAttributes.copy forState:state];
}
};
});
} oncePerIdentifier:@"UIButton (QMUI) titleAttributes"];
}
// 去除最后一个字的 kern 效果,避免字符串尾部出现多余的空白
+ (NSAttributedString *)qbt_attributedStringByRemovingLastKern:(NSAttributedString *)string {
if (!string.length) {
return string;
}
NSMutableAttributedString *attributedString = string.mutableCopy;
[attributedString removeAttribute:NSKernAttributeName range:NSMakeRange(string.length - 1, 1)];
return attributedString.copy;
}
#pragma mark - ImageTintColor
- (void)qmui_setImageTintColor:(UIColor *)color forState:(UIControlState)state {
if (!color && self.qbt_imageTintColors) {
[self.qbt_imageTintColors removeObjectForKey:@(state)];
return;
}
[UIButton qbt_swizzleForImageTintColorIfNeeded];
if (!self.qbt_imageTintColors) {
self.qbt_imageTintColors = [[NSMutableDictionary alloc] init];
}
self.qbt_imageTintColors[@(state)] = color;
UIImage *stateImage = [self imageForState:state];
if (!stateImage) return;
stateImage = [[stateImage qmui_imageWithTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[self setImage:stateImage forState:state];
}
+ (void)qbt_swizzleForImageTintColorIfNeeded {
[QMUIHelper executeBlock:^{
OverrideImplementation([UIButton class], @selector(setImage:forState:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UIButton *selfObject, UIImage *image, UIControlState state) {
BOOL isFirstSetImage = image && ![selfObject imageForState:UIControlStateNormal];
UIColor *imageTintColor = selfObject.qbt_imageTintColors[@(state)];
if (imageTintColor) {
image = [[image qmui_imageWithTintColor:imageTintColor] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
}
// call super
void (*originSelectorIMP)(id, SEL, UIImage *, UIControlState);
originSelectorIMP = (void (*)(id, SEL, UIImage *, UIControlState))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, image, state);
if (isFirstSetImage) {
[selfObject.qbt_imageTintColors enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, UIColor * _Nonnull color, BOOL * _Nonnull stop) {
UIControlState s = key.unsignedIntegerValue;
if (s != state) {// 避免死循环
UIImage *stateImage = [selfObject imageForState:s];
if (stateImage) {
stateImage = [[stateImage qmui_imageWithTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[selfObject setImage:stateImage forState:s];
}
}
}];
}
};
});
} oncePerIdentifier:@"UIButton (QMUI) titleAttributes"];
}
@end