捋捋思路
要實現怎么樣的效果?
Demo: https://github.com/iJudson/RxReadingMode
應用場合
一個應用,為滿足用戶多變的私欲,擁有多個主題風格、不同的閱讀模式(日間/夜間),這種情況喜聞樂見,像某易某音樂,就同時擁有個性換膚和夜間模式的功能
但即使一個應用擁有再多的風格,本質無非是事先準備好這些不同風格的「顏色樣式」、「圖片樣式」、「遮罩樣式」而已,當你點擊調整風格的按鈕,所有需要替換風格的界面收到通知之后,拿到事先準備好的樣式進行替換即可。
實現方式
如上所述,我們需要監聽者去監聽「模式切換的按鈕」,當接收到用戶的點擊時,通知所有界面,啟用另一套主題風格。因而,我們需要使用到監聽模式,由于涉及到多個界面不同層次下的監聽,我們自然而然會排除 「Delegate」 和 「Block」這兩種監聽模式,那么在非 Reactive 下,我們也只能使用通知中心這種監聽模式了,但其實如果使用 Reactive ,會簡單得特別多,代碼質量也會高很多,而且當一個應用擁有幾十個界面時,我們就不需要無止境的去移除監聽者,也不需要擔心因為某個界面忘了移除監聽者而帶來的崩潰問題,Reactive 中有一個自動移除監聽者的功能,會幫我們處理好這些事情。
NotificationCenter (通知中心)
作為非 Reactive 下的唯一實現方式,雖然著實會比 Reactive 的使用繁瑣很多,但是我們還是可以讓它盡可能的優雅的,由于這種實現方式并不是我這篇文章的主題,所以我只特別簡單的談談我的實現思路:
定義通知總中心管理器 「NotificationCenterManager」
應用中所有的通知的添加和移除都可以通過該總中心去管理定義顏色樣式配置中心「ThemeStyleConfigs」
主要是用于管理不同風格下的所有主體樣式為所需控件類的 Extension 綁定設值屬性
通過運行時,為 UIView、UIImageView、UIButton、UILabel 的 Extension,添加綁定一個對外的顏色屬性,目的是外部僅需要為該屬性的 set 方法設置,即可為相應的控件添加監聽者,當總得通知中心告訴該控件的監聽者需要置換另一套顏色樣式,即通過樣式配置中心拿到該控件所需樣式進行替換
RxSwift
使用 CocoaPod 將第三方框架 RxSwift 、RxCoCoa 引進項目中
pod 'RxSwift', '3.6.1'
pod 'RxCocoa', '3.6.1'
pod 'RxDataSources', '1.0.4' // 如果界面上有 UICollectionView/UIScrollView ,則需引入該第三方
詳情請見 Reactive 地址:https://github.com/ReactiveX/RxSwift樣式管理配置類
主要是用于管理項目中所有不同風格的樣式,再多的樣式,外部都不去干涉,而我們只需要在該類中去配置即可-
樣式風格獲取類
顧名思義,該類的是用于輸出某種具體的樣式。而要輸出某種樣式,那么我們的做法是,首先為該類輸入不同的風格樣式,然而再根據要求,輸出其中的一種我們需要的樣式 。
不難發現- 在該類中,我們需要添加一個監聽者,告訴我們:項目當前需要哪種樣式(如:是日間的樣式,還是夜間的樣式)
- 在該類,我們也需要提供一個可被觀察的樣式屬性,外部觀察并拿到該樣式屬性為控件賦具體樣式值
樣式調節器
主要樣式調節的開關,控制樣式的輸入,當需要調節模式變化時,我們只需要控制不同的樣式輸入,即可達到模式轉換的目的
說到這里,你可能不知所云吧?不過沒事,我們先進入實戰,往后再回頭理解以上內容,屆時,你對這個模式設計的理解應該也會更加深刻。
使用 RxSwift 構建閱讀模式(日間/夜間)
我們以構建一個擁有兩種風格的閱讀模式為例,而多種風格的應用皮膚,與此基本是一模一樣的,無非是多準備一套風格罷了。
一些再熟悉不過的基本配置
正常來說,MVVM 本就為 RxSwift 而生,項目的設計模式最好選擇 MVVM 模式去設計,但是這里的主體是閱讀模式的構建,為了節省大家的理解時間,我們可以稍微簡單粗暴些
- 創建 RxReadingMode 項目
- 在 該項目的 Main.storyBoard 上添加所有需要添加夜間模式的控件,并連線到代碼 ViewController 中
- 導入 Reactive 框架
pod 'RxSwift', '3.6.1'
pod 'RxCocoa', '3.6.1' - 在 ViewController 中添加頭文件
import RxSwift
import RxCocoa
完成以上步驟之后,我們運行下項目,確保這一系列操作下來是沒有任何問題的:
Good!沒有什么問題!到這里,我們已經完成了 1/10 了。而需要添加夜間模式的控件我們已經準備好了,接下來是否為這些控件準備不同模式下的樣式呢?
樣式管理配置類
我們需要準備兩套樣式:日間模式和夜間模式。為了方便管理,我們定義樣式管理類 ThemeStyleConfigs,再在其中通過結構體定義日間和夜間這兩套樣式:
/// 夜間模式下的樣式配置
struct NightTime {
// 主背景顏色
static let primaryBackgroundColor = UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1.0)
// 大標題文本顏色樣式
static let titleTextColor = UIColor(red: 191/255.0, green: 191/255.0, blue: 191/255.0, alpha: 1.0)
// 小標題文本顏色樣式
static let detailLabelTextColor = UIColor(red: 140/255.0, green: 140/255.0, blue: 140/255.0, alpha: 1.0)
// 返回按鈕圖片樣式
static let backButtomImage = UIImage(named: "night_BackArrow@24x24")
}
/// 日間模式下的樣式配置 (與以上對應)
struct DayTime {
static let primaryBackgroundColor = UIColor.white
static let titleTextColor = UIColor(red: 63/255.0, green: 63/255.0, blue: 63/255.0, alpha: 1.0)
static let detailLabelTextColor = UIColor(red: 101/255.0, green: 106/255.0, blue: 113/255.0, alpha: 1.0)
static let backButtomImage = UIImage(named: "day_BackArrow@24x24")
}
而當我們想拿某個模式下的樣式,即可通過以下方式:
// 如:拿「日間模式」下的主背景顏色樣式
let dayTimeColor = ThemeStyleConfigs.DayTime.primaryBackgroundColor
基本的準備工作到這里就結束了,很無聊?別急,我們要開始一些不一樣的工作了。
ReadingModeAdjuster - 閱讀體驗調節器
閱讀體驗調節器,即開啟不同閱讀模式的總開關,我們遵循封裝思想,將一些復雜的邏輯封裝在類內,保證外部的調用代碼可讀性盡可能的高,而且調用簡單;
- 定義被外部監聽的閱讀模式屬性,其主要用于被混和風格的樣式類(下面會講)監聽,為方便使用,這里定義為類屬性
// 默認值為日間模式
static var readingMode = Variable<ReadingMode>(.dayTime)
- 前面說過,這里是閱讀模式的總開關,即當點擊控制器的模式切換開關時,我需要去修改 1 中閱讀模式的值,當然我們可以直接拿到 1 中的屬性去賦值,但是這樣子就將沒必要對控制器開放的 Observable 屬性向它開放了,這并不是我們所希望的,那么我們該如何處理呢?我們通過一個對外的類方法:
// 調用該方法,即可修改 readingMode 屬性并引起一連串的連鎖反應
static func updateStatus(readingMode: ReadingMode = .dayTime) {
self.readingMode.value = readingMode
}
到這里,完成了該類的配置,但還是總感覺少了點什么?這個類就只有這么點料?我們就這么冷落一個熱情的 Variable 屬性?
MixedStyle - 混合風格取樣類
正如前面所說,我們構建該類的目的是,向該類輸入一些不同的風格樣式,該類就自動輸出我們所需要的樣式,跟自動售貨機一樣。聽起來很復雜,但是其實很簡單。
誒,但是風格類型不是有很多種?我們可能需要修改圖片的背景顏色(UIColor 類型),又可能需要修改 UILabel 的字體大小(CGFloat 類型)。那....?我們這里可以使用泛型類,該類的類型會在輸入一個值的時候確定。
而實際上,這個類需要做的就只有一件事情,就是輸出一個具體的風格樣式,但是輸入這么多種風格樣式,我們到底要輸出哪種樣式呢?這當然就取決于「上一個操作」被冷落的屬性 readingMode,在該類中,我們去監聽該屬性的變化,怎么樣的閱讀模式就輸出怎么樣的風格樣式。
- 定義混合風格類
struct MixedStyle<T> {
/// 混合風格類屬性
/// 混合風格類初始化構造器的配置
}
- 混合風格類屬性的定義
/// 混合風格類屬性
var dayTimeStyle: T // 日間模式樣式輸入
var nightTimeStyle: T // 夜間間模式樣式輸入
// outPut
var presentedStyle: Driver<T> // 外部輸出屬性,即呈現給外部的模式樣式,因該值僅在純 UI 下使用,故定義為 Driver
fileprivate let disposeBag = DisposeBag() // 監聽者自動銷毀器
- 混合風格類初始化構造器的配置
/// 混合風格類初始化構造器的配置
init(dayTime dayStyle: T, nightTime nightStyle: T) {
self.dayTimeStyle = dayStyle
self.nightTimeStyle = nightStyle
// 默認日間模式 監聽閱讀調節器的閱讀模式屬性,當該屬性發生變化,即更新 presentedStyle 的值
presentedStyle = ReadingModeAdjuster.readingMode.asObservable().flatMapLatest { (readMode) -> Observable<T> in
switch readMode {
case .dayTime:
return Observable.just(dayStyle)
case .nightTime:
return Observable.just(nightStyle)
}
}
.asDriver(onErrorJustReturn: dayStyle)
}
到這一步,我們就完全把一些配置類和管理類準備好了,那么接下來就到了使用這些類的環節。請移步到控制器類
ViewController-控制器中調用配置管理類
有了前面的配置,其實我們在控制器的使用就變得十分簡單,我們需要做的只有「2件事情」
- 將兩套樣式(日間/夜間)輸入到 MixedStyle 類中,并為該類的輸出樣式(presentedStyle)添加監聽者,當該輸出樣式發生變化,監聽者拿到這個變化值,進行 UI 屬性綁定操作
// 輸入兩套風格樣式到 MixedStyle 類中
let mixedViewColors = MixedStyle(dayTime: ThemeStyleConfigs.DayTime.primaryBackgroundColor, nightTime: ThemeStyleConfigs.NightTime.primaryBackgroundColor)
// self.view 的背景顏色去監聽 MixedStyle 類的呈現風格并進行背景顏色的屬性綁定
mixedViewColors.presentedStyle.drive(self.view.rx.backgroundColor).disposed(by: disposeBag)
let mixedImages = MixedStyle(dayTime: ThemeStyleConfigs.DayTime.backButtomImage, nightTime: ThemeStyleConfigs.NightTime.backButtomImage)
mixedImages.presentedStyle.drive(self.backButton.rx.normalImage).disposed(by: disposeBag)
let mixedTextColors = MixedStyle(dayTime: ThemeStyleConfigs.DayTime.titleTextColor, nightTime: ThemeStyleConfigs.NightTime.titleTextColor)
mixedTextColors.presentedStyle.drive(self.modeTitleLabel.rx.textColor).disposed(by: disposeBag)
mixedTextColors.presentedStyle.drive(self.modeToggleLabel.rx.textColor).disposed(by: disposeBag)
mixedTextColors.presentedStyle.drive(self.themeStyleLabel.rx.textColor).disposed(by: disposeBag)
let mixedDetailTextColors = MixedStyle(dayTime: ThemeStyleConfigs.DayTime.detailLabelTextColor, nightTime: ThemeStyleConfigs.NightTime.detailLabelTextColor)
mixedDetailTextColors.presentedStyle.drive(self.modeDetailLabel.rx.textColor).disposed(by: disposeBag)
mixedTextColors.presentedStyle.drive(self.checkButton.rx.textColor).disposed(by: disposeBag)
在這里,有個小插曲:我自定義了一些 UI 控件屬性監聽者,熟悉 RxSwift 的人可能可以很輕易看出,因為 RxSwift 這個第三方中并沒有 UIView 背景顏色監聽者,如果我們想一些 UI 屬性成為監聽者,我們不得不去自定義監聽者,RxSwift 這個功能確定十分好用,詳情可見 Reactive+Extension 類。
///自定義 UIView 背景顏色監聽者
extension Reactive where Base: UIView {
var backgroundColor: UIBindingObserver<Base, UIColor> {
return UIBindingObserver(UIElement: base) { view, color in
view.backgroundColor = color
}
}
}
- 監聽 UISwitch 切換開關的操作,當更新 UISwitch 開關的狀態時,我們相應的同步到 ReadingModeAdjuster 的 readingMode 屬性,還記得之前所說的類方法?
// 當開關打開的時候 開啟夜間模式 否則開啟日間模式
ReadingModeAdjuster.updateStatus(readingMode: sender.isOn ? .nightTime : .dayTime)
這樣子去使用該類,代碼的可讀性是不是高了很多,而且也很方便使用?
到這里,我們閱讀模式的 Demo 也就構建完成了,效果圖可見文首,而其 Demo : https://github.com/iJudson/RxReadingMode
其實,細心的朋友可能發現圖片還沒添加夜間模式的樣式,這個后續功能大家可以試著實現下,思考如何實現才為「最優」。
希望大家在實現的過程中也有所收獲...