再谈 Swift 换肤功能

作者: YxxxHao | 来源:发表于2017-08-24 23:38 被阅读478次

    在之前我写的 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)
        }
        
    }
    

    相关文章

      网友评论

      本文标题:再谈 Swift 换肤功能

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