iOS 高效靈活地配置可複用檢視元件的主題
阿新 • • 發佈:2020-07-29
本文首發於 [Ficow Shen's Blog](https://ficow.cn),原文地址: [iOS 高效靈活地配置可複用檢視元件的主題](https://blog.ficow.cn/post/19)。
## 內容概覽
- 前言
- 如何配置主題?
- 如何更高效地配置主題?
- 面向協議/介面的方案
## 前言
在開發視覺化應用的過程中,`配置控制元件的樣式`是最常見的工作內容。請問讀者是否遇到過這樣的需求:在`多個專案中複用`多種視覺化控制元件,而且這些控制元件可以`配置顏色、字型`等視覺化元素?
本文主要針對`控制元件數量較大`,而且`需要配置的控制元件屬性較多`的這種需求對主題配置方案進行探索,希望能夠給讀者帶來些許啟發。
## 如何配置主題?
大家最熟悉的方式就是給控制元件新增 `控制樣式的屬性`,然後 `讓呼叫方去設定控制元件的樣式屬性` 以實現自定義樣式的需求。
``` swift
public final class ReusableComponent: UIView {
private let titleLabel = UILabel()
// 暴露一個顏色配置屬性,供呼叫方更改文字顏色
public var titleColor: UIColor = .darkGray {
didSet {
titleLabel.textColor = titleColor
}
}
}
let component = ReusableComponent()
component.titleColor = .red
```
在控制元件數量較少、樣式屬性也較少的情況下,直接設定樣式屬性的方式是非常簡單高效的。
如果`控制元件數量大`、`樣式屬性較多`、`使用範圍廣甚至需要在多個專案中使用`時,如何實現簡單高效的樣式配置呢?請看以下示例程式碼,並思考這個問題。
``` swift
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)
```
請看上面的示例程式碼,這裡`僅僅配置幾個樣式屬性`就已經需要`寫很多行程式碼`。如果需要大面積修改這種配置,我們`很容易就漏掉某個屬性`。怎麼辦?
``` swift
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?
``` swift
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開發語言,`面向協議程式設計`的概念您一定聽說過。靈魂拷問又來了,究竟怎樣的程式設計方式才是面向協議程式設計呢?
``` 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)
```
針對控制元件的主題定義一個協議,然後讓主題型別去遵循這個協議。呼叫方不再知曉控制元件內部的主題型別,控制元件內部後續的變動不會導致呼叫方的編譯錯誤,這樣也就實現了呼叫鏈上下游的解耦。
如果以後需要對控制元件內部的樣式進行調整,您可以定義新的協議來滿足新的需求,而不是去修改舊的協議。這種變更方式與後端介面支援不同版本類似,也比較靈活。
以上就是本文的全部內容,希望對您有所啟發!