State and Input

Bow Arch encourages a strong modeling of the problem domain to capture the state and inputs of a component into immutable data structures, that are usually designed as Algebraic Data Types.

How you model state and inputs is very dependent on your particular project. However, as a general guideline, state is usually modeled using a product type, and inputs are usually modeled as a sum type.

Product types in Swift can be represented using structs, tuples or classes, and while being isomorphic, each one of them have different semantics and/or ergonomics:

Typically, modeling state with structs will be our preferred choice.

As for Sum types, Swift provides enums to represent them. Swift enums can have associated values that will be the companion data we need to perform an action for the input they represent.

Parent-child relationships

Both state and input of a given component need to be captured in its parent state and input. That is, the parent state should have a field representing the child state; similarly, the parent input should have a case representing the child input.

Accessing and modifying immutable data

We have made a strong emphasis in modeling state and input as immutable data structures. How should you access and modify them? The answer is optics.

Optics are algebraic structures that let us work with immutable data structures in a functional way, and are highly composable. In particular, the optics that we will need are:

You can use Bow Optics to write your own lenses and prisms, or get them automatically generated, to work with your data structures.

Example

Consider an app that renders a home screen with a user profile and a list of articles. Tapping on the user profile goes to a new screen to show a detail of the user profile, where we can perform editions.

We can model state as:

struct UserProfile {
    let name: String
    let picture: URL
}

struct Article {
    let title: String
    let content: String
    let publicationDate: Date
    let isFavorite: Bool
}

State is then grouped into the parent state:

struct HomeScreen {
    let profile: UserProfile
    let articles: [Article]
}

Similarly, we can model inputs as:

enum UserProfileInput {
    case changePicture(URL)
    case changeName(String)
}

enum ArticleInput {
    case markFavorite(Article)
}

Inputs can also be grouped into a parent input:

enum HomeScreenInput {
    case userProfile(UserProfileInput)
    case article(ArticleInput)
}

We can write a lens to access the user profile from a home screen:

let userProfileLens = Lens<HomeScreen, UserProfile>(
    get: { home in home.profile },
    set: { home, newProfile in HomeScreen(profile: newProfile, articles: home.articles) }
)

Likewise, we can write a prism to access the user profile input from a home screen input:

let userProfilePrism = Prism<HomeScreenInput, UserProfileInput>(
    extract: { homeInput in
        guard case let .userProfile(input) = homeInput else {
            return nil
        }
        return input
    },
    embed: HomeScreenInput.userProfile
)