環境
RxSwiftとRxCocoaはCocoaPodsなどでインストールしましょう。
- XCode (12.2)
- Swift (5.3.1)
- RxSwift (6.0.0)
- RxCocoa (6.0.0)
実装(ON/OFF)
上層で流れてくる値に関わらずストリームに流れる値がfalse->true->falseと切り替わる関数です。
Rxオペレーターのscan()
を使って前回の状態を参照しているところがポイントです。
scan()
は購読するだけでは初期値を流さないのでstartWith()
で初期値を流しています。
extension Observable {
func flipflop(initialValue: Bool) -> Observable<Bool>{
scan(initialValue) { current, _ in !current }
.startWith(initialValue)
}
}
適当なViewControllerで動くサンプルです。ボタンをタップするとON/OFFが切り替わっています。
import UIKit
import RxSwift
import RxCocoa
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ラベル配置
let label = UILabel()
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
// ボタン配置
let button = UIButton(type: .system)
button.setTitle("Switch", for: .normal)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 10)
])
// イベント接続
_ = button.rx.tap
.asObservable()
.flipflop(initialValue: false)
.map { $0 ? "ON" : "OFF" }
.bind(to: label.rx.text)
}
}
実装(状態が3つ以上)
先ほどはON/OFFしか状態がない場合についての実装になりましたが、今度は3種類以上の状態を持つ場合について一般化してみましょう。
transition
引数で次の状態を与えることで状態の遷移を表現しています。
先程実装したflipflop()
もstate(initialValue: transition:)
の応用で置き換えられます。
extension Observable {
func state<T>(initialValue: T,
transition: @escaping (T) -> T) -> Observable<T>{
scan(initialValue) { current, _ in transition(current) }
.startWith(initialValue)
}
// 上記の関数を使うとこのような実装になる
func flipflop(initialValue: Bool) -> Observable<Bool>{
state(initialValue: false,
transition: !)
}
}
こちらはアプリ上で動くサンプルです。enum Hand
はじゃんけんの手を表現していて、ボタンを押すと表示されている手に勝つようグー->パー->チョキの順で表示が変わります。
// じゃんけんの手
enum Hand {
case rock
case paper
case scissors
var name: String {
switch self {
case .rock: return "グー"
case .paper: return "パー"
case .scissors: return "チョキ"
}
}
// グー<-パー<-チョキ<-グーの順に勝つ
var defeatedBy: Hand {
switch self {
case .rock: return .paper
case .paper: return .scissors
case .scissors: return .rock
}
}
}
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// UIは同じなので省略
// イベント接続
_ = button.rx.tap
.asObservable()
.state(initialValue: Hand.rock,
transition: \.defeatedBy)
.map(\.name)
.bind(to: label.rx.text)
}
}