App主题换肤功能

Posted by GH on December 18, 2019

现在国内很多App,每到节假日,都会搞一些活动,这样就免不了需要一个统一管理所谓“皮肤”功能【这里主要指图片、字体、颜色等资源】,下面就是我设计的”皮肤“功能模块。

主题皮肤切换模块

GCTTheme配置篇

调用GCTThemeManager之前需要注册配置数据, 调用的时机目前是在didFinishLaunchingWithOptions的时候 (这个后期可以修改!!)

1
2
3
4
5
// 1. GCTThemeSwitchObject 控制开关的类,灵活控制开关就可以配置这个类 (面向协议)
GCTThemeSwitchObject *switchObj = [GCTThemeSwitchObject new];
[switchObj setGCTThemeSwitch:YES];
// 2. GCTThemeData 数据源的类,目前只读取本地数据,后期远程数据只需要配置它(面向协议)
[[GCTThemeManager sharedInstanceWithThemeData:[GCTThemeData new]] switchBaseOn:switchObj];

GCTTheme使用篇

方法1:如果要修改的控件是UIView或者是它的子类并且这个UIView不等于nil,可以直接调用方法

1
- (void)configSkinWithModule:(GCTThemeModuleTypes)module skinDatas:(NSDictionary<NSString *, NSString *> *)skinDatas;

方法2:如果上面的条件有其中一个不满足,可以直接调用单例GCTThemeManager

1
2
3
4
5
6
7
// 需要配置皮肤替换功能的,图片获取需要 1. moduleName  2. imgKey
- (UIImage *)getImageByKey:(NSString *)imgKey from:(GCTThemeModuleTypes)module;
- (NSString *)getImageNameByKey:(NSString *)imgKey from:(GCTThemeModuleTypes)module;
// 需要配置皮肤替换功能的,颜色获取需要 1. moduleName  2. colorKey
- (UIColor *)getColorByKey:(NSString *)colorKey from:(GCTThemeModuleTypes)module;
// 需要配置皮肤替换功能的,文字获取需要 1. moduleName  2. titleKey
- (NSString *)getTitleByKey:(NSString *)titleKey from:(GCTThemeModuleTypes)module;

实际使用情况

直接配置TabBarController图片、文字。【因为tabBarItem默认是懒加载的,所以只能通过GCTThemeManager去实现】

1
2
3
4
navController.tabBarItem.title = [[GCTThemeManager sharedInstance] getTitleByKey:model.titleKey from:GCTThemeModuleTypeHomeTabbar];
navController.tabBarItem.image = [UIImage imageMakeWithName:model.imageName type:@"png"];
NSString *imageName = [[GCTThemeManager sharedInstance] getImageNameByKey:model.imageSelectedKey from:GCTThemeModuleTypeHomeTabbar];
navController.tabBarItem.selectedImage = [[UIImage imageMakeWithName:imageName type:@"png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
  1. 在默认情况下,使用的是背景色。
  2. 在春节皮肤的配置下使用的是图片 【这里可以以此类推,某种皮肤下面使用的属性1,在另一种皮肤下使用的是属性2,因为当找不到对应的属性值的时候返回nil,也就是不实现】
1
2
self.tipsView.homeBG.image = [[GCTThemeManager sharedInstance] getImageByKey:@"home_wifi_scaning_unconnected" from:GCTThemeModuleTypeHomeWifi];
self.backgroundColor = [[GCTThemeManager sharedInstance] getColorByKey:@"home_wifi_found_scaning" from:GCTThemeModuleTypeHomeWifi];

GCTTheme原理篇

GCTThemeSwitchObject遵循的协议

1
2
3
4
5
6
@protocol GCTThemeSwitchProtocol <NSObject>

- (void)setGCTThemeSwitch:(BOOL)isOpen;
- (BOOL)GCTThemeSwitchIsOpen;

@end

数据源GCTThemeData遵循的协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef enum : NSUInteger {
    GCTThemeDefaultData,
    GCTThemeFestivalData
} GCTThemeDataTypes;

NS_ASSUME_NONNULL_BEGIN

@protocol GCTThemeDataProtocol <NSObject>

// Getter Method
- (NSDictionary *)skinDataFor:(GCTThemeDataTypes)themeType;

// Setter Method
- (void)setSkinData:(NSDictionary *)data for:(GCTThemeDataTypes)themeType;

@end

GCTThemeManager核心代码

1
2
3
4
5
6
7
8
9
10
11
12
- (NSString *)findResourceBy:(NSString *)key from:(GCTThemeModuleTypes)module with:(NSString *)subKey {
    // 切换数据
    NSDictionary *resourcesData = self.switchIsOpen ? self.tempSkinData : self.skinData;
    NSString *moduleString = [self convertThemeType2String:module];
    if (moduleString == nil || [moduleString isEqualToString:@""] || resourcesData == nil || ![resourcesData.allKeys containsObject:moduleString]) return nil;
    NSDictionary *moduleDic = resourcesData[moduleString];
    if (![moduleDic.allKeys containsObject:subKey]) { return nil; }
    NSDictionary *Dic = moduleDic[subKey];
    if (![Dic.allKeys containsObject:key]) { return nil; }
    return Dic[key];
}

测试篇

配置以及Mock数据

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
- (void)setUp {
    self.themeData = [GCTThemeData new];
    // Mock Data
    [self.themeData setSkinData:@{@"home_tabbar": @{@"images": @{@"image_home_button_selected": @"Home_Selected",
                                                                 @"image_video_button_selected": @"Provider_Selected",
                                                                 @"image_discovery_button_selected": @"Discovery_Selected",
                                                                 @"image_mine_button_selected": @"Mine_Selected"}}} for:GCTThemeDefaultData];
    [self.themeData setSkinData:@{@"home_middle": @{@"colors": @{@"color_before_background": @"#cccccc"},
                                                    @"values": @{@"value_home_title": @"测试"}
                                                    }} for:GCTThemeFestivalData];
    
    
    // Mock Switch
    self.switchObject = [GCTThemeSwitchObject new];
    [self.switchObject setGCTThemeSwitch:NO];
    
    self.manager = [GCTThemeManager sharedInstanceWithThemeData:self.themeData];
    [self.manager switchBaseOn:self.switchObject];
}

/*
 ** 测试是否为单例
 */
- (void)testGCTThemeManagerIsSingle {
    NSMutableArray *managers = [NSMutableArray array];
    
    for (int i = 0; i < 3; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            GCTThemeManager *tempManager = [[GCTThemeManager alloc] init];
            [managers addObject:tempManager];
        });
    }
   
    [managers enumerateObjectsUsingBlock:^(GCTThemeManager *obj, NSUInteger idx, BOOL * _Nonnull stop) {
        XCTAssertEqual(self.manager, obj, @"GCTThemeManager is not single");
    }];
}

