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

?

本文首發(fā)于 Ficow Shen's Blog,原文地址: iOS 高效靈活地配置可復(fù)用視圖組件的主題。

?

內(nèi)容概覽

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

?
?

前言

?

在開發(fā)可視化應(yīng)用的過程中,配置控件的樣式是最常見的工作內(nèi)容。請(qǐng)問讀者是否遇到過這樣的需求:在多個(gè)項(xiàng)目中復(fù)用多種可視化控件,而且這些控件可以配置顏色、字體等可視化元素?

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

?
?

如何配置主題?

?

大家最熟悉的方式就是給控件添加 控制樣式的屬性,然后 讓調(diào)用方去設(shè)置控件的樣式屬性 以實(shí)現(xiàn)自定義樣式的需求。

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

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

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

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

?

如果控件數(shù)量大、樣式屬性較多、使用范圍廣甚至需要在多個(gè)項(xiàng)目中使用時(shí),如何實(shí)現(xiàn)簡單高效的樣式配置呢?請(qǐng)看以下示例代碼,并思考這個(gè)問題。

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)

請(qǐng)看上面的示例代碼,這里僅僅配置幾個(gè)樣式屬性就已經(jīng)需要寫很多行代碼。如果需要大面積修改這種配置,我們很容易就漏掉某個(gè)屬性。怎么辦?

?
?
?

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

為控件定義一個(gè)主題類型并定義一個(gè)主題屬性,調(diào)用方不用擔(dān)心漏掉某個(gè)配置項(xiàng)。而且,調(diào)用方甚至可以定義一個(gè)全局的主題對(duì)象,在需要使用的時(shí)候直接賦值即可。

但是,我們依然要為每一個(gè)控件實(shí)例進(jìn)行樣式配置。您可以設(shè)想一下,如果您需要對(duì) ReusableComponent1, ReusableComponent2, ... , ReusableComponentN 這些控件進(jìn)行主題配置,您就需要定義超級(jí)多的主題類型。 而且,調(diào)用方需要確切知曉控件里面的主題類型,然后在配置主題的時(shí)候去初始化一個(gè)主題類型的實(shí)例并傳給控件實(shí)例。

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

?
?

如何更高效地配置主題?

?

每次用到控件都去指定主題的方式極其低效,我們先要想方設(shè)法優(yōu)化這個(gè)問題。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)

一般來說,應(yīng)用內(nèi)使用的控件的主題風(fēng)格都是統(tǒng)一的。所以,更多的實(shí)際場(chǎng)景是我們需要對(duì)控件類型進(jìn)行統(tǒng)一的樣式配置。

ReusableComponent類型上增加一個(gè)靜態(tài)變量,這樣只需要在使用控件前,對(duì)控件進(jìn)行統(tǒng)一配置即可。如果稍后需要對(duì)某個(gè)控件實(shí)例進(jìn)行定制,只需要修改控件實(shí)例的theme屬性即可。這解決了配置效率低下的問題。

如果控件是定義在一個(gè)公用庫里面,有多個(gè)項(xiàng)目需要用到庫中的控件,那么直接暴露控件內(nèi)部定義的主題類型給調(diào)用方將是一件非常不妙的事情。我們應(yīng)該盡可能少地暴露公用庫中的內(nèi)容,以達(dá)到高度的封裝效果。這樣,以后可能會(huì)發(fā)生的內(nèi)部變動(dòng)就不擔(dān)心會(huì)受到下游調(diào)用方的約束。

那么,怎么封裝呢?

?
?

面向協(xié)議/接口的方案

?

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

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)

針對(duì)控件的主題定義一個(gè)協(xié)議,然后讓主題類型去遵循這個(gè)協(xié)議。調(diào)用方不再知曉控件內(nèi)部的主題類型,控件內(nèi)部后續(xù)的變動(dòng)不會(huì)導(dǎo)致調(diào)用方的編譯錯(cuò)誤,這樣也就實(shí)現(xiàn)了調(diào)用鏈上下游的解耦。

如果以后需要對(duì)控件內(nèi)部的樣式進(jìn)行調(diào)整,您可以定義新的協(xié)議來滿足新的需求,而不是去修改舊的協(xié)議。這種變更方式與后端接口支持不同版本類似,也比較靈活。

?

以上就是本文的全部內(nèi)容,希望對(duì)您有所啟發(fā)!

?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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