In this episode, we implement the location view. The user can navigate to the location view by tapping a location in the locations view. The location view displays the current weather conditions at the top and a forecast at the bottom.
Populating the Location View
Add a group to the Views group and name it Location. Add a Swift file to the Location group by choosing the SwiftUI View template from the iOS > User Interface section. Name the file LocationView.swift.

Every view that displays data is driven by a view model so we define a view model for LocationView. Add a group to the Location group and name it View Models. Add a Swift file to the View Models group and name it LocationViewModel.swift.
Declare a struct with name LocationViewModel. The struct defines a private, constant property, location, of type Location. Because we declare the location property privately, we need to define an initializer that accepts a Location object.
import Foundation
struct LocationViewModel {
// MARK: - Properties
private let location: Location
// MARK: - Initialization
init(location: Location) {
self.location = location
}
}
Revisit LocationView.swift, declare a constant property, viewModel, of type LocationViewModel, and update the static previews property of the LocationView_Previews struct. The view model of the location view is injected through the initializer.
import SwiftUI
struct LocationView: View {
// MARK: - Properties
let viewModel: LocationViewModel
// MARK: - View
var body: some View {
Text("Hello, World!")
}
}
struct LocationView_Previews: PreviewProvider {
static var previews: some View {
LocationView(viewModel: .init(location: .preview))
}
}
Despite its name, the LocationViewModel struct won't be responsible for populating the location view. We can split the location view up into two sections. The top section displays the current weather conditions and the bottom sections displays a forecast. We create a view and a view model for both sections. This keeps the views and the view models focused and easier to maintain.
We add a SwiftUI view to the Location group and name it CurrentConditionsView. Add a Swift file to the View Models group for the view model of the current conditions view. As you may have guessed, we name the Swift file CurrentConditionsViewModel.swift and declare a struct with name CurrentConditionsViewModel.
import Foundation
struct CurrentConditionsViewModel {
}
CurrentConditionsView declares a constant property, viewModel, of type CurrentConditionsViewModel. We update the static previews property of the CurrentConditionsView_Previews struct to keep the compiler happy.
import SwiftUI
struct CurrentConditionsView: View {
// MARK: - Properties
let viewModel: CurrentConditionsViewModel
// MARK: - View
var body: some View {
Text("Current Conditions")
}
}
struct CurrentConditionsView_Previews: PreviewProvider {
static var previews: some View {
CurrentConditionsView(viewModel: .init())
}
}
We repeat these steps for the forecast view. We add a SwiftUI view to the Location group and name it ForecastView. Add a Swift file to the View Models group for the view model of the forecast view. Name the Swift file ForecastViewModel.swift and declare a struct with name ForecastViewModel.
import Foundation
struct ForecastViewModel {
}
Open ForecastView.swift, declare a constant property, viewModel, of type ForecastViewModel, and update the static previews property of the ForecastView_Previews struct.
import SwiftUI
struct ForecastView: View {
// MARK: - Properties
let viewModel: ForecastViewModel
// MARK: - View
var body: some View {
Text("Forecast")
}
}
struct ForecastView_Previews: PreviewProvider {
static var previews: some View {
ForecastView(viewModel: .init())
}
}
Before we continue, I want to note that it is possible to use the LocationViewModel struct to drive the current conditions and forecast views. A view model can drive multiple views. It doesn't need to be tied to one view.
The problem is that a view model driving multiple views has the tendency to become overly complex and its API can become confusing as the application grows. Creating dedicated view models for the current conditions and forecast views comes with very little overhead, but the benefits are significant. This becomes clear later in this series as the application gains in complexity.
Building the Location View
Open LocationView.swift. The location view displays a current conditions view at the top and a forecast view at the bottom. This means we start with a VStack. We set the VStack's alignment to leading and its spacing to 0.0 points.
var body: some View {
VStack(alignment: .leading, spacing: 0.0) {
}
}
Before we can create a current conditions view, we need a view model. Remember that the initializer of the CurrentConditionsView struct accepts an object of type CurrentConditionsViewModel. While it can be tempting to create the view model in the location view, that isn't the solution I have in mind. The view model of the location view has the context it needs to create the view models for the current conditions and forecast views. Let me show you how that works.
Open LocationViewModel.swift and declare a computed property, currentConditionsViewModel, of type CurrentConditionsViewModel. In the body of the computed property, we create and return a CurrentConditionsViewModel object. We also create a computed property, forecastViewModel, of type ForecastViewModel for the forecast view. The idea is identical.
import Foundation
struct LocationViewModel {
// MARK: - Properties
private let location: Location
// MARK: -
var currentConditionsViewModel: CurrentConditionsViewModel {
.init()
}
var forecastViewModel: ForecastViewModel {
.init()
}
// MARK: - Initialization
init(location: Location) {
self.location = location
}
}
Revisit LocationView.swift. In the VStack, we create a current conditions view. The location view's view model provides the view model for the current conditions view. It's that simple. We do the same for the forecast view.
var body: some View {
VStack(alignment: .leading, spacing: 0.0) {
CurrentConditionsView(
viewModel: viewModel.currentConditionsViewModel
)
ForecastView(
viewModel: viewModel.forecastViewModel
)
}
}
Navigating to the Location View
Let's add the ability to navigate from the locations view to the location view. Open LocationsView.swift and revisit the view builder in which we create the location cells. Before we make changes, we make a change to the LocationCellViewModel struct. The LocationCellViewModel object needs to provide the view model for the location view the user navigates to.
Open LocationCellViewModel.swift and declare a computed property, locationViewModel, of type LocationViewModel. In the body of the computed property, we create and return a LocationViewModel object.
import Foundation
struct LocationCellViewModel: Identifiable {
// MARK: - Properties
private let location: Location
// MARK: -
var locationViewModel: LocationViewModel {
.init(location: location)
}
...
}
Revisit LocationsView.swift. We create a navigation link in the view builder. The initializer accepts a destination, the view the user navigates to, and a label, the view the user taps to navigate to the destination. In the closure that returns the destination, we create a location view, using the location view model provided by the location cell view model. The label of the navigation link is the location cell.
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [GridItem()], spacing: 20.0) {
ForEach(viewModel.locationCellViewModels) { viewModel in
NavigationLink {
LocationView(viewModel: viewModel.locationViewModel)
} label: {
LocationCell(viewModel: viewModel)
}
}
}
.padding()
}
.navigationTitle("Thunderstorm")
}
}
We are now be able to navigate from the locations view to the location view by tapping a location cell in the vertical grid.
What's Next?
In a UIKit application, a view model typically drives the view of a view controller. There is a one-to-one mapping. SwiftUI makes it trivial to break a user interface up into multiple views and each of these views can be driven by a view model. There is no hard rule, though.
In the location view, it makes sense to split the user interface up into a top section and a bottom section. Each section corresponds to a view that is driven by a view model. It is perfectly fine to use one view that is driven by a view model. I prefer to keep views and their view models small, simple, and lightweight.