主题切换框架的设计

2020-08-05  本文已影响0人  啧啧同学

背景

现在很多App都有“换肤”功能,产品和UI的想法一天一个样,所以设计一套简单、易调用、易维护的“换肤”框架尤其重要

先查看框架的实际调用🌰

// 切换皮肤的方法调用

    func change(to value: CCBThemeType) {
        if value == .light {
            return YFThemeManager.shared.switchTheme(CCBLightTheme(), animated: true)
        } else {
            return YFThemeManager.shared.switchTheme(CCBDarkBlueTheme(), animated: true)
        }
    }

某个支持换肤的View

class FCAlertView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupSubViews()
    }

     required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupSubViews() {
        .....
        //切换皮肤时, themeConfig的block会被自动触发,并通过切换后的主题的颜色值来重新渲染
        themeConfig { (v, t) in
            guard let view = v as? FCAlertView, let theme = t as? FCThemeColor else { return }
           
            view.cancelBtn.setTitleColor(theme.textColorTitle, for: .normal)
            view.okBtn.setTitleColor(theme.brandColor, for: .normal)
        }
    }

    //MARK: - Property
    lazy var cancelBtn: UIButton = {
        let btn = UIButton()
        btn.titleLabel?.font = UIFont.regular(18.0)
        btn.setTitle("取消", for: .normal)
        btn.addTarget(self, action: #selector(onCancelBtnClickEvent(_:)), for: .touchUpInside)
        return btn
    }()
    
    lazy var okBtn: UIButton = {
        let btn = UIButton()
        btn.titleLabel?.font = UIFont.regular(18.0)
        btn.setTitle("确定", for: .normal)
        btn.addTarget(self, action: #selector(onOkBtnClickEvent(_:)), for: .touchUpInside)
        return btn
    }()
}

设计思路:
1.在主题切换时,所有支持主题切换的对象,触发themeConfig的Block.
2.themeConfig中block的设计.

1. 在主题切换时,触发所有的“主题对象”中的block思路;

使用NSHashTable而不用NSSet的考虑
1.NSSet(NSMutableSet)持有其元素的强引用,同时这些元素是使用hash值及isEqual:方法来做hash检测及判断是否相等的。
2.NSHashTable是可变的,它没有不可变版本。
3.它可以持有元素的弱引用,而且在对象被销毁后能正确地将其移除。而这一点在NSSet是做不到的。

2. block 的设计

block的设计,主要是传入参数的设计

根据以上思路:

1.通过协议,定义主题对象
// 主题对象
@objc
public protocol ThemeClass: class {}

// 可进行主题切换的对象
protocol Themeable: ThemeClass {
    var theme: ThemeProvider { get set }
}

2.定义configClosure:
public typealias ThemeConfigClosure = (ThemeClass?, ThemeProtocol) -> ()
3. 主题功能封装:ThemeProvider
public class ThemeProvider: NSObject {

    internal init(_ obj: ThemeClass) {
        self.obj = obj
    }

    internal func _config(_ configClosure: @escaping ThemeConfigClosure) {
         //以下操作
        //configClosure添加至themeConfigs
        // 更新YFThemeManager的主题对象哈希表
        //通过obj 及 当前的主题,主动触发configClosure
    }
 
    //MARK: property
    internal var themeConfigs: [ThemeConfigClosure] = []
    private weak var obj: ThemeClass?
}

4.主题对象的定义:

只有实现 此Themeable的对象才是能够进行主题切换的对象;这里定义所有的NSObject均为主题对象

extension NSObject: Themeable {
    struct AssociateKey {
        static var theme = "com.fc.theme.provider"
    }

    public var theme: ThemeProvider {
         get {
            if objc_getAssociatedObject(self, &AssociateKey.theme) == nil {
                self.theme = ThemeProvider(self)
            }
            
            return objc_getAssociatedObject(self, &AssociateKey.theme) as! ThemeProvider
         }
         
         set(new) {
             objc_setAssociatedObject(self, &AssociateKey.theme, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
         }
     }
}

extension NSObject {
    @objc
    func themeConfig(configClosure: @escaping ThemeConfigClosure) {
        self.theme._config(configClosure)
    }
}
5.app主题管理器:Manager,管理主题的所有操作,包括
// manager
@objcMembers
public class YFThemeManager: NSObject {
    // 单例: 一个APP只会有一个管理类
    public static let shared: YFThemeManager = YFThemeManager()
    
    /// 主题变更通知
    public static let themeUpdateNotification: String = "com.fc.theme.update.notification"
    
     ///加载主题
    public func install(_ theme: ThemeProtocol) {  ...  }
    //切换主题
    public func switchTheme(_ theme: ThemeProtocol, animated: Bool) { ... }
    
    /// 更新主题Bundle(删除、新增、修改)
    @objc
    public func update(bundle: YFThemeBundle, for theme: ThemeProtocol) { ... }
    ///获取主题图片
    @objc
    public class func themeImage(_ identifier: String, imageName: String, type: String?) -> UIImage? {  ... }
        
   ///遍历trackedHashTable,以当前主题,刷新所有Themeable对象
    @objc
    public func refreshUI(_ animated: Bool) {
        let refresh: () -> () = {
            self.trackedHashTable.allObjects.forEach { (obj) in
                if let themeObj = obj as? Themeable {
                    themeObj.theme.themeConfigs.forEach { [weak themeObj, weak self] (config) in
                        guard let strongSelf = self else { return }
                        
                        config(themeObj, strongSelf.currentTheme)
                    }
                } else { /**do nothing*/ }
            }
        }
        
        if animated {
            UIView.animate(withDuration: 0.3) {
                refresh()
            }
        } else {
            refresh()
        }
    }
    
    //MARK: cache
    private func storeThemeToLocal(_ theme: ThemeProtocol) {
        guard !theme.themeClassName.isEmpty else {
            return
        }
        
        YFStorage.shared.saveString(theme.themeClassName, key: themeCacheKey)
    }
    
    private func loadThemeFromLocal() -> ThemeProtocol? {
        guard let className = YFStorage.shared.getString(themeCacheKey) else {
            return nil
        }
        
        guard let themeClass = NSClassFromString(className) as? ThemeProtocol.Type else {
            return nil
        }
        
        return themeClass.init()
    }
    
    //MARK: property
    //当前主题
    public var currentTheme: ThemeProtocol = DummyTheme()
    //缓存的key
    private let themeCacheKey = "com.f c.theme.manager.cache.key"
   //存放主题对象的哈希表
    private lazy var trackedHashTable: NSHashTable<AnyObject> = {
        return NSHashTable<AnyObject>.init(options: .weakMemory)
    }()
    //专门开辟一个串行队列,保证增、删、改的所有操作都能全部进行
    private var themeQueue: DispatchQueue = DispatchQueue(label: "com.fc.YFThemeManagerQueue")    
}

数据管理

1. UI提供不同类型的颜色的对照表

不同的“皮肤”,设计会提供一套主题颜色对照表,比如 app的主题色、弹窗背景颜色、标题字体颜色、副标题背景色、 描述文案背景色、界面背景色 、actionSheet背景颜色等等, 不同的主题,对应控件颜色均不同;相同的控件,不同主题加载的图片可能也是不同的!此时设计童鞋会提供几套主题的图片及颜色集。问题是如何管理这些颜色集!?

注意到,控件与颜色是一一对应的,所以自然想到会用字典来存放。这里考虑到可读性和易维护的角度,采用Xcode的Plist文件来存放, 对应的Key代表对应的控件类型, Value对应主题配置的颜色

举个🌰:
比如紫色主题的Plist的 sourceCode:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>tabbarColor</key>
    <string>#383838</string>
    <key>sideMenuColor</key>
    <string>#2f2f2f</string>
    <key>dialogColor</key>
    <string>#333333</string>
    <key>indicatorColor</key>
    <string>#444444</string>
    <key>lineColor</key>
    <string>#262626</string>
    <key>backgroundColor</key>
    <string>#222222</string>
    <key>cardColor</key>
    <string>#888888</string>
    <key>navigationBarColor</key>
    <string>#292929</string>
    <key>textColorSubTitle</key>
    <string>#888888</string>
    <key>textColorTitle</key>
    <string>#FFFFFF</string>
    <key>brandColor</key>
    <string>#CE57D0</string>
    <key>cellItemColor</key>
    <string>#1a1a1a</string>
</dict>
</plist>

对应主题的图片则存放在bundle文件中,相同图片不同主题,名字需要相同

2.根据UI提供的颜色对照表,通过定义一个Protocol类,对主题进行约束,从而确保每个主题的颜色集均相同并且均能成功初始化。
protocol FCThemeColor: class {
  
    var cellItemColor: UIColor { get }
    // tabbar颜色
    var tabbarColor: UIColor { get }
    //侧边栏的颜色
    var sideMenuColor: UIColor { get }
    //弹窗背景色
    var dialogColor: UIColor { get }
    // 指示器背景色
    var indicatorColor: UIColor { get }
    // 分割线颜色
    var lineColor: UIColor { get }
    // 公共背景颜色
    var backgroundColor: UIColor { get }
    // 卡片颜色
    var cardColor: UIColor { get }
    // 导航栏背景颜色
    var navigationBarColor: UIColor { get }
    // 标题颜色
    var textColorTitle: UIColor { get }
    // 副标题颜色
    var textColorSubTitle: UIColor { get }
    //品牌主题颜色
    var brandColor: UIColor { get }
}
3.主题图片的存放

相同的控件,不同的主题下加载的图片可能不同,与颜色类似,也需要按照主题来存放不同的图片

Xcode自带的bundle可以很好的进行不同主题的区分,只需要根据不同的主题,创建不同的bundle,bundle中存放对应主题的图片即可(图片的命名需要相同),当获取主题图片时,仅需要通过 manager提供的方法,提供图片名称即可

4.定义一个主题的基类并且实现FCThemeColor协议
@objcMembers
class FCBaseTheme: NSObject, FCThemeColor {

    class func current() -> FCThemeColor {
          // 返回当前主题
    }
     //初始化
    // 这里使用第三方库SwiftyJSON来处理数据结构
    init(json: JSON) {
        let cellItem = json["cellItemColor"].stringValue
        let tabbar = json["tabbarColor"].stringValue
        .....
        
        cellItemColor = UIColor(from: cellItem)
        tabbarColor = UIColor(from: tabbar)
        ......
    }
    
    //MARK: -  FCThemeColor
    let cellItemColor: UIColor
    let tabbarColor: UIColor
    .....
}
5.不同的主题,调用对应的Plist进行数据初始化,如:

//MARK: - Purple theme color
@objcMembers
class FCPurpleTheme: FCBaseTheme, ThemeProtocol {
    
    required init() {
        
        let buddlePath = Bundle.main.path(forResource: "PurpleColorConfig", ofType: "plist") ?? ""
        
        let colorDict = NSDictionary(contentsOfFile: buddlePath) as? [String: Any] ?? [:]
        
        super.init(json: JSON(colorDict))
    }
}

class FCWhiteTheme: FCBaseTheme, ThemeProtocol {
    
    required init() {
        
        let buddlePath = Bundle.main.path(forResource: "WhiteColorConfig", ofType: "plist") ?? ""
        
        let colorDict = NSDictionary(contentsOfFile: buddlePath) as? [String: Any] ?? [:]
        
        super.init(json: JSON(colorDict))
    }
}
上一篇下一篇

猜你喜欢

热点阅读