Skip to content

Up Down

Choi Geonu edited this page Jul 9, 2016 · 2 revisions

Up & Down is simple view that has a number to be increased & decreased.

We'll make simple Mode & View & Controller here.

Screenshot

TBD

Code

Assume that we already have a xib or storyboard scene.

Model

UpDownModel has simple functionalities.

  1. Increase a number

  2. Decrease a number

  3. Reset a number to 0

Let's define these functionalities as Action

enum UpDownAction {
    case Increase
    case Decrease
    case Reset
}

It's very simple. Action can be any type, but enum will be enough for most of cases.

And Model is here.

struct UpDownModel: ReducerModel {
    typealias Action = UpDownAction
    typealias State = Int
    
    let initialState = 0
    func reduce(state: State, with action: Action) -> State {
        switch action {
        case .Increase:
            return state + 1
        case .Decrease:
            return state - 1
        case .Reset:
            return initialState
        }
    }
}

It does simply dispatch the Actions to mutation of state.

By using RecuerModel, we can remember previous state of model.

It uses Observable.scan and you can find an implementation at here.

View

We will implement UpDownView as combination of View an UserInteractable. This combination is general for most of cases in GUI programs.

First, let's define Events that UpDownView can occurs.

enum UpDownEvent {
    case ClickUp
    case ClickDown
    case ClickReset
}

It describes user's interactions as enum.

And now, define UpDownView

struct UpDownView: View, UserInteractable {
    typealias State = Int
    typealias Event = UpDownEvent
    ...

Set required State as Int and output Event as UpDownEvent.

    let countLabel: UILabel
    let upButton: UIButton
    let downButton: UIButton
    let resetButton: UIButton

And actual UI components it needs.

    func update(stateStream: Observable<State>) -> Disposable {
        return stateStream.subscribeNext { (number) in
            self.countLabel.text = "\(number)"
        }
    }

This is implementation of updating. It just subscribes State stream and returns disposable.

    func interact() -> Observable<Event> {
        return [
            self.upButton.rx_tap.map{_ in Event.ClickUp},
            self.downButton.rx_tap.map{_ in Event.ClickDown},
            self.resetButton.rx_tap.map{_ in Event.ClickReset},
            ].toObservable().merge()
    }
}

And provides event stream captured from UI components.

Here's full source of UpDownView

struct UpDownView: View, UserInteractable {
    typealias State = Int
    typealias Event = UpDownEvent
    
    let countLabel: UILabel
    let upButton: UIButton
    let downButton: UIButton
    let resetButton: UIButton
    
    func update(stateStream: Observable<State>) -> Disposable {
        return stateStream.subscribeNext { (number) in
            self.countLabel.text = "\(number)"
        }
    }
    
    func interact() -> Observable<Event> {
        return [
            self.upButton.rx_tap.map{_ in Event.ClickUp},
            self.downButton.rx_tap.map{_ in Event.ClickDown},
            self.resetButton.rx_tap.map{_ in Event.ClickReset},
            ].toObservable().merge()
    }
}

Controller

Controller is dispatcher that converts event to action. We can make a HTTP request, background jobs, and many things at here. But all we need is conversion of Event to Action for now.

struct UpDownController: MapController {
    typealias Event = UpDownEvent
    typealias Action = UpDownAction
    
    func mapEventToAction(event: Event) -> Action {
        switch event {
        case .ClickUp:
            return Action.Increase
        case .ClickDown:
            return Action.Decrease
        case .ClickReset:
            return Action.Reset
        }
    }
}

MapController is a shortcut for

func use(eventStream: Observable<Event>) -> Observable<Self.Action> {
    return eventStream.map({ (event) in
        switch event {
            ...
        }
    })
}

UIViewController

And now combine Model, View, Controller into application.

A UIViewController will help for do that.

class UpDownViewController: UIViewController {
    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var upButton: UIButton!
    @IBOutlet weak var downButton: UIButton!
    @IBOutlet weak var resetButton: UIButton!
    
    var disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let model = UpDownModel()
        let view = UpDownView(countLabel: countLabel,
                              upButton: upButton,
                              downButton: downButton,
                              resetButton: resetButton)
        let controller = UpDownController()
        combineModel(model, withView: view, controller: controller).addDisposableTo(disposeBag)
    }
}

combineModel(model, withView: view, controller: controller) can be replaced with view.update(model.manipulate(controller.use(view.interact()))). It makes a Disposable, so we have to dispose() in someday. disposeBag will do that.

Full Source

import UIKit
import RxSwift
import RxCocoa
import RxMVC

enum UpDownEvent {
    case ClickUp
    case ClickDown
    case ClickReset
}

enum UpDownAction {
    case Increase
    case Decrease
    case Reset
}

typealias State = Int

struct UpDownModel: ReducerModel {
    typealias Action = UpDownAction
    typealias State = Int
    
    let initialState = 0
    func reduce(state: State, with action: Action) -> State {
        switch action {
        case .Increase:
            return state + 1
        case .Decrease:
            return state - 1
        case .Reset:
            return initialState
        }
    }
}

struct UpDownView: View, UserInteractable {
    typealias State = Int
    typealias Event = UpDownEvent
    
    let countLabel: UILabel
    let upButton: UIButton
    let downButton: UIButton
    let resetButton: UIButton
    
    func update(stateStream: Observable<State>) -> Disposable {
        return stateStream.subscribeNext { (number) in
            self.countLabel.text = "\(number)"
        }
    }
    
    func interact() -> Observable<Event> {
        return [
            self.upButton.rx_tap.map{_ in Event.ClickUp},
            self.downButton.rx_tap.map{_ in Event.ClickDown},
            self.resetButton.rx_tap.map{_ in Event.ClickReset},
            ].toObservable().merge()
    }
}

struct UpDownController: MapController {
    typealias Event = UpDownEvent
    typealias Action = UpDownAction
    
    func mapEventToAction(event: Event) -> Action {
        switch event {
        case .ClickUp:
            return Action.Increase
        case .ClickDown:
            return Action.Decrease
        case .ClickReset:
            return Action.Reset
        }
    }
}

class UpDownViewController: UIViewController {
    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var upButton: UIButton!
    @IBOutlet weak var downButton: UIButton!
    @IBOutlet weak var resetButton: UIButton!
    
    var disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let model = UpDownModel()
        let view = UpDownView(countLabel: countLabel,
                              upButton: upButton,
                              downButton: downButton,
                              resetButton: resetButton)
        let controller = UpDownController()
        combineModel(model, withView: view, controller: controller).addDisposableTo(disposeBag)
    }
}

Override functions of Model and Controller has no side effects. It makes your application simple, and predictable.

Clone this wiki locally