前几天看到了一个非常有趣的 repo RxAutomaton,我 clone 下来玩了一下,感觉非常有趣,演示了状态机在登录中的应用例子,应用中登录的逻辑如下。

登录逻辑

而这一部分的逻辑可以用代码描述,而且很清晰。

1
2
3
4
5
6
7
8
/* Input | fromState => toState | Effect */
/* ----------------------------------------------------------*/
.Login | .LoggedOut => .LoggingIn | loginOKProducer,
.LoginOK | .LoggingIn => .LoggedIn | .empty(),
.Logout | .LoggedIn => .LoggingOut | logoutOKProducer,
.LogoutOK | .LoggingOut => .LoggedOut | .empty(),
.ForceLogout | canForceLogout => .LoggingOut | forceLogoutOKProducer

对于一个理解状态机不深的我,本文就不再赘述状态机的概念了,这里有一篇很有趣的文章,介绍了为什么有时候我们要考虑使用状态机,使用状态机的好处

其实看到 repo 中的登录例子,我惊了个呆,代码竟然还可以这样下,这样的代码写法相比之前的代码会更清晰很多,毕竟我们已经将代码逻辑流程都写成了上面的样子,这很容易理解。比如 .Login | .LoggedOut => .LoggingIn | loginOKProducer 描述的是在已经登出的状态时,我们可以进行登录的操作,当进行登录的操作时,状态会有已登出变为正在登录,此时便会进行登录的操作,转而变成登录成功的状态。更简洁的描述就是点击登录,从注销状态变成登录状态

这很有趣,特别适合涉及到状态切换繁杂、交互逻辑复杂的场景。

Yep 中有一个场景很适合使用状态机解决问题。

当然,如果你还没有使用过 Yep ,那我建议你可以先去下载之后体验一番再继续阅读本文。

这里是存在很多状态的,比如未录音、录音中、播放中、已暂停、已停止等,而触发的逻辑也很多,录音、停止录音、播放、暂停、重置等。这让我们处理代码逻辑显得很头疼,我们需要很多的状态变量维护当前状态,在进行某个行为时,需要先检查当前状态,根据当前状态走不同的逻辑。

比如。

1
2
3
4
5
6
7
8
9
10
11
12
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 状态机的应用,实践了一下自己对于状态机的理解。

理清逻辑非常重要!!!

理清状态切换逻辑

这里我们用一张图解释。这就非常尴尬了 嗯。

录音 + 播放逻辑

而代码也很好的对应了上述状态切换逻辑。

描述好切换逻辑,接下来要关注的就只有两件事。

  1. 触发 Input 逻辑,即谁来(如何)改变状态
  2. 不同状态对应的 UI

触发 Input 场景

比如一个 reset (录音完成后,重置到最初状态) 的场景。

1
2
3
4
resetButton.rx.tap
.map { Input.reset }
.subscribe(onNext: inputObserver.onNext)
.addDisposableTo(disposeBag)

上述代码表明触发 reset 的原因是点击了 resetButton

不同状态对应的 UI

最后我们就只需要根据不同状态场景展示不同的 UI 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
  • 专注状态样式(State -> UI)

这样一来代码就变得非常清晰,更改起来也很方便。
比如,添加逻辑的变化关系时只需要添加 State -> State 的逻辑。

待改进的地方

可以注意到在播放时,Input 的 playCompleted 是自动从 playingplayStopped 的,即这一行为不是(用户)主动触发的,更好的写法大概如下。

1
2
3
/* Input | fromState => toState | Effect */
/* ----------------------------------------------------------------*/
.playCompleted | (.playing => .playStopped) | playCompletedProducter

如果你对这一步的实现很感兴趣,可以参考 RxAutomaton 中的例子,这里就不再给出代码也不再赘述了。这不会很复杂。

补充

完成上述代码并不轻松,将代码都迁移到 Swift 3 就是一件非常麻烦的事情,我遇到了无数次的 Xcode 崩溃、高亮崩溃。当然这不是最想说的。

可以展望的一点是,我们有可能通过 enum 的关联值特性,移除代码中的 private var feedVoice: FeedVoice?,这一属性被用来保存播放的音频。但如果使用 enum 则有可能将 feedVoice 做为状态切换时传递的值(这里是音频)。

本文 Demo 地址 https://github.com/DianQK/RxYepRecord