View

public protocol View : AnyObject

Basic protocol representing the 4 main phases of the lifecycle of a UIView in Tempura.

Ideally all the reusable simple Views of the app should conform to this protocol. Please note that this protocol is just used in order to enforce the same lifecycle for all the views.

For more complex Views please refer to the ModellableView protocol.

Overview

A view is a piece of UI that is visible on screen. It contains no business logic, it can contain UI logic.

class Switch: UIView, View {

  // subviews
  private var thumb = UIView()

  // properties
  var isOn: Bool = false {
    didSet {
      guard self.isOn != oldValue else { return }
      self.update()
    }
  }

  override init(frame: CGRect = .zero) {
    super.init(frame: frame)
    self.setup()
    self.style()
  }

  // interactions
  var valueDidChange: ((Bool) -> ())?

  func setup() {
    // define the subviews that will make up the UI
  }

  func style() {
    // define the default look and feel of the UI elements
  }

  func update() {
    // update the UI based on the value of the properties
  }

  override func layoutSubviews() {
    // layout the subviews, optionally considering the properties
  }
}

The interface that the View is exposing is composed by properties and interactions. The properties are the internal state of the element that can be manipulated from the outside, interactions are callbacks used to listen from outside of the view to changes occurred inside the element itself (like user interacting with the element changing its value). The lifecycle of a View contains four different phases:

public protocol View: class {
  func setup()
  func style()
  func update()
  func layoutSubviews()
}

This protocol is not doing anything for us, it’s just a way to enforce the SSUL phases.

Setup

The setup phase should execute only once when the View is created, here you tipically want to create and add all the children views as subviews

func setup() {
  self.addSubview(self.headerView)
  self.addSubview(self.contentView)
  self.addSubview(self.footerView)
}

Style

The style phase should execute only once when the View is created, right after the setup phase. Here you configure all the style related properties that will not change over time. For all the style attributes that change depending on the “state” of the view, look at the View.update() phase.

func style() {
  self.headerView.backgroundColor = .white
  self.contentView.layer.cornerRadius = 20
}

Update

The update phase should execute every time the “state” of the View is changed. Here you update the View in order to reflect its new state.

var headerImage: UIImage? {
  didSet {
    self.update()
  }
}
func update() {
  self.headerView.image = self.headerImage
}

Layout

The layout phase is where you define the layout of your view. It’s using the same layoutSubviews() method of UIView, meaning that can be triggered using the usual setNeedsLayout() and layoutIfNeeded() methods of UIKit.

override func layoutSubviews() {
  self.headerView.frame = CGRect(x: 0, y:0, width: self.bounds.width, height: 100)
  self.contentView.frame = CGRect(x: 0, y: 100, width: self.bounds.width, height: 300)
}

Note on layout updates

When the layout of your view changes over time, you are responsible to call setNeedsLayout() inside the update() phase in order to trigger a layout update.

func update() {
  self.headerView.image = self.headerImage
  self.setNeedsLayout()
}
override func layoutSubviews() {
  let containsImage: Bool = self.headerImage != nil
  let headerHeight: CGFloat = containsImage ? 100 : 0
  self.headerView.frame = CGRect(x: 0, y:0, width: self.bounds.width, height: headerHeight)
}

Note on calling setup and style

When your UIView subclass conforms to the View protocol, you are responsible to call the setup() and style() methods inside the init.

class TestView: UIView, View {
 override init(frame: CGRect = .zero) {
   super.init(frame: frame)
   self.setup()
   self.style()
 }

 func setup() {}
 func style() {}
 func update() {}
 override func layoutSubviews() {}
}
  • The setup phase should execute only once when the View is created, here you tipically want to create and add all the children views as subviews.

    Declaration

    Swift

    func setup()
  • The style phase should execute only once when the View is created, right after the setup phase. Here you configure all the style related properties that will not change over time. For all the style attributes that change depending on the “state” of the view, look at the View.update() phase.

    Declaration

    Swift

    func style()
  • The update phase should execute every time the “state” of the View is changed. Here you update the View in order to reflect its new state.

    Declaration

    Swift

    func update()
  • The layout phase is where you define the layout of your view. It’s using the same layoutSubviews() method of UIView, meaning that can be triggered using the usual setNeedsLayout() and layoutIfNeeded() methods of UIKit.

    Declaration

    Swift

    func layoutSubviews()