前几天看到了一个非常有趣的 repo RxAutomaton (opens new window),我 clone 下来玩了一下,感觉非常有趣,演示了状态机在登录中的应用例子,应用中登录的逻辑如下。
而这一部分的逻辑可以用代码描述,而且很清晰。
/* Input | fromState => toState | Effect */
/* --------------------------------------------*/
.Login | .LoggedOut => .LoggingIn | loginOKProducer,
.LoginOK | .LoggingIn => .LoggedIn | .empty(),
.Logout | .LoggedIn => .LoggingOut | logoutOKProducer,
.LogoutOK | .LoggingOut => .LoggedOut | .empty(),
.ForceLogout | canForceLogout => .LoggingOut | forceLogoutOKProducer
对于一个理解状态机不深的我,本文就不再赘述状态机的概念了,这里有一篇很有趣的文章,介绍了为什么有时候我们要考虑使用状态机,使用状态机的好处 (opens new window)。
其实看到 repo 中的登录例子,我惊了个呆,代码竟然还可以这样下,这样的代码写法相比之前的代码会更清晰很多,毕竟我们已经将代码逻辑流程都写成了上面的样子,这很容易理解。比如 .Login | .LoggedOut => .LoggingIn | loginOKProducer
描述的是在已经登出的状态时,我们可以进行登录的操作,当进行登录的操作时,状态会有已登出变为正在登录,此时便会进行登录的操作,转而变成登录成功的状态。更简洁的描述就是点击登录,从注销状态变成登录状态。
这很有趣,特别适合涉及到状态切换繁杂、交互逻辑复杂的场景。
Yep (opens new window) 中有一个场景很适合使用状态机解决问题。
当然,如果你还没有使用过 Yep (opens new window) ,那我建议你可以先去下载 (opens new window)之后体验一番再继续阅读本文。
这里是存在很多状态的,比如未录音、录音中、播放中、已暂停、已停止等,而触发的逻辑也很多,录音、停止录音、播放、暂停、重置等。这让我们处理代码逻辑显得很头疼,我们需要很多的状态变量维护当前状态,在进行某个行为时,需要先检查当前状态,根据当前状态走不同的逻辑。
比如。
private var audioPlaying: Bool = false
// ...
func playOrPauseAudio(sender: UIButton) {
if AudioBot.playing {
AudioBot.pausePlay()
audioPlaying = false
} else {
guard let fileURL = feedVoice?.fileURL else {
return
}
// ...
}
这个就非常尴尬了。随着代码量的增加。理解逻辑也会变得更加复杂。我在这个页面实践了一下基于 RxAutomaton 状态机的应用,实践了一下自己对于状态机的理解。
理清逻辑非常重要!!!
这里我们用一张图解释。这就非常尴尬了 嗯。
而代码也很好的对应了上述状态切换逻辑。
描述好切换逻辑,接下来要关注的就只有两件事。
比如一个 reset (录音完成后,重置到最初状态) 的场景。
resetButton.rx.tap
.map { Input.reset }
.subscribe(onNext: inputObserver.onNext)
.addDisposableTo(disposeBag)
上述代码表明触发 reset
的原因是点击了 resetButton
。
最后我们就只需要根据不同状态场景展示不同的 UI 。
automaton.state.asObservable()
.subscribe(onNext: { (state) in
switch state {
case .recording:
// 正在录音的 UI
case .recorded:
// 已录音的 UI
case .playing:
// 正在播放的 UI
case .reset:
// 重置的 UI
case .playPausing:
// 暂停播放的 UI
case .canceled:
// cancel 的 UI
case .playStopped:
self.playButton.setImage(R.image.button_voice_play(), for: .normal)
}
})
.addDisposableTo(disposeBag)
不得不说,这样的写法很有意思,在没有状态机的支持下,完成上述的代码也不是件复杂的事情,但代码的逻辑就不如上面这种状态机的形式更清晰,易读性得到了极大的增加。相比原有代码,我们从实现功能转变到了。
这样一来代码就变得非常清晰,更改起来也很方便。
比如,添加逻辑的变化关系时只需要添加 State -> State
的逻辑。
可以注意到在播放时,Input 的 playCompleted
是自动从 playing
到 playStopped
的,即这一行为不是(用户)主动触发的,更好的写法大概如下。
/* Input | fromState => toState | Effect */
/* ------------------------------------------------*/
.playCompleted | (.playing => .playStopped) | playCompletedProducter
如果你对这一步的实现很感兴趣,可以参考 RxAutomaton (opens new window) 中的例子,这里就不再给出代码也不再赘述了。这不会很复杂。
完成上述代码并不轻松,将代码都迁移到 Swift 3 就是一件非常麻烦的事情,我遇到了无数次的 Xcode 崩溃、高亮崩溃。当然这不是最想说的。
可以展望的一点是,我们有可能通过 enum 的关联值特性,移除代码中的 private var feedVoice: FeedVoice?
,这一属性被用来保存播放的音频。但如果使用 enum 则有可能将 feedVoice
做为状态切换时传递的值(这里是音频)。
本文 Demo 地址 https://github.com/DianQK/RxYepRecord (opens new window) 。