In the previous episode, we declared the State enum. It defines the possible states of the AddLocationView. In this episode, we integrate the State enum into the AddLocationView.

Switching on the View's State

Open AddLocationView.swift and navigate to the computed body property. Below the TextField, we add a switch statement to switch on the value of the view model's state property. We add a case to the switch statement for the empty case, the querying case, the message case, and the results case. Remember that the message case and the results case each have an associated value.

var body: some View {
    VStack {
        TextField(viewModel.textFieldPlaceholder, text: $viewModel.query)
            .padding()

        switch viewModel.state {
        case .empty:
            ()
        case .querying:
            ()
        case .message(let message):
            ()
        case .results(let viewModels):
            ()
        }

        List {
            ForEach(viewModel.addLocationCellViewModels) { cellViewModel in
                AddLocationCell(viewModel: cellViewModel) {
                    viewModel.addLocation(with: cellViewModel.id)
                    showsAddLocationView.wrappedValue.toggle()
                }
            }
        }
    }
}

The next step is defining a view for each state of the AddLocationView. The view for the empty case is simple, a Spacer view.

switch viewModel.state {
case .empty:
    Spacer()
case .querying:
    ()
case .message(let message):
    ()
case .results(let viewModels):
    ()
}

For the querying case, we display a vertically centered ProgressView and for the message case we display a vertically centered Text view. To avoid code duplication, we define a view that can handle both states.

At the bottom of AddLocationView.swift, declare a fileprivate struct with name MessageView that conforms to the View protocol. The View protocol requires that the MessageView struct declares a computed body property. This should look familiar.

fileprivate struct MessageView: View {

    // MARK: - View

    var body: some View {
    	
    }

}

A MessageView has one of two styles, it displays a ProgressView or it displays a Text view. Declare a nested enum with name Style. The Style enum defines two cases, progressView and message. The message case has an associated value of type String, the content of the Text view.

fileprivate struct MessageView: View {

    // MARK: - Types

    enum Style {

        // MARK: - Cases

        case progressView
        case message(String)

    }

    // MARK: - View

    var body: some View {
		
    }

}

We also declare a constant property with name style of type Style.

fileprivate struct MessageView: View {

    // MARK: - Types

    enum Style {

        // MARK: - Cases

        case progressView
        case message(String)

    }

    // MARK: - Properties

    let style: Style

    // MARK: - View

    var body: some View {
		
    }

}

Let's focus on the implementation of the computed body property. We start with a VStack that contains two Spacer views to vertically center the ProgressView and the Text view. Below the first Spacer view, we use a switch statement to switch on the value of the view's style property. We add a case to the switch statement for the progressView case and the message case. Remember that the message case has an associated value of type String, the content of the Text view.

fileprivate struct MessageView: View {

    // MARK: - Types

    enum Style {

        // MARK: - Cases

        case progressView
        case message(String)

    }

    // MARK: - Properties

    let style: Style

    // MARK: - View

    var body: some View {
        VStack {
            Spacer()
            switch style {
            case .progressView:
                ()
            case .message(let message):
                ()
            }
            Spacer()
        }
    }

}

The next step is defining a view for each style of the MessageView. The view for the progressView case is a ProgressView. The view for the message case is a Text view that displays the associated value of the message case.

var body: some View {
    VStack {
        Spacer()
        switch style {
        case .progressView:
            ProgressView()
        case .message(let message):
            Text(message)
        }
        Spacer()
    }
}

Before we put the MessageView to use, we style the Text view using a few view modifiers. We set the Text view's font to body and its foreground color to darkGray. That's it.

var body: some View {
    VStack {
        Spacer()
        switch style {
        case .progressView:
            ProgressView()
        case .message(let message):
            Text(message)
                .font(.body)
                .foregroundColor(.darkGray)
        }
        Spacer()
    }
}

Revisit the switch statement we added to the computed body property of the AddLocationView. In the querying case, we create a MessageView. The style we pass to the initializer of the MessageView struct is progressView. We repeat this for the message case. The difference is the style we pass to the initializer of the MessageView struct.

switch viewModel.state {
case .empty:
    Spacer()
case .querying:
    MessageView(style: .progressView)
case .message(let message):
    MessageView(style: .message(message))
case .results(let viewModels):
    ()
}

Displaying Results

The implementation of the results case isn't complicated either. We can reuse the implementation we started with. We no longer ask the view model for an array of AddLocationCellViewModel objects. The array of AddLocationCellViewModel objects is the associated value of the results case.

switch viewModel.state {
case .empty:
    Spacer()
case .querying:
    MessageView(style: .progressView)
case .message(let message):
    MessageView(style: .message(message))
case .results(let viewModels):
    List {
        ForEach(viewModels) { cellViewModel in
            AddLocationCell(viewModel: cellViewModel) {
                viewModel.addLocation(with: cellViewModel.id)
                showsAddLocationView.wrappedValue.toggle()
            }
        }
    }
}

Run the application to see the result of the changes we made. Navigate to the AddLocationView and enter the name of a town or city in the TextField. The most notable changes are the vertically centered ProgressView when a forward geocoding request is in flight and the vertically centered message when the Core Location framework failed to find a match for the town or city the user entered in the TextField.

What's Next?

Spending a little bit of time refining the user experience of your application goes a long way. Not only does it result in a happy user, it also impacts other, less obvious aspects of an application, such as less customer support issues and positive App Store reviews. In true MVVM fashion, the heavy lifting is handled by the view model. The view simply displays what its view model provides.