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 theshouldConnectWhenVisible
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 theshouldDisconnectWhenVisible
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 toconnected
=true
as soon as it becomes visible.Declaration
Swift
public var shouldConnectWhenVisible: Bool
-
When
true
the ViewController will be set toconnected
=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()