捋捋思路
要实现怎么样的效果?
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
其实,细心的朋友可能发现图片还没添加夜间模式的样式,这个后续功能大家可以试着实现下,思考如何实现才为「最优」。
希望大家在实现的过程中也有所收获...