/*
 ** 测试开关
 */
- (void)testGCTThemeSwitch {
    XCTAssertFalse(self.manager.switchIsOpen, "default switch is closed");
    
    [self.switchObject setGCTThemeSwitch:YES];
    [self.manager switchBaseOn:self.switchObject];
    XCTAssertTrue(self.manager.switchIsOpen, "switch is open");
}

/*
 ** 初始化后数据是否正确
 */
- (void)testGCTThemeInitNoMockDataIsNotNil {
    XCTAssertNotNil(self.manager.skinData, "default data is not to be nil");
    XCTAssertTrue(self.manager.skinData.count != 0, "default data is not to be nil");
    XCTAssertNotNil(self.manager.tempSkinData, "temp data is not to be nil");
    XCTAssertTrue(self.manager.tempSkinData.count != 0, "temp data is not be nil");
}

/*
 ** 通过key来获取图片
 */
- (void)testGCTThemeGetImageByKey {
    XCTAssertNil([self.manager getImageByKey:@"" from:GCTThemeModuleTypeHomeTabbar], "key is nil image should be nil");
    XCTAssertNil([self.manager getImageByKey:@"image_home_button_selected_error" from:GCTThemeModuleTypeHomeTabbar], "key error image should be nil");
    XCTAssertNil([self.manager getImageByKey:@"image_home_button_selected" from:GCTThemeModuleTypeHomeWifi], @"type is error image should be nil");
    XCTAssertNotNil([self.manager getImageByKey:@"image_home_button_selected" from:GCTThemeModuleTypeHomeTabbar], "key and type is right and data has value, image should not be nil");
}

/*
 ** 通过key来获取图片名字
 */
- (void)testGCTThemeGetImageNameByKey {
    XCTAssertEqual([self.manager getImageNameByKey:@"image_home_button_selected" from:GCTThemeModuleTypeHomeTabbar], @"Home_Selected", "image name should be equal");
}

/*
 ** 通过key来获取颜色
 */
- (void)testGetColorByKey {
    // 从春节主题中获取
    [self.switchObject setGCTThemeSwitch:YES];
    [self.manager switchBaseOn:self.switchObject];
    XCTAssertTrue([[self.manager getColorByKey:@"color_before_background" from:GCTThemeModuleTypeHomeMiddleContent] isEqual:[UIColor colorWithHexString:@"#cccccc"]], "UIColor should be equal");
}

- (void)testGCTThemeGetTitleByKey {
    // 从春节主题中获取
    [self.switchObject setGCTThemeSwitch:YES];
    [self.manager switchBaseOn:self.switchObject];
    XCTAssertTrue([[self.manager getTitleByKey:@"value_home_title" from:GCTThemeModuleTypeHomeMiddleContent] isEqual:@"测试"], "Title should be equal");
}

思考篇

配置的时机,需要切换,尽量不要放到AppDelegate中,影响启动时间。 GCTThemeManager中的开关GCTThemeSwitchObject以及数据源GCTThemeData相应的属性都是同时又Getter/Setter方法的,数据不安全!! !!都要通过字符串去查找,编码不是很友好!(后期会通过R.Obj去修正这个问题。) 统一修改的时机每次启动只有一次,后期这里需要修改。