Skip to content

Instantly share code, notes, and snippets.

@marcmo
Created November 12, 2018 11:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcmo/551153797934f48b703e4f10a18611b8 to your computer and use it in GitHub Desktop.
Save marcmo/551153797934f48b703e4f10a18611b8 to your computer and use it in GitHub Desktop.
example of RxAutomaton side effect handling
//
// ViewController.swift
// RxAutomatonDemo
//
// Created by Yasuhiro Inami on 2016-08-15.
// Copyright © 2016 Yasuhiro Inami. All rights reserved.
//
import UIKit
import RxSwift
import RxCocoa
import RxAutomaton
import Pulsator
class AutomatonViewController: UIViewController
{
enum Job: String, CustomStringConvertible
{
case completeLogin
case nothing
var description: String { return self.rawValue }
}
@IBOutlet weak var diagramView: UIImageView?
@IBOutlet weak var label: UILabel?
@IBOutlet weak var statusLabel: UILabel!
@IBOutlet weak var loginButton: UIButton?
@IBOutlet weak var logoutButton: UIButton?
@IBOutlet weak var forceLogoutButton: UIButton?
@IBOutlet weak var triggerButton: UIButton!
private var pulsator: Pulsator?
private var _automaton: Automaton<State, Input>?
private let _disposeBag = DisposeBag()
override func viewDidLoad()
{
super.viewDidLoad()
let (textSignal, textObserver) = Observable<String?>.pipe()
func doApiCall() {
}
/// Count-up effect.
func countUpProducer(status: String, count: Int = 2, interval: TimeInterval = 1, nextInput: Input) -> Observable<Input>
{
return Observable<Int>.interval(interval, scheduler: MainScheduler.instance)
.take(count)
.scan(0) { x, _ in x + 1 }
.startWith(0)
.map {
switch $0 {
case 0: return "\(status)..."
case count: return "\(status) Done!"
default: return "\(status)... (\($0))"
}
}
.do(onNext: textObserver.onNext)
.then(value: nextInput)
}
var workToDo = PublishRelay<Job>()
var workToDoDriver: Driver<String> {
return workToDo
.map {
switch $0 {
case .completeLogin: return "TODO complete login"
default: return "nothing to do"
}
}
.asDriver(onErrorJustReturn: "error")
}
var triggerState = PublishRelay<Bool>()
var triggerLogin: Observable<Input> {
return Observable.create { observer in
workToDo.accept(.completeLogin)
triggerState.subscribe { success in
if (success.element ?? false) {
observer.onNext(.loginOK)
} else {
observer.onNext(.loginNOTOK)
}
workToDo.accept(.nothing)
}.disposed(by: self._disposeBag)
return Disposables.create()
}
}
let logoutOKProducer = countUpProducer(status: "Logout", nextInput: .logoutOK)
let forceLogoutOKProducer = countUpProducer(status: "ForceLogout", nextInput: .logoutOK)
// NOTE: predicate style i.e. `T -> Bool` is also available.
let canForceLogout: (State) -> Bool = [.loggingIn, .loggedIn].contains
/// Transition mapping.
let mappings: [Automaton<State, Input>.EffectMapping] = [
/* Input | fromState => toState | Effect */
/* ----------------------------------------------------------*/
.login | .loggedOut => .loggingIn | triggerLogin,
.loginOK | .loggingIn => .loggedIn | .empty(),
.logout | .loggedIn => .loggingOut | logoutOKProducer,
.logoutOK | .loggingOut => .loggedOut | .empty(),
.forceLogout | canForceLogout => .loggingOut | forceLogoutOKProducer
]
let (inputSignal, inputObserver) = Observable<Input>.pipe()
let automaton = Automaton(state: .loggedOut, input: inputSignal, mapping: reduce(mappings), strategy: .latest)
self._automaton = automaton
automaton.replies
.subscribe(onNext: { reply in
print("received reply = \(reply)")
})
.disposed(by: _disposeBag)
automaton.state.asObservable()
.subscribe(onNext: { state in
print("current state = \(state)")
})
.disposed(by: _disposeBag)
// Setup buttons.
do {
self.loginButton?.rx.tap
.subscribe(onNext: { _ in inputObserver.onNext(.login) })
.disposed(by: _disposeBag)
self.logoutButton?.rx.tap
.subscribe(onNext: { _ in inputObserver.onNext(.logout) })
.disposed(by: _disposeBag)
self.forceLogoutButton?.rx.tap
.subscribe(onNext: { _ in inputObserver.onNext(.forceLogout) })
.disposed(by: _disposeBag)
self.triggerButton?.rx.tap
.subscribe(onNext: { _ in
triggerState.accept(true)
})
.disposed(by: _disposeBag)
}
// Setup label.
do {
textSignal
.bind(to: self.label!.rx.text)
.disposed(by: _disposeBag)
workToDoDriver
.drive(self.statusLabel.rx.text)
.disposed(by: _disposeBag)
}
// Setup Pulsator.
do {
let pulsator = _createPulsator()
self.pulsator = pulsator
self.diagramView?.layer.addSublayer(pulsator)
automaton.state.asDriver()
.map(_pulsatorColor)
.map { $0.cgColor }
.drive(pulsator.rx_backgroundColor)
.disposed(by: _disposeBag)
automaton.state.asDriver()
.map(_pulsatorPosition)
.drive(pulsator.rx_position)
.disposed(by: _disposeBag)
// Overwrite the pulsator color to red if `.forceLogout` succeeded.
automaton.replies
.filter { $0.toState != nil && $0.input == .forceLogout }
.map { _ in UIColor.red.cgColor }
.bind(to: pulsator.rx_backgroundColor)
.disposed(by: _disposeBag)
}
}
}
// MARK: Pulsator
private func _createPulsator() -> Pulsator
{
let pulsator = Pulsator()
pulsator.numPulse = 5
pulsator.radius = 100
pulsator.animationDuration = 7
pulsator.backgroundColor = UIColor(red: 0, green: 0.455, blue: 0.756, alpha: 1).cgColor
pulsator.start()
return pulsator
}
private func _pulsatorPosition(state: State) -> CGPoint
{
switch state {
case .loggedOut: return CGPoint(x: 40, y: 100)
case .loggingIn: return CGPoint(x: 190, y: 20)
case .loggedIn: return CGPoint(x: 330, y: 100)
case .loggingOut: return CGPoint(x: 190, y: 180)
}
}
private func _pulsatorColor(state: State) -> UIColor
{
switch state {
case .loggedOut:
return UIColor(red: 0, green: 0.455, blue: 0.756, alpha: 1) // blue
case .loggingIn, .loggingOut:
return UIColor(red: 0.97, green: 0.82, blue: 0.30, alpha: 1) // yellow
case .loggedIn:
return UIColor(red: 0.50, green: 0.85, blue: 0.46, alpha: 1) // green
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment