introduce
-
I haven’t written an article for a long time. I am studying skin change recently, so I will share my recent experience with you.
-
The way of iOS skin is relatively simple. After searching a lot of information, it is found that the mainstream way is as follows:
-
A: Peels by adding attributes to categories, there’s a Manager that manages colors and images, and when the theme changes, it notifies the relevant classes in UIKit that it’s time to change the view color, The view then changes its color based on the colors of the different themes provided in the Manager.
- The advantage of this scheme lies in: the overall idea is relatively simple and clear, and it is not difficult to achieve.
- The disadvantages are:
- For each control, the color is already fixed and there is no way to set, for example, two children of the same parent view to display different colors.
- When our project was complete and large, the disadvantages of this approach became obvious: it was cumbersome to change the interface because we had many interfaces and needed to add the added Category properties to every control on each interface, which was a lot of work.
-
Method 2: use the system-provided UIAppearance to change the theme. The advantage of this method is that the system provides a very simple and convenient API for us to use. The most common one is + (instancetype)appearance; Methods and + (instancetype) appearanceWhenContainedIn: (Class < UIAppearanceContainer >) ContainerClass,… ; These two methods. Specific usage is as follows: [[UINavigationBar appearance] setBarTintColor: myNavBarBackgroundColor]; You can set the barTintColor of the global UINavigationBar. The [[UIBarButtonItem appearanceWhenContainedIn: [UINavigationBar class]. nil] setBackgroundImage:myNavBarButtonBackgroundImage forState:state barMetrics:metrics]; Represents setting color in the specified view, in this case setting the background image of UIBarButtonItem on the UINavigationBar.
-
The principle of this approach is as follows: the UI appearance_selector tag will be used to save the appearance of the UI. When the view is added to the Window, the previously saved appearance will be called to update the view appearance. Therefore, not all properties of UIKit classes can be set using this method. Only UI_APPEARANCE_SELECTOR can be set using this method when there is a flag on the property.
-
The advantage of this approach is that it is very easy to set the appearance of some global system controls.
-
But the disadvantages are also obvious:
- When we want to distinguish the child view above the same parent view, this scheme will be very inconvenient, like the first method, it is difficult to achieve the purpose of customization.
- In addition, when we want to set the font color of UILabel and other controls in different views, it often fails. By checking the system API, we can find the UILabel
setTextColor:
And so onUI_APPEARANCE_SELECTORSo that’s why this skin change is not a panacea.Stack Overflow has an article on why UILabel Settings fail colorsThey said it was a bug in apple’s system. And the way to solve this problem is relatively simple, just rewrite itsetTextColor:
Method, add one to itUI_APPEARANCE_SELECTORFlag bit, so you can give it a custom color. The downside of this approach, however, is that the changes to the code are not reduced at all. On the contrary, when there are many controls that do not display colors correctly, there is a lot of work to be done.
-
Conclusion: I think this method of setting UIAppearance is suitable for setting themes such as UINavigationBar and UITabbar when the global color is fixed. When our skin is relatively simple and does not involve changing the color of almost all the controls like night mode, I think this method can also be used for skin operation.
-
Another thing to note about this method is that when we change the theme color, we need to remove the control from the window and then add it again to trigger this method.
- (void)p_updateSystemWindow { NSArray *windowArray = [UIApplication sharedApplication].windows; for (UIWindow *window in windowArray) { for (UIView *subView in window.subviews) { [subView removeFromSuperview]; [window addSubview:subView]; }}}Copy the code
-
-
Own ideas
- First of all, we should clarify the requirement background:
- The most basic is: can achieve skin change
- The project has been completed, and the project is too complex to modify controller by controller
- Can realize the control of personalized color customization, and not all a class of controls are the same color
- Problems arising:
- Is it possible to combine the above two methods to produce their own way to carry out a simple skin change?
- How to change the code as little as possible, can achieve the effect of skin?
- How to realize the control of personalized color customization?
- How to solve:
- Since the whole project has been completed, if I want to change the code as little as possible, can I hook the system with methodSwizzling
setXXXColor:
Method implementation requires no or minimal changes to the original project code. - Since the control needs to be customized, can we use tag to add tags to the control to be personalized so that different colors can be used according to different tags instead of keeping the original colors of the personalized control unchanged?
- Since the whole project has been completed, if I want to change the code as little as possible, can I hook the system with methodSwizzling
I practice
-
The first step is to provide a Manager to control the theme. In my case, it is called LYThemeManager. This Manager controls switching between themes and notifies the UI control that it is time to change its color when the theme changes. And it provides (UIColor *)colorWithReceiver:(id)receiver selString:(NSString *)selector; And (UIColor *)colorWithReceiver:(id)receiver withTag:(NSInteger)tag selString:(NSString *)selector; Respectively is to achieve global control UI Settings and personalized control UI Settings.
-
There are two dictionaries inside LYThemeManager to read different plist, colorInfoDic to read global UI color Settings and specialColorInfoDic to read personalized control color Settings. The details of plIST are as follows:
In specialPlist, the first number indicates the tag value and the second number indicates the attribute meaning of the setting.
-
So for example UIView category, first of all in this class, I use methodSwizzle to implement the hook system methods, and here I hook the system setBackgroundColor: method and setTintColor: method.
+ (void)load { [self swizzleViewColor]; } #pragma mark - MethodSwizzling + (void)swizzleViewColor { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [LYMethodSwizzleUtils swizzleInstanceMethodWithClass:[self class] OriginMethod:@selector(setBackgroundColor:) swappedMethod:@selector(ly_setBackgroundColor:)]; [LYMethodSwizzleUtils swizzleInstanceMethodWithClass:[self class] OriginMethod:@selector(setTintColor:) swappedMethod:@selector(ly_setTintColor:)]; }); } Copy the code
-
Take the setBackgroundColor: method as an example:
- (void)ly_setBackgroundColor:(UIColor *)color {// use selector to select a method. UIColor *bgColor = [[LYThemeManager shareManager] colorWithReceiver:self withTag:self.tag selString:[NSString stringWithFormat:@"%ld:viewBackgroundColor", self.tag]]; if (bgColor) { [self.pickers setObject:bgColor forKey:@"setBackgroundColor:"]; [self ly_setBackgroundColor:bgColor]; } else { [self ly_setBackgroundColor:color]; }}Copy the code
Why am I using personalized color Settings here :(UIColor *)colorWithReceiver (id)receiver withTag (NSInteger)tag selString (NSString *)selector; That’s because almost all controls in UIKit inherit from UIView, and when we directly set all setBackgroundColor: methods to the same color, the effect is disastrous: all controls are the same color. It’s impossible to distinguish. So I’m going to personalize it, and only change the color of the view in the Controller.
-
Add a dictionary attribute pickers, this attribute is used to add our hook method, its key is the method name, value is the color it should be set, when receiving the notification of changing the color, need to walk through all the data in this attribute, to achieve the color update.
@interface UIView () @property (nonatomic, strong) NSMutableDictionary <NSString *, UIColor *> *pickers; @end #pragma mark - Add Property - (NSMutableDictionary<NSString *,UIColor *> *)pickers { NSMutableDictionary <NSString *, UIColor *> *pickers = objc_getAssociatedObject(self, @selector(pickers)); if (! pickers) { pickers = @{}.mutableCopy; objc_setAssociatedObject(self, @selector(pickers), pickers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateTheme) name:LYThemeChangeNotification object:nil]; } return pickers; }Copy the code
-
Finally, the response to the notification:
#pragma mark - Response Notification - (void)updateTheme { [self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, UIColor * _Nonnull obj, BOOL * _Nonnull stop) { SEL selector = NSSelectorFromString(key); [UIView animateWithDuration:0.3 animations:^{#pragma clang Diagnostic push #pragma clang Diagnostic ignored "-Warc-performSelector-leaks" [self performSelector:selector withObject:obj]; #pragma clang diagnostic pop }]; }]; }Copy the code
-
Since almost all controls in UIKit inherit from UIView and respond the same way as UIView, the Add Property step to the Property picker and the response to notifications are omitted in other categories.
-
The setTextColor: method in UILabel also uses personalized Settings. For UILabel textColor that does not require special Settings, the default color is the same as the default color.
-
All tag values are stored in themeconfig. PCH as macros. This is also a disadvantage when there are too many controls that need to be personalized.
-
Overall train of thought is such, this plan is a preliminary plan only, still have a lot of a lot of inadequacy place.
- The disadvantages are:
- Managing colors with tags, for example, actually changes the original project code as we need to set tag values for different controls.
- The hook system approach may introduce unexpected bugs. However, in the way I hook, when I cannot find the corresponding field in the color matching table, I will directly use the original color for setting, and there is no big problem.
- The advantages of this approach are:
- Changes to the original project can be minimized
- And can realize the control of different requirements for personalized customization. Basically complete the solution to the problem raised at the beginning.
- The disadvantages are:
-
conclusion
- This kind of scheme is still a relatively immature scheme, which has not been certified by real projects. When the project is relatively large, this scheme may not be able to solve the problem well. But this is something new. I will continue to modify and try in this aspect in the future. Also welcome to have the idea of everyone to discuss with me, I hope to be generous to give advice!
- The code for the project is at: address