ModellableView
public protocol ModellableView : View
Mixin protocol for UIView subclasses based on the same SSUL lifecycle of View
. Conforming to ModellableView
, a UIView
will get a model: ViewModel
property
and the update(oldModel: ViewModel?)
will be automatically called each time the model property will change.
If your UIView is simple and you don’t want to use a ViewModel, refer to the View
protocol instead.
Overview
The View
protocol is good enough for small reusable UI elements that can be manipulated through properties.
There are a couple of drawbacks to this approach:
- it’s not easy to test UI elements
- in the
View.update()
phase we don’t know the actual property that is changed, meaning that we cannot reason in terms of differences from the old values - changing two or more properties at the same time will trigger two or more updates.
To solve all of these issues we introduce the concept of ViewModel
.
A ViewModel is a struct that contains all the properties that define the state of the View.
struct ContactViewModel: ViewModel {
var name: String = "John"
var lastName: String = "Doe"
}
A ModellableView
then is a special case of View
that is using a ViewModel to represent its state.
All the Setup, Style and Layout phases described in View
are still in use, the only difference is that
the Update method of the ModellableView is getting an oldModel
parameter.
struct ContactView: UIView, ModellableView {
// subviews to create the UI
private var title = UILabel()
private var subtitle = UILabel()
// interactions
var nameDidChange: ((String) -> ())?
var lastNameDidChange: ((String) -> ())?
override init(frame: CGRect = .zero) {
super.init(frame: frame)
self.setup()
self.style()
}
func setup() {
// define the subviews that will make up the UI
self.addSubview(self.title)
self.addSubview(self.subtitle)
self.title.on(.didEndEditing) { [weak self] label in
self?.nameDidChange?(label.text)
}
self.subtitle.on(.didEndEditing) { [weak self] label in
self?.lastNameDidChange?(label.text)
}
}
func style() {
// define the default look and feel of the UI elements
}
func update(oldModel: ContactViewModel?) {
// update the UI based on the value of `self.model`
// you can use `oldModel` to reason about diffs
self.title.text = self.model.name
self.subtitle.text = self.model.lastname
}
override func layoutSubviews() {
// layout the subviews
}
}
Conforming to ModellableView
will:
- create the
model: ContactViewModel
variable automatically for you. - automatically call the
ModellableView.update(oldModel:)
method every time the model changes - allow to test the ViewModel instead of testing the ModellableView
- include the oldModel inside the
ModellableView.update(oldModel:)
so that you can reason about diffs - allow to change more than one property and trigger only one update
public protocol ModellableView: View {
associatedtype VM: ViewModel
// the ViewModel of the View.
// `update(oldModel: VM?)` will be called each time model will change
var model: VM? { get set }
// the model is changed, update the View
func update(oldModel: VM?)
}
-
The type of the ViewModel associated with the View
Declaration
Swift
associatedtype VM : ViewModel
-
model
Default implementationThe ViewModel of the View. Once changed, the
update(oldModel: VM?)
will be called. The model variable is automatically created for you once you conform to the ModellableView protocol. Swift is inferring the Type through theoldModel
parameter of theupdate(oldModel: ViewModel?)
method and we are adding the var exploiting a feature of the Objective-C runtime called Associated Objects.Default Implementation
The ViewModel of the View. Once changed, the
update(oldModel: VM?)
will be called. The model variable is automatically created for you once you conform to the ModellableView protocol. Swift is inferring the Type through theoldModel
parameter of theupdate(oldModel: ViewModel?)
method and we are adding the var exploiting a feature of the Objective-C runtime called Associated Objects.Declaration
Swift
var model: VM? { get set }
-
Called when the ViewModel is changed. Update the View using
self.model
.Declaration
Swift
func update(oldModel: VM?)
-
update()
Extension methodWill throw a fatalError. Use
update(oldModel:)
instead.Declaration
Swift
public func update()