Skip to content

Latest commit

 

History

History
118 lines (81 loc) · 4.7 KB

declaring-types-notes.md

File metadata and controls

118 lines (81 loc) · 4.7 KB

Notes about declaring types

ScreenModule

State

When declaring State, and deciding where and how to declare the individual pieces of content for a Screen, there are generally three kinds of content:

  • Dynamic - these are parts of the screen that can change while it is running - username and password fields for a login screen are great examples of this, as they change when the user types. Dynamic State properties are declared as a var.
  • Screen invariant - pieces of content that will never change during the lifetime of a screen instance, but could differ from instance to instance. For example, we could add a message property to the login screen for describing why a user needs to login - e.g., "Log in to change your settings", or "Confirm your login to delete your account". Invariant State properties are declared as a let.
  • Constant - These are things that are always the same, no matter what. For example, the login screen in our fictitious app will always have the title "Login". Constants aren't added to State.
enum State: Equatable {
  var username: String
  var password: String
  let message: String
}

Action

An interesting way to think about Action cases is as functions in a protocol. E.g., consider the set of Actions:

enum Action: Equatable {
  case didTapButton
  case didEnterValue(String)
}

is analgous to this protocol:

protocol MyScreenActionDelegate {
  func didTapButton()
  func didEnterValue(_ value: String)
}

The nice thing about Actions as enum cases is that:

  • you can collect them - e.g. as an undo stack, or for unit testing purposes;
  • they can be comparable for easy unit testing - e.g., to make sure a a View produces the proper actions in response to user actions.
  • they are a concrete type - so they can be nested within another type (as we do in a ScreenModule), and they play well with generics.

createScreen

There are a few convenience versions of createScreen available. The most commonly used - besides the no-argument version - is the one that allows for a specific initial state. In a login screen example, if we wanted to pre-populate our username field with the last username used, we can call a version of createScreen that allows us to specify an initial state:

let initialState = LoginScreenModule.State(username: "Billie")
let screen = LoginScreenModule.createScreen(with: initialState)

Note that it's quite common to create your own versions of createScreen, to allow for even more concise usage:

public static func createScreen(username: String) -> Screen {
  let initialState = LoginScreenModule.State(username: username)
  return createScreen(with: initialState)
}

// Client usage:
let screen = LoginScreenModule.createScreen(username: "Billie")

Default types

ScreenModule / StoreModule

You don't have to declare all of the value types in a ScreenModule. For, example if you don't need an Output, there's no need to declare an empty enum for it. When left out, a ScreenModule's' Output will be declared for you as NoOutput, which is in fact just an empty enum. Action defaults to NoAction, and State to EmptyState. The only component with no default value is the createScreen function - this function must always be defined.

enum BlankModule: ScreenModule {
    static func createScreen(with store: BlankStore) -> Screen {
        return Screen(store, UIViewController())
    }
}

final class BlankStore: LassoStore<BlankModule> {
}

Technically speaking, Action, Output, and State are all associated types in the StoreModule protocol. ScreenModule is a protocol with StoreModule conformance plus the notion of a Store and UIViewController grouped as a Screen

FlowModule

The FlowModule protocol also has defaults for the types you can declare. Similar to ScreenModule, Output is defaulted to NoOutput in a FlowModule. The RequiredContext associated type is defaulted to UIViewController. If your module has no special placement requirements, you can leave out the RequiredContext:

enum MyFlowModule: FlowModule {
  enum Output: Equatable {
    case somethingHappened
  }
}

Furthermore, in cases where you're writing a module that will not emit any output, you can leave out that declaration, too.

There are also some pre-defined modules for common situations, so you can even get away with not explicitly declaring your own FlowModule. These are:

  • NoOutputFlow
    • Output = NoOutput
    • RequiredContext = UIViewController
  • NoOutputNavigationFlow
    • Output = NoOutput
    • RequiredContext = UINavigationController