iOS 高效靈活地配置可復用視圖組件的主題

 

本文首發於 Ficow Shen’s Blog,原文地址: iOS 高效靈活地配置可復用視圖組件的主題

 

內容概覽

  • 前言
  • 如何配置主題?
  • 如何更高效地配置主題?
  • 面向協議/接口的方案

 
 

前言

 

在開發可視化應用的過程中,配置控件的樣式是最常見的工作內容。請問讀者是否遇到過這樣的需求:在多個項目中復用多種可視化控件,而且這些控件可以配置顏色、字體等可視化元素?

本文主要針對控件數量較大,而且需要配置的控件屬性較多的這種需求對主題配置方案進行探索,希望能夠給讀者帶來些許啟發。

 
 

如何配置主題?

 

大家最熟悉的方式就是給控件添加 控制樣式的屬性,然後 讓調用方去設置控件的樣式屬性 以實現自定義樣式的需求。

public final class ReusableComponent: UIView {
    private let titleLabel = UILabel()

    // 暴露一個顏色配置屬性,供調用方更改文本顏色
    public var titleColor: UIColor = .darkGray {
        didSet {
            titleLabel.textColor = titleColor
        }
    }
}

let component = ReusableComponent()
component.titleColor = .red

在控件數量較少、樣式屬性也較少的情況下,直接設置樣式屬性的方式是非常簡單高效的。

 

如果控件數量大樣式屬性較多使用範圍廣甚至需要在多個項目中使用時,如何實現簡單高效的樣式配置呢?請看以下示例代碼,並思考這個問題。

public final class ReusableComponent: UIView {
    private let titleLabel = UILabel()
    private let descriptionLabel = UILabel()
    private let confirmButton = UIButton()
    
    public var titleColor: UIColor = .darkGray {
        didSet {
            titleLabel.textColor = titleColor
        }
    }
	
    public var titleFont: UIFont = .systemFont(ofSize: 20) {
        didSet {
            titleLabel.font = titleFont
        }
    }
    
    public var descriptionColor: UIColor = .gray {
        didSet {
            descriptionLabel.textColor = descriptionColor
        }
    }
	
    public var descriptionFont: UIFont = .systemFont(ofSize: 14) {
        didSet {
            descriptionLabel.font = descriptionFont
        }
    }
    
    public var confirmTitleColor: UIColor = .darkGray {
        didSet {
            confirmButton.setTitleColor(confirmTitleColor, for: .normal)
        }
    }
	
    public var confirmTitleFont: UIFont = .systemFont(ofSize: 16) {
        didSet {
            confirmButton.titleLabel?.font = confirmTitleFont
        }
    }
}

let component = ReusableComponent()
component.titleColor = .black
component.titleFont = .systemFont(ofSize: 19)
component.descriptionColor = .lightGray
component.descriptionFont = .systemFont(ofSize: 13)
component.confirmTitleColor = .black
component.confirmTitleFont = .systemFont(ofSize: 15)

請看上面的示例代碼,這裡僅僅配置幾個樣式屬性就已經需要寫很多行代碼。如果需要大面積修改這種配置,我們很容易就漏掉某個屬性。怎麼辦?

 
 
 

public final class ReusableComponent: UIView {
    
    public struct Theme {
        let titleColor: UIColor
        let titleFont: UIFont
        let descriptionColor: UIColor
        let descriptionFont: UIFont
        let confirmTitleColor: UIColor
        let confirmTitleFont: UIFont
    }
    
    public static let defaultTheme = Theme(titleColor: .darkGray,
                                           titleFont: .systemFont(ofSize: 20),
                                           descriptionColor: .gray,
                                           descriptionFont: .systemFont(ofSize: 14),
                                           confirmTitleColor: .darkGray,
                                           confirmTitleFont: .systemFont(ofSize: 16))
    
    public var theme: Theme = defaultTheme {
        didSet {
            titleLabel.textColor = theme.titleColor
            titleLabel.font = theme.titleFont
            descriptionLabel.textColor = theme.descriptionColor
            descriptionLabel.font = theme.descriptionFont
            confirmButton.setTitleColor(theme.confirmTitleColor, for: .normal)
            confirmButton.titleLabel?.font = theme.confirmTitleFont
        }
    }
    
    private let titleLabel = UILabel()
    private let descriptionLabel = UILabel()
    private let confirmButton = UIButton()
}

let component = ReusableComponent()
let theme = ReusableComponent.Theme(titleColor: .black,
                                    titleFont: .systemFont(ofSize: 19),
                                    descriptionColor: .lightGray,
                                    descriptionFont: .systemFont(ofSize: 13),
                                    confirmTitleColor: .black,
                                    confirmTitleFont: .systemFont(ofSize: 15))
component.theme = theme

為控件定義一個主題類型並定義一個主題屬性,調用方不用擔心漏掉某個配置項。而且,調用方甚至可以定義一個全局的主題對象,在需要使用的時候直接賦值即可。

