再谈 Swift 换肤功能

在之前我写的 iOS应用主题(图片,颜色)统一管理 一文中,曾介绍了 Swift 皮肤切换功能,但由于那时对 Swift 的理解不够深,所以现在再看之前写的那篇文章,感觉其中的实现很糟糕,所以今天再来谈谈 Swift 的换肤功能。读该文前,建议先读下上述文章。

首先,当然是先上 demo

接着就是效果图:

theme.gif

实现

这个换肤功能的代码量大概就在二百行左右,核心代码就50行左右,这里就不多说,先看下核心代码的:

// Protocols.swift
protocol ThemeProtocol {   
}

extension  ThemeProtocol where Self: UIView { 
    func addThemeObserver() {
        print("addViewThemeObserver")
        NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: NSNotification.Name(rawValue: kUpdateTheme), object: nil)
    }
    func removeThemeObserver() {
        print("removeViewThemeObserver")
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: kUpdateTheme), object: nil)
    } 
}

extension ThemeProtocol where Self: UIViewController {  
    func addThemeObserver() {
        print("addViewControllerThemeObserver")
        NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: NSNotification.Name(rawValue: kUpdateTheme), object: nil)
    }
    func removeThemeObserver() {
        print("removeViewControllerThemeObserver")
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: kUpdateTheme), object: nil)
    }
}

extension UIView {
    func updateTheme() {
        print("update view theme")
    }
}

extension UIViewController {
    func updateTheme() {
        print("update view controller theme")
    }
}

换肤其实就是一个监听者模式,一般情况下,涉及到换肤功能的,要么是在 UIViewController 中,要么就是在 UIView 中,这里先定义一个 ThemeProtocol 协议,然后通过协议的扩展来实现 UIView 和 UIViewController 对换肤功能的监听或移除监听方法,但因为协议的扩展是 Swift 中仅有的,在 OC 中并不支持,所以不能在协议扩展中实现 updateTheme 方法,这里通过扩展 UIView 和 UIViewController 来实现 updateTheme 方法。

我们在 UIView 或 UIViewController 中实现 ThemeProtocol 协议后, 我们就可以对换肤功能进行监听,其它没有实现 ThemeProtocol 协议的相关 UIView 或 UIViewController 就不会受影响,实现如下:

class TestView: UIView, ThemeProtocol {

    override init(frame: CGRect) {
        super.init(frame: frame)
        // 添加监听
        addThemeObserver()
        self.backgroundColor = UIColor("bg_testview")
    }
    
    // 换肤动作
    override func updateTheme() {
        super.updateTheme()
        self.backgroundColor = UIColor("bg_testview")
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
          // 移除添加
        removeThemeObserver()
    }
}

在上篇文章中,为了实现对主题的监听,是通过实现一个基类来实现的,但是这也导致了耦合度奇高,可以移植性差,通过上述的方法,就可以很好地解决这个问题了。

核心内容其实就是上面这些,剩下的内容就和 iOS应用主题(图片,颜色)统一管理 这篇文章几乎一样了,就是实现一个 ThemeManager 类,通过切换 bundle 来对图片和颜色资源进行管理,这里就不详细说了,代码也比较简单,直接下载 demo 看就可以了,这里就上一张目录图:

316641E1-4335-430C-A03D-6F688DD30932.png

附录

ThemeManager.swift 内容:

import UIKit

let kUpdateTheme = "kUpdateTheme"
let kThemeStyle = "kThemeStyle"

final class ThemeManager: NSObject {
    
    var style: ThemeStyle {
        return themeStyle
    }
    
    static var instance = ThemeManager()
    
    private var themeBundleName: String {
        switch themeStyle {
        case .black:
            return "blackTheme"
        default:
            return "defaultTheme"
        }
    }
    
    private var themeStyle: ThemeStyle = .default
    private var themeColors: NSDictionary?
    
    private override init() {
        super.init()
        if let style = UserDefaults.standard.object(forKey: kThemeStyle) as? Int {
            themeStyle = ThemeStyle(rawValue: style)!
        } else {
            UserDefaults.standard.set(themeStyle.rawValue, forKey: kThemeStyle)
            UserDefaults.standard.synchronize()
        }
        
        themeColors = getThemeColors()
    }
    
    private func getThemeColors() -> NSDictionary? {
        
        let bundleName = themeBundleName
        
        guard let themeBundlePath = Bundle.path(forResource: bundleName, ofType: "bundle", inDirectory: Bundle.main.bundlePath) else {
            return nil
        }
        guard let themeBundle = Bundle(path: themeBundlePath) else {
            return nil
        }
        guard let path = themeBundle.path(forResource: "themeColor", ofType: "txt") else {
            return nil
        }
        
        let url = URL(fileURLWithPath: path)
        let data = try! Data(contentsOf: url)
        
        do {
            return try JSONSerialization.jsonObject(with: data, options: [JSONSerialization.ReadingOptions(rawValue: 0)]) as? NSDictionary
        } catch {
            return nil
        }

    }
    
