欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS【开发之 APP自定义换肤、主题更换】

程序员文章站 2022-06-30 20:54:21
主题更换的设计思路 现在App类似桌面软件的趋势,在功能完善之后,渐渐追求个性化,以满足不同用户的审美,主题更换就是其中一项,像Weico微博客户端,UC浏览器。所谓主题,可...

主题更换的设计思路

现在App类似桌面软件的趋势,在功能完善之后,渐渐追求个性化,以满足不同用户的审美,主题更换就是其中一项,像Weico微博客户端,UC浏览器。所谓主题,可以看成相同功能不同展现可视资源的集合,例如,按钮无论在什么主题下都需要背景图片这个资源,只是在不同主题下是不同的背景图片而已。

如何在iOS中实现主题更换的核心思路为:

  1. 资源按主题放置:相同功能的资源名称相同,放在不同的主题路径或者前缀使用主题名。
  2. 增加中间层,隔离不同主题相同功能资源使用的变化。

    1. 主题管理

    主题的特性导致代码不关心资源的表现是什么,只关心资源的功能,而主题是易变化的,因此需要将易变化的部分抽离出来,整合到一个管理者中,主题的变化在管理者中完成,而不影响资源使用的地方。而且这个管理者是全局唯一的,因此使用单例。

     

    1

    2

    3

    4

    5

    6

    7

    8

    9

     

    + (ThemeManager *)sharedInstance

    {

    static ThemeManager *sharedInstance = nil;

    if (sharedInstance == nil)

    {

    sharedInstance = [[ThemeManager alloc] init];

    }

    return sharedInstance;

    }

    主题中的资源使用plist进行存储,颜色的RGBA值跟字体的信息可以直接存入plist,而图片则可以存入图片的位置。按主题命名plist文件,ThemeManager的初始化跟主题更换就从main bundle中按主题名字读取对应的plist文件。

     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

     

    - (id)init

    {

    if (self = [super init])

    {

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSString *themeName = [defaults objectForKey:@"theme"] ?: @"default";

     

    NSString *path = [[NSBundle mainBundle] pathForResource:themeName ofType:@"plist"];

    self.theme = [NSDictionary dictionaryWithContentsOfFile:path];

    }

    return self;

    }

    代码中不再是直接使用主题相关的资源,而是通过ThemeManager得到对应主题下的资源。

     

    1

    2

    3

    4

    5

    6

    7

     

    // 直接使用资源:

    UIImage *image = [UIImage imageNamed:@"xxx_btn_background"];

     

    // 通过主题管理器使用资源:

    NSDictionary *theme = [ThemeManager sharedInstance].theme;

    NSString *imageName = [theme objectForKey:@"xxx_btn_background"];

    UIImage *image = [UIImage imageNamed:imageName];

    上面的代码在使用时还是有些复杂,代码只关心资源的功能,不关系也不应该关心取资源的细节,因此应在ThemeManager对取资源进行如下封装:

     

    1

     

    - (UIImage *)imageForKey:(NSString *)key;

    在使用主题中的资源时,代码就变成了:

     

    1

     

    UIImage *image = [[ThemeManager sharedInstance] imageForKey:@"xxx_btn_background"];

    2. 资源的放置

    系统将主题相关的资源文件部署到ios设备中时,在默认情况下,系统会将所有的资源plat平铺拷贝到mainBundle目录下,即使你的资源是按文件夹来组织的。(我们可以在模拟器中查看Bundle的情况,模拟器的路径是:~/Library/Application Support/iPhone Simulator)

    因此,在将资源文件加入到工程时,不要选默认的”Recursively create groups for any add folders”,要选择“Create Folder Reference for any add folders”,这样才能保证资源文件按照原有文件夹的组织格式被拷贝到mainBundle中。

    关于上述的两个选项,就涉及到Xcode的Group(黄色)跟Folder Reference(蓝色)的概念了,参见从别处摘抄来的理解:

    XCode项目中的文件夹分成两类: group 和 directory reference, 分别是虚结构和实结构. 黄颜色的 group 是默认的格式, 它的结构和磁盘上的文件夹毫无关系, 仅仅表示资源的逻辑组织结构, 这在管理源文件是非常方便. 同一段代码可以被很多项目使用, 也可能只使用一个目录的部分文件, 它不需要被拷贝到当前项目中, 但可以在当前项目中保持一个清晰的逻辑结构. 而且引用头文件时不需要指明复杂的层次结构, 因为这些文件在XCode看来是 flat 的, 即它们处在同一层文件夹里.

    但是 group 带来便利的同时也导致更加棘手的麻烦, 文件重名冲突问题; 尤其当你要使用上千个资源文件时, 这种问题已经极难避免; 而且, 资源文件一般是要拷贝到目标程序中的, 虽然它们在项目中可以有结构的组织, 但是复制到程序中时将会 flat 地输出到程序的根目录中, 这将是怎样的一个灾难! 同时, 如果你在外部向文件夹中加入了上百幅图片, 你不得不把它们再向xcode中加入一遍. 归根结底, 还要求助于我们传统的蓝色的 directory reference。

    3. 主题更换通知

    对于没有显示的界面,更换主题是不需要通知的,因为在取资源时是根据当前主题取的,但是对于正在显示的界面,更换主题时就需要进行通知,让界面重新取资源后再重绘。由于这类通知是全局性的,因此应该使用NSNotification实现通知机制。

    在ThemeManager的changeTheme中调用[NSNotificaitonCenter defaultCenter]的postNotificationName:object:发出通知,而在各个涉及到主题更换的ViewController中使用addObserver:selector:name:object:监听通知事件。

    4. 总结

    其实主题的设计思路跟类簇很像,例如对于NSNumber,不同类型的数据其实真正返回的是NSNumber相对于此类型的子类,但是对于NSNumber的使用者而言,其并不关心NSNumber返回的具体子类是什么,只要满足NSNumber定义的接口就行。设计总是类似的,针对易变化的部分,增加一个中间层(接口)将易变化的部分封装起来,提供给使用者稳定不易变的服务。

    总之,OOP跟DesignPattern在我看来主要做了两件事:

    1. 隔离变化
    2. 封装细节