但是,我們依然要為每一個控件實例進行樣式配置。您可以設想一下,如果您需要對 ReusableComponent1, ReusableComponent2, … , ReusableComponentN 這些控件進行主題配置,您就需要定義超級多的主題類型。 而且,調用方需要確切知曉控件裏面的主題類型,然後在配置主題的時候去初始化一個主題類型的實例並傳給控件實例。

那麼,有沒有什麼辦法更簡單、靈活、高效呢?

 
 

如何更高效地配置主題?

 

每次用到控件都去指定主題的方式極其低效,我們先要想方設法優化這個問題。How?

public final class ReusableComponent: UIView {
    
    // ...
    
    public static var theme: Theme = defaultTheme
    
    public var theme: Theme = ReusableComponent.theme {
        didSet {
            titleLabel.textColor = theme.titleColor
            titleLabel.font = theme.titleFont
            descriptionLabel.textColor = theme.descriptionColor
            descriptionLabel.font = theme.descriptionFont
            confirmButton.setTitleColor(theme.confirmTitleColor, for: .normal)
            confirmButton.titleLabel?.font = theme.confirmTitleFont
        }
    }
    
    // ...
}

ReusableComponent.theme = ReusableComponent.Theme(titleColor: .black,
                                                  titleFont: .systemFont(ofSize: 19),
                                                  descriptionColor: .lightGray,
                                                  descriptionFont: .systemFont(ofSize: 13),
                                                  confirmTitleColor: .black,
                                                  confirmTitleFont: .systemFont(ofSize: 15))
let component = ReusableComponent()
print(component.theme)

一般來說,應用內使用的控件的主題風格都是統一的。所以,更多的實際場景是我們需要對控件類型進行統一的樣式配置。

ReusableComponent類型上增加一個靜態變量,這樣只需要在使用控件前,對控件進行統一配置即可。如果稍後需要對某個控件實例進行定製,只需要修改控件實例的theme屬性即可。這解決了配置效率低下的問題。

如果控件是定義在一個公用庫裏面,有多個項目需要用到庫中的控件,那麼直接暴露控件內部定義的主題類型給調用方將是一件非常不妙的事情。我們應該儘可能少地暴露公用庫中的內容,以達到高度的封裝效果。這樣,以後可能會發生的內部變動就不擔心會受到下游調用方的約束。

那麼,怎麼封裝呢?

 
 

面向協議/接口的方案

 

如果您長期使用Swift開發語言,面向協議編程的概念您一定聽說過。靈魂拷問又來了,究竟怎樣的編程方式才是面向協議編程呢?

public protocol ReusableComponentTheme {
    var titleColor: UIColor { get }
    var titleFont: UIFont { get }
    var descriptionColor: UIColor { get }
    var descriptionFont: UIFont { get }
    var confirmTitleColor: UIColor { get }
    var confirmTitleFont: UIFont { get }
}

public final class ReusableComponent: UIView {
    
    struct Theme: ReusableComponentTheme {
        var titleColor: UIColor { .darkGray }
        var titleFont: UIFont { .systemFont(ofSize: 20) }
        var descriptionColor: UIColor { .gray }
        var descriptionFont: UIFont { .systemFont(ofSize: 14) }
        var confirmTitleColor: UIColor { .darkGray }
        var confirmTitleFont: UIFont { .systemFont(ofSize: 16) }
    }
    
    public static var theme: ReusableComponentTheme = Theme()
    
    public var theme: ReusableComponentTheme = ReusableComponent.theme {
        didSet {
            titleLabel.textColor = theme.titleColor
            titleLabel.font = theme.titleFont
            descriptionLabel.textColor = theme.descriptionColor
            descriptionLabel.font = theme.descriptionFont
            confirmButton.setTitleColor(theme.confirmTitleColor, for: .normal)
            confirmButton.titleLabel?.font = theme.confirmTitleFont
        }
    }
    
    private let titleLabel = UILabel()
    private let descriptionLabel = UILabel()
    private let confirmButton = UIButton()
}

struct CustomReusableComponentTheme: ReusableComponentTheme {
    var titleColor: UIColor { .black }
    var titleFont: UIFont { .systemFont(ofSize: 19) }
    var descriptionColor: UIColor { .lightGray }
    var descriptionFont: UIFont { .systemFont(ofSize: 13) }
    var confirmTitleColor: UIColor { .black }
    var confirmTitleFont: UIFont { .systemFont(ofSize: 15) }
}

ReusableComponent.theme = CustomReusableComponentTheme()
let component = ReusableComponent()
print(component.theme)

針對控件的主題定義一個協議,然後讓主題類型去遵循這個協議。調用方不再知曉控件內部的主題類型,控件內部後續的變動不會導致調用方的編譯錯誤,這樣也就實現了調用鏈上下游的解耦。

如果以後需要對控件內部的樣式進行調整,您可以定義新的協議來滿足新的需求,而不是去修改舊的協議。這種變更方式與後端接口支持不同版本類似,也比較靈活。

 

以上就是本文的全部內容,希望對您有所啟發!

 

Tags: