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

實現(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 看就可以了,這里就上一張目錄圖:

附錄
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)
}
}