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?)
   }
  • VM

    The type of the ViewModel associated with the View

    Declaration

    Swift

    associatedtype VM : ViewModel
  • model 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 the oldModel parameter of the update(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 the oldModel parameter of the update(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 method

    Will throw a fatalError. Use update(oldModel:) instead.

    Declaration

    Swift

    public func update()