首页 >Swift

再谈 Swift 换肤功能

2017-08-25 16:47 编辑: suiling 分类:Swift 来源:简书

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

首先,当然是先上 demo

接着就是效果图:

1.gif

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 看就可以了,这里就上一张目录图:

2.jpg

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(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)
    }
    
}

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

搜索CocoaChina微信公众号:CocoaChina
微信扫一扫
订阅每日移动开发及APP推广热点资讯
公众号:
CocoaChina
我要投稿   收藏文章
上一篇:对于Swift中Optional的新理解
下一篇:Swift3.0基础语法(一)——情人节特辑
我来说两句
发表评论
您还没有登录!请登录注册
所有评论(0

综合评论

相关帖子

sina weixin mail 回到顶部