使用 RxSwift 构建不同风格的阅读模式(附 Demo)

捋捋思路

要实现怎么样的效果?
TogglingReadingMode

Demohttps://github.com/iJudson/RxReadingMode

应用场合

一个应用,为满足用户多变的私欲,拥有多个主题风格、不同的阅读模式(日间/夜间),这种情况喜闻乐见,像某易某音乐,就同时拥有个性换肤和夜间模式的功能


某易某音乐-夜间模式-个性换肤.JPG

但即使一个应用拥有再多的风格,本质无非是事先准备好这些不同风格的「颜色样式」、「图片样式」、「遮罩样式」而已,当你点击调整风格的按钮,所有需要替换风格的界面收到通知之后,拿到事先准备好的样式进行替换即可。

实现方式

如上所述,我们需要监听者去监听「模式切换的按钮」,当接收到用户的点击时,通知所有界面,启用另一套主题风格。因而,我们需要使用到监听模式,由于涉及到多个界面不同层次下的监听,我们自然而然会排除 「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

  • 样式管理配置类
    主要是用于管理项目中所有不同风格的样式,再多的样式,外部都不去干涉,而我们只需要在该类中去配置即可

  • 样式风格获取类
    顾名思义,该类的是用于输出某种具体的样式。而要输出某种样式,那么我们的做法是,首先为该类输入不同的风格样式,然而再根据要求,输出其中的一种我们需要的样式 。
    不难发现

    1. 在该类中,我们需要添加一个监听者,告诉我们:项目当前需要哪种样式(如:是日间的样式,还是夜间的样式)
    2. 在该类,我们也需要提供一个可被观察的样式属性,外部观察并拿到该样式属性为控件赋具体样式值
  • 样式调节器
    主要样式调节的开关,控制样式的输入,当需要调节模式变化时,我们只需要控制不同的样式输入,即可达到模式转换的目的
    说到这里,你可能不知所云吧?不过没事,我们先进入实战,往后再回头理解以上内容,届时,你对这个模式设计的理解应该也会更加深刻。

使用 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 - 阅读体验调节器

阅读体验调节器,即开启不同阅读模式的总开关,我们遵循封装思想,将一些复杂的逻辑封装在类内,保证外部的调用代码可读性尽可能的高,而且调用简单;

  1. 定义被外部监听的阅读模式属性,其主要用于被混和风格的样式类(下面会讲)监听,为方便使用,这里定义为类属性
// 默认值为日间模式
 static var readingMode = Variable<ReadingMode>(.dayTime)
  1. 前面说过,这里是阅读模式的总开关,即当点击控制器的模式切换开关时,我需要去修改 1 中阅读模式的值,当然我们可以直接拿到 1 中的属性去赋值,但是这样子就将没必要对控制器开放的 Observable 属性向它开放了,这并不是我们所希望的,那么我们该如何处理呢?我们通过一个对外的类方法:
// 调用该方法,即可修改 readingMode 属性并引起一连串的连锁反应
static func updateStatus(readingMode: ReadingMode = .dayTime) {
    self.readingMode.value = readingMode
}

到这里,完成了该类的配置,但还是总感觉少了点什么?这个类就只有这么点料?我们就这么冷落一个热情的 Variable 属性?

MixedStyle - 混合风格取样类

正如前面所说,我们构建该类的目的是,向该类输入一些不同的风格样式,该类就自动输出我们所需要的样式,跟自动售货机一样。听起来很复杂,但是其实很简单。

诶,但是风格类型不是有很多种?我们可能需要修改图片的背景颜色(UIColor 类型),又可能需要修改 UILabel 的字体大小(CGFloat 类型)。那....?我们这里可以使用泛型类,该类的类型会在输入一个值的时候确定。

而实际上,这个类需要做的就只有一件事情,就是输出一个具体的风格样式,但是输入这么多种风格样式,我们到底要输出哪种样式呢?这当然就取决于「上一个操作」被冷落的属性 readingMode,在该类中,我们去监听该属性的变化,怎么样的阅读模式就输出怎么样的风格样式。

  1. 定义混合风格类
struct MixedStyle<T> { 
  /// 混合风格类属性

 /// 混合风格类初始化构造器的配置

}
  1. 混合风格类属性的定义
/// 混合风格类属性
var dayTimeStyle: T // 日间模式样式输入
var nightTimeStyle: T // 夜间间模式样式输入

// outPut
var presentedStyle: Driver<T> // 外部输出属性,即呈现给外部的模式样式,因该值仅在纯 UI 下使用,故定义为 Driver
fileprivate let disposeBag = DisposeBag() // 监听者自动销毁器

  1. 混合风格类初始化构造器的配置
 /// 混合风格类初始化构造器的配置
 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件事情」

  1. 将两套样式(日间/夜间)输入到 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
        }
    }
}
  1. 监听 UISwitch 切换开关的操作,当更新 UISwitch 开关的状态时,我们相应的同步到 ReadingModeAdjuster 的 readingMode 属性,还记得之前所说的类方法?
// 当开关打开的时候 开启夜间模式 否则开启日间模式
ReadingModeAdjuster.updateStatus(readingMode: sender.isOn ? .nightTime : .dayTime)

这样子去使用该类,代码的可读性是不是高了很多,而且也很方便使用?
到这里,我们阅读模式的 Demo 也就构建完成了,效果图可见文首,而其 Demohttps://github.com/iJudson/RxReadingMode

其实,细心的朋友可能发现图片还没添加夜间模式的样式,这个后续功能大家可以试着实现下,思考如何实现才为「最优」。

希望大家在实现的过程中也有所收获...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,102评论 4 62
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,654评论 18 139
  • 山里的夜特别冷,静静的躺在床上,听得到各种鸟叫虫鸣,还有风雨来过的脚步。陈信把眼睛睁着,反正漆黑一团,什么也看不见...
    如歌的行板紫雪阅读 397评论 1 1
  • 善良只需要一角钱 记得小时候去楼下超市跑腿买一袋七角钱的淀粉,结账时发现淀粉涨了一角钱。年少幼小的我惶然失措,不知...
    桃子王阅读 844评论 0 1
  • 难忘的仲夏的七月中旬,空气里还是弥漫着瓜果的飘香,踏上芬芳的泥土,采撷了一把空气中夏天的味道,才知道已经到达了我们...
    西河笑生阅读 319评论 0 3