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
andpassword
fields for a login screen are great examples of this, as they change when the user types. DynamicState
properties are declared as avar
. - 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". InvariantState
properties are declared as alet
. - 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
}
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.
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")
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
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