    func updateThemeStyle(_ style: ThemeStyle) {
        if themeStyle.rawValue == style.rawValue {
            return
        }
        themeStyle = style
        UserDefaults.standard.set(style.rawValue, forKey: kThemeStyle)
        UserDefaults.standard.synchronize()
        themeColors = getThemeColors()
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: kUpdateTheme), object: nil)
    }
    
    func themeColor(_ colorName: String) -> Int {
        guard let hexString = themeColors?.value(forKey: colorName) as? String else {
            assert(true, "Invalid color key")
            return 0
        }
        let colorValue = Int(strtoul(hexString, nil, 16))
        return colorValue
    }
}

Extensions.swift 内容:

import UIKit

extension UIImage {
    
    static func loadImage(_ imageName: String) -> UIImage? {
        return loadImage(imageName, style: ThemeManager.instance.style)
    }
    
    // 如果明确资源不受 theme 变化而变化,使用这个接口会更快
    static func loadDefaultImage(_ imageName: String) -> UIImage? {
        return loadImage(imageName, style: .default)
    }
    
    static func loadImage(_ imageName: String, style: ThemeStyle) -> UIImage? {
        
        if imageName.isEmpty || imageName.characters.count == 0 {
            return nil
        }
        
        var bundleName = "defaultTheme"
        switch style {
        case .black:
            bundleName =  "blackTheme"
        default:
            bundleName = "defaultTheme"
        }

        guard let themeBundlePath = Bundle.path(forResource: bundleName, ofType: "bundle", inDirectory: Bundle.main.bundlePath) else {
            return nil
        }
        guard let themeBundle = Bundle(path: themeBundlePath) else {
            return nil
        }
        
        var isImageUnder3x = false
        var nameAndType = imageName.components(separatedBy: ".")
        var name = nameAndType.first!
        let type = nameAndType.count > 1 ? nameAndType[1] : "png"
        var imagePath  =  themeBundle.path(forResource: "image/" + name, ofType: type)
        let nameLength = name.characters.count
        
        if imagePath == nil && name.hasSuffix("@2x") && nameLength > 3 {
            let index = name.index(name.endIndex, offsetBy: -3)
            name = name.substring(with: Range<String.Index>(name.startIndex ..< index))
        }
        
        if imagePath == nil && !name.hasSuffix("@2x") {
            let name2x = name + "@2x";
            imagePath = themeBundle.path(forResource: "image/" + name2x, ofType: type)
            if imagePath == nil && !name.hasSuffix("3x") {
                let name3x = name + "@3x"
                imagePath = themeBundle.path(forResource: "image/" + name3x, ofType: type)
                isImageUnder3x = true
            }
        }
        
        var image: UIImage?
        if let imagePath = imagePath {
            image = UIImage(contentsOfFile: imagePath)
        } else {
            // 如果当前 bundle 里面不存在这张图片的路径,那就去默认的 bundle 里面找,
            // 为什么要这样做呢,因为部分资源在不同 theme 中是一样的,就不需要导入重复的资源,使应用包的大小变大
            image = UIImage.loadDefaultImage(imageName)
        }
        if #available(iOS 8, *) {
            return image
        }
        if !isImageUnder3x {
            return image
        }
        return image?.scaledImageFrom3x()
    }
    
    private func scaledImageFrom3x() -> UIImage {
        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 = CGRect.zero
        scaleRect.size.width = scaleWidth
        scaleRect.size.height = scaleHeight
        UIGraphicsBeginImageContextWithOptions(scaleRect.size, false, UIScreen.main.scale)
        draw(in: scaleRect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return newImage
    }
}

extension UIColor {

    convenience init(red: Int, green: Int, blue: Int) {
        assert(red >= 0 && red <= 255, "Invalid red component")
        assert(green >= 0 && green <= 255, "Invalid green component")
        assert(blue >= 0 && blue <= 255, "Invalid blue component")
        
        self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
    }
    
    convenience init(_ colorName: String) {
        let  netHex = ThemeManager.instance.themeColor(colorName)
        self.init(red:(netHex >> 16) & 0xff, green:(netHex >> 8) & 0xff, blue:netHex & 0xff)
    }
    
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,583评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,669评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,684评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,682评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,533评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,396评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,814评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,458评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,745评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,789评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,565评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,410评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,828评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,050评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,342评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,793评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,010评论 2 337

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,957评论 4 60
  • 方法一:设置git缓存密码 打开credential helper以便Git在一段时间内缓存你的账号密码: 默认保...
    stanf1l阅读 480评论 0 0
  • 炖啊炖啊炖。灵感来自《最孤独的冰箱和有故事的远方》(推荐)。 1.鸡汤那么多 早先由于闲暇时特别喜欢逛书店,我被动...
    陆壹壹阅读 229评论 1 2
  • 使用图像 图形处理软件分为: 矢量:也称面向对象的图形应用程序,文件所占的存储空间更小 位图:(GIF,JPEG)...
    钎探穗阅读 662评论 0 0
  • 春梦一刻值千金,当中的旖旎和缠绵让人在梦醒时分回味无穷,但过后却因为社会道德和良知变得内疚或者愧恨,可能因为梦见缠...
    Psychonline阅读 478评论 0 2