美文网首页iOS小记iOS工程实践Swifty Coding
iOS应用主题(图片,颜色)统一管理

iOS应用主题(图片,颜色)统一管理

作者: YxxxHao | 来源:发表于2016-08-24 19:14 被阅读1804次

    // 2017.8.24 更新
    进阶篇:再谈 Swift 换肤功能,看完该文后强烈建议看下进阶篇,彩蛋彩蛋彩蛋哦。


    在我过去的一个多月里,发现很多不愉快的事情,导致 OpenGL SE系列的文章好久没有更新了,今天来分享下以前做的主题管理来做一个新开始,OpenGL SE的文章会继续坚持写下去,欢迎关注。

    只需@3x图片

    现在工作改做SDK后,发现很少和界面相关的东西打交道了,但做过APP的同学们都应该知道,为了适应各种屏幕的尺寸,图片资源需要提供@1x、@2x和@3x来适配屏幕界面,现在基本没有 1x屏幕的设备了,可以不用提供这个分辨率的图片了。但@2x和@3x可以说是重复的资源,这只会增大应用包的大小。


    C69924BD-2C0E-4279-A4EB-3D822C5AB340.png

    在这,我只使用@3x的图片来做适配:
    先写在前面,iOS 8后系统自动会将@3x图片自动适配图片,也就是说你的应用不支持iOS 8以下系统的话,你可以直接使用@3x的资源就可以了,你可以直接跳过这一节。

    这里为了实现换肤功能,所有的资源我都会存在Bundle里面,首先解释下,Bundle是静态的,作为一个资源包是不参加项目编译的,也就是说,bundle包中不能包含可执行的文件,它仅仅是作为资源,被解析成为特定的2进制数据。对于在iOS 8系统上会自动将@3x的资源自动适配后,我们只需要考虑iOS 8下的系统,这个时候我们只需要手动去重新绘制图片的大小(比较消耗性能的动作),实现如下:

    Swift:

    private func scaledImageFrom3x() -> UIImage {
            let locScale = UIScreen.mainScreen().scale
            let theRate: CGFloat = 1.0 / 3.0
            let oldSize = self.size
            let scaleWidth = CGFloat(oldSize.width) * theRate
            let scaleHeight = CGFloat(oldSize.height) * theRate
            var scaleRect = CGRectZero
            scaleRect.size.width = scaleWidth
            scaleRect.size.height = scaleHeight
            UIGraphicsBeginImageContextWithOptions(scaleRect.size, false, locScale)
            drawInRect(scaleRect)
            var newImage = UIImage()
            newImage = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            return newImage
        }
    

    OC:

    - (UIImage *)scaledImageFrom3x
    {
        float locScale = [UIScreen mainScreen].scale;
        
        float theRate = 2.0 / 3.0;
        UIImage *newImage = nil;
        
        CGSize oldSize = self.size;
        
        CGFloat scaledWidth = oldSize.width * theRate;
        CGFloat scaledHeight = oldSize.height * theRate;
        
        CGRect scaledRect = CGRectZero;
        scaledRect.size.width  = scaledWidth;
        scaledRect.size.height = scaledHeight;
        
        UIGraphicsBeginImageContextWithOptions(scaledRect.size, NO, locScale);
        
        [self drawInRect:scaledRect];
        
        newImage = UIGraphicsGetImageFromCurrentImageContext();
        
        UIGraphicsEndImageContext();
        
        if(newImage == nil) {
            NSLog(@"could not scale image");
        }
        return newImage;
    }
    

    换肤功能

    换肤功能,其实就是图片和颜色等资源的切换,也就是说你有几套皮肤,就提供对应的几套资源,当切换皮肤的时候,切换资源访问的路径并发出要换肤的通知,当前界面监听换肤的通知后再去刷新界面就完成了换肤的功能了。
    我们实现一个ThemeManager的主题管理类,应用的所有资源访问都通过这个类来实现统一管理,所有的主题基本上都是由颜色和资源(图片,音频,文本等)来决定的,所以换肤时只要更改主题颜色库(themeColors)和主题资源库(themeBundle),实现如下:

    Swift:

    class CPThemeManager: NSObject {
        
        private var themeStyle: CPThemeType?
        private var themeBundle: NSBundle?
        private var themeColors: Dictionary<String, AnyObject>?
    
        // MARK: 单例
        static let shareInstance = CPThemeManager()
        private override init() {}
    }
    

    OC:

    static BTThemeManager * _themeManager = nil;
    
    @interface BTThemeManager () {
        NSDictionary *_themeColors;
    }
    
    @property (nonatomic, strong) NSDictionary *themeColors;
    @property (nonatomic, strong) NSBundle     *themeBundle;
    
    @end
    
    @implementation BTThemeManager
    
    @synthesize themeStyle = _themeStyle;
    @synthesize themeColors = _themeColors;
    
    + (BTThemeManager *)getInstance
    {
        static dispatch_once_t onceToken;
        
        dispatch_once(&onceToken, ^{
            _themeManager = [[BTThemeManager alloc]init];
        });
        
        return _themeManager;
    }
    
    - (id) init
    {
        if (self = [super init]) {}
        return self;
    }
    
    @end
    

    上面也说了,换肤实质只是更换资源访问的路径,所以提供一个设置主题的方法来进行资源路径的设置(setThemeStyle),在重新设置完资源路径后,再对外发出更新界面的通知,实现如下:

    Swift:

    func setThemeStyle(themeStyle: CPThemeType) {
            
            //设置资源路径
            
            NSNotificationCenter.defaultCenter().postNotificationName("CPThemeChangeNotification", object: nil)
        }
    

    OC:

    - (void)setThemeStyle:(BTThemeType)themeStyle
    {
        if (_themeStyle == themeStyle ) {
            return;
        }
        
        _themeStyle = themeStyle;
        
        //设置资源路径
        
        [[NSNotificationCenter defaultCenter] postNotificationName:BTThemeChangeNotification object:nil];
    }
    

    下面将说整个主题管理功能的最重要一步:监听主题的切换。

    首先定义一个需要更新主题的协议方法:

    Swift:

    protocol CPThemeListenerProtocol {
        func CPThemeDidNeedUpdateStyle() -> Void
    }
    

    OC:

    @protocol BTThemeListenerProtocol <NSObject>
    
    - (void) BTThemeDidNeedUpdateStyle;
    
    @end
    

    然后在主题管理理里面添加一个注册监听主题切换方法:

    Swift:

    func addThemeListener(object: CPBaseViewController) {
        NSNotificationCenter.defaultCenter().addObserver(object,
                                                         selector:#selector(object.CPThemeDidNeedUpdateStyle),
                                                         name: "CPThemeChangeNotification",
                                                         object: nil)
    }
    
    func removeThemeListener(object: AnyObject) {
        NSNotificationCenter.defaultCenter().removeObserver(object)
    }
    

    因为Swift selector现在只能通过类名.方法名来设置,导致如果要使用则必须要继承一个基类,如果你们有更好的方法,求分享下。

    OC:

    - (void)addThemeListener:(id )obj
    {
        if([obj respondsToSelector:@selector(BTThemeDidNeedUpdateStyle)]){
            [[NSNotificationCenter defaultCenter] addObserver:obj selector:@selector(BTThemeDidNeedUpdateStyle) name:BTThemeChangeNotification object:nil];
        }
        
    }
    
    - (void) removeThemeListener:(id)obj
    {
        if (obj) {
            [[NSNotificationCenter defaultCenter] removeObserver:obj];
        }
    }
    

    最后在要实现主题切换的页面里添加主题管理类的监听切换方法,并实现协议的方法,把需要做主题切换的资源访问都放在这个方法里面,然后就搞定啦。搞了?好像少了点什么,还没有说如何去访问资源呢,这个我想大家都能自己去去实现,就是在基类里实现一个统一访问资源的方法:

    - (void )BTThemeImage:(NSString *)imageName completionHandler:(void (^)(UIImage *image))handler;
    {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 耗时的操作
            NSString *imagePath = [NSString stringWithFormat:@"image/%@",imageName];
            UIImage *image = nil;
            
            //通过资源路径去访问
            
            if (image == nil) {
                image = [UIImage imageNamed:imageName];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                // 更新界面
                handler(image);
            });
        });
    }
    

    写在最后:欢迎大家一起多交流多学习,有更好的想法实现什么的, 求分享~~~

    相关文章

      网友评论

      • 最讨厌梅雨天啦:使用单例,然后把所有的主题都放到本地的plist文件里面,
        通过[ ThemeManager sharedManager].primaryColor 访问各种颜色以及图片路径,并且 Themanager的数据放置在本地来保存当前主题
      • RelaxLiu:说实话感觉这样实现主题换肤功能还是挺麻烦的,第一,每个界面单独接收换肤通知实现换肤操作,那一个大型app几百个界面,突然加入换肤功能 开发量太大 并且还要给每个view声明成属性,以便在通知回调中刷新view。第二,现在一些公司是分产品线的,分不同的业务代码,如果皮肤这个功能你们产品线去做,其它业务其实是不关心你的皮肤功能的,如果是你这样就需要公司所有业务线都知道你这套逻辑并且修改他们的代码,这也很痛苦。
        菲皇:我的做法是,皮肤管理单例暴露动态渲染的接口,管理类弱引用外部view设置图片、颜色的代码块以及view,当需要换肤时只需要遍历弱引用集合,刷新view;当需要换回默认皮肤时,只需要执行一边block。
      • coolLee:话说有没有demo啊,求分享
      • da27c260cc85:图片和颜色我做的类扩展

      本文标题:iOS应用主题(图片,颜色)统一管理

      本文链接:https://www.haomeiwen.com/subject/nssosttx.html