ViewController

open class ViewController<V> : UIViewController, AnyViewController where V : UIView, V : ViewControllerModellableView

Manages a screen of your app, it keeps the UI updated and listens for user interactions.

Overview

In Tempura, a Screen is composed by three different elements that interoperate in order to get the actual pixels on the screen and to keep them updated when the state changes. These are ViewController, ViewModelWithState and ViewControllerModellableView. The ViewController is a subclass of UIViewController that is responsible to manage the set of views that are shown in each screen of your UI.

   struct CounterState: State {
     var counter: Int = 0
   }
   struct IncrementCounter: Action {
     func updatedState(inout currentState: CounterState) {
       currentState.counter += 1
     }
   }
   struct DecrementCounter: Action {
     func updatedState(inout currentState: CounterState) {
       currentState.counter -= 1
     }
   }
   struct CounterViewModel: ViewModelWithState {
     var countDescription: String

     init(state: CounterState) {
       self.countDescription = "the counter is at \(state.counter)"
     }
   }
   class CounterView: UIView, ViewControllerModellableView {

     // subviews
     var counterLabel = UILabel()
     var addButton = UIButton(type: .custom)
     var subButton = UIButton(type: .custom)

     // interactions
     var didTapAdd: Interaction?
     var didTapSub: Interaction?

     // setup
     func setup() {
       self.addButton.on(.touchUpInside) { [unowned self] button in
         self.didTapAdd?()
       }
       self.subButton.on(.touchUpInside) { [unowned self] button in
         self.didTapSub?()
       }
       self.addSubview(self.counterLabel)
       self.addSubview(self.subButton)
       self.addSubview(self.addButton)
     }

     // style
     func style() {
       self.backgroundColor = .white
       self.addButton.setTitle("Add", for: .normal)
       self.subButton.setTitle("Sub", for: .normal)
     }

     // update
     func update(oldModel: CounterViewModel?) {
       self.counterLabel.text = self.model?.countDescription
       self.setNeedsLayout()
     }

     // layout
     override func layoutSubviews() {
       self.counterLabel.sizeToFit()
       self.addButton.frame = CGRect(x: 0, y: 100, width: 100, height: 44)
       self.subButton.frame = CGRect(x: 100, y: 100, width: 100, height: 44)
     }
   }
   class CounterViewController: ViewController<CounterView> {

   override func setupInteraction() {
     self.rootView.didTapAdd = { [unowned self] in
       self.dispatch(IncrementCounter())
     }
     self.rootView.didTapSub = { [unowned self] in
       self.dispatch(DecrementCounter())
     }
   }

Lifecycle of a ViewController

In order to instantiate a ViewController’s subclass you need to provide a Katana Store instance. This instance will be used by the ViewController to listen for state updates.

   let vc = CounterViewController(store: appStore)

When a ViewController is created it will start receiving state updates as soon as the connected property will become true.

When the ViewController becomes visible, the UIKit UIViewController.viewWillAppear() will be called and Tempura will set connected to true and the ViewController will start receiving the updates from the state. If you don’t want this to happen automatically every time the ViewController will become visible, set shouldConnectWhenVisible to false.

As soon a new state is available from the Katana store, the ViewController will instantiate a new ViewModel out of that state and feed the rootView with that, calling ModellableView.update(oldModel:)

When something happens inside the ViewControllerModellableView (or its subviews) the ViewController is responsible to listen for these Interaction callbacks and react accordingly dispatching actions in order to change the state.

When a ViewController is removed from the hierarchy or hidden by some other ViewController, UIKit will call UIViewController.viewWillDisappear() and Tempura will set connected to false, detaching the ViewController from the state updates. If you don’t want this to happen automatically every time the ViewControllet will become invisible, set sholdDisconnectWhenInvisible to false

  • true if the ViewController is connected to the store, false otherwise. A connected ViewController will receive all the updates from the store. Tempura will set this property to true when the ViewController is about to be displayed on screen, if you want to change this behaviour look at the shouldConnectWhenVisible property. Tempura will set this property to false when the ViewController is about to be hidden, if you want to change this behaviour look at the shouldDisconnectWhenVisible property.

    Declaration

    Swift

    open var connected: Bool { get set }
  • The store the ViewController will use to receive state updates.

    Declaration

    Swift

    public var store: PartialStore<V.VM.S>
  • The state of this ViewController

    Declaration

    Swift

    public var state: V.VM.S { get }
  • When true, the ViewController will be set to connected = true as soon as it becomes visible.

    Declaration

    Swift

    public var shouldConnectWhenVisible: Bool
  • When true the ViewController will be set to connected = false as soon as it becomes invisible.

    Declaration

    Swift

    public var shouldDisconnectWhenInvisible: Bool
  • The latest ViewModel received by this ViewController from the state.

    Declaration

    Swift

    public var viewModel: V.VM? { get set }
  • Use the rootView to access the main view managed by this viewController.

    Declaration

    Swift

    open var rootView: V { get }
  • Used internally to load the specific main view managed by this view controller.

    Declaration

    Swift

    override open func loadView()
  • Returns a newly initialized ViewController object.

    Declaration

    Swift

    public init(store: PartialStore<V.VM.S>, connected: Bool = false)
  • Override to setup something after init.

    Declaration

    Swift

    open func setup()
  • Shortcut to the non-generic dispatch function.

    Declaration

    Swift

    open func dispatch(_ dispatchable: Dispatchable)
  • Shortcut to the dispatch function. This will return a Promise when called with a Dispatchable.

    Declaration

    Swift

    @discardableResult
    open func __unsafeDispatch<T>(_ dispatchable: T) -> Promise<Void> where T : StateUpdater
  • Shortcut to the dispatch function. This will return a Promise when called on a non returning SideEffect T.

    Declaration

    Swift

    @discardableResult
    open func __unsafeDispatch<T>(_ dispatchable: T) -> Promise<Void> where T : SideEffect
  • Shortcut to the dispatch function. This will return a Promise when called on a SideEffect T.

    Declaration

    Swift

    @discardableResult
    open func __unsafeDispatch<T>(_ dispatchable: T) -> Promise<T.ReturnValue> where T : ReturningSideEffect
  • Required init.

    Declaration

    Swift

    public required init?(coder _: NSCoder)
  • The ViewController is about to be displayed.

    Declaration

    Swift

    override open func viewWillAppear(_ animated: Bool)
  • The ViewController is about to be removed from the view hierarchy.

    Declaration

    Swift

    override open func viewWillDisappear(_ animated: Bool)
  • Called after the controller'€™s view is loaded into memory.

    Declaration

    Swift

    override open func viewDidLoad()
  • Called just before the update, override point for subclasses.

    Declaration

    Swift

    open func willUpdate(new _: V.VM?)
  • Called right after the update, override point for subclasses.

    Declaration

    Swift

    open func didUpdate(old _: V.VM?)
  • Asks to setup the interaction with the managed view, override point for subclasses.

    Declaration

    Swift

    open func setupInteraction()
  • Called just before the unsubscribe, override point for subclasses.

    Declaration

    Swift

    open func willUnsubscribe()
  • Add a ViewController (and its rootView) as a child of self You must provide a ContainerView inside self.rootView, that container view will automatically receive the rootView of the child ViewController as child

    Declaration

    Swift

    public func add<View>(_ child: ViewController<View>, in view: ContainerView) where View : UIView, View : ViewControllerModellableView
  • Replace a ViewController (and its rootView) to the last child of self. You must provide a ContainerView inside self.rootView, the latest view of that container view is replaced with the rootView of the child ViewController with a cross-dissolve transition

    Declaration

    Swift

    public func transition<View: ViewControllerModellableView>(
      to child: ViewController<View>,
      in view: ContainerView,
      duration: Double = 0.3,
      options: UIView.AnimationOptions = [.transitionCrossDissolve],
      completion: (() -> Void)? = nil
    )
  • Remove self as child ViewController of parent

    Declaration

    Swift

    public func remove()