再談 Swift 換膚功能

在之前我寫的 iOS應(yīng)用主題(圖片,顏色)統(tǒng)一管理 一文中,曾介紹了 Swift 皮膚切換功能,但由于那時對 Swift 的理解不夠深,所以現(xiàn)在再看之前寫的那篇文章,感覺其中的實現(xiàn)很糟糕,所以今天再來談?wù)?Swift 的換膚功能。讀該文前,建議先讀下上述文章。

首先,當(dāng)然是先上 demo

接著就是效果圖:

theme.gif

實現(xiàn)

這個換膚功能的代碼量大概就在二百行左右,核心代碼就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")
    }
}

換膚其實就是一個監(jiān)聽者模式,一般情況下,涉及到換膚功能的,要么是在 UIViewController 中,要么就是在 UIView 中,這里先定義一個 ThemeProtocol 協(xié)議,然后通過協(xié)議的擴展來實現(xiàn) UIView 和 UIViewController 對換膚功能的監(jiān)聽或移除監(jiān)聽方法,但因為協(xié)議的擴展是 Swift 中僅有的,在 OC 中并不支持,所以不能在協(xié)議擴展中實現(xiàn) updateTheme 方法,這里通過擴展 UIView 和 UIViewController 來實現(xiàn) updateTheme 方法。

我們在 UIView 或 UIViewController 中實現(xiàn) ThemeProtocol 協(xié)議后, 我們就可以對換膚功能進行監(jiān)聽,其它沒有實現(xiàn) ThemeProtocol 協(xié)議的相關(guān) UIView 或 UIViewController 就不會受影響,實現(xiàn)如下:

class TestView: UIView, ThemeProtocol {

    override init(frame: CGRect) {
        super.init(frame: frame)
        // 添加監(jiān)聽
        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()
    }
}

在上篇文章中,為了實現(xiàn)對主題的監(jiān)聽,是通過實現(xiàn)一個基類來實現(xiàn)的,但是這也導(dǎo)致了耦合度奇高,可以移植性差,通過上述的方法,就可以很好地解決這個問題了。

核心內(nèi)容其實就是上面這些,剩下的內(nèi)容就和 iOS應(yīng)用主題(圖片,顏色)統(tǒng)一管理 這篇文章幾乎一樣了,就是實現(xiàn)一個 ThemeManager 類,通過切換 bundle 來對圖片和顏色資源進行管理,這里就不詳細說了,代碼也比較簡單,直接下載 demo 看就可以了,這里就上一張目錄圖:

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

附錄

ThemeManager.swift 內(nèi)容:

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 內(nèi)容:

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 {
            // 如果當(dāng)前 bundle 里面不存在這張圖片的路徑,那就去默認的 bundle 里面找,
            // 為什么要這樣做呢,因為部分資源在不同 theme 中是一樣的,就不需要導(dǎo)入重復(fù)的資源,使應(yīng)用包的大小變大
            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)
    }
    
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,077評論 4 61
  • 方法一:設(shè)置git緩存密碼 打開credential helper以便Git在一段時間內(nèi)緩存你的賬號密碼: 默認保...
    stanf1l閱讀 548評論 0 0
  • 燉啊燉啊燉。靈感來自《最孤獨的冰箱和有故事的遠方》(推薦)。 1.雞湯那么多 早先由于閑暇時特別喜歡逛書店,我被動...
    陸壹壹閱讀 300評論 1 2
  • 使用圖像 圖形處理軟件分為: 矢量:也稱面向?qū)ο蟮膱D形應(yīng)用程序,文件所占的存儲空間更小 位圖:(GIF,JPEG)...
    釬探穗閱讀 761評論 0 0
  • 春夢一刻值千金,當(dāng)中的旖旎和纏綿讓人在夢醒時分回味無窮,但過后卻因為社會道德和良知變得內(nèi)疚或者愧恨,可能因為夢見纏...
    Psychonline閱讀 555評論 0 2

友情鏈接更多精彩內(nèi)容