?
本文首發(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ā)!
?