In this episode, we display weather data in the location view. Remember that the location view displays two subviews or child views, the current conditions view and the forecast view. These views are responsible for displaying the weather data for a location.
Adding Details to the Location View
Before we revisit the current conditions view and the forecast view, we make two subtle changes to the location view. First, we add a divider between the current conditions view and the forecast view. The forecast view will contain a scroll view so adding a divider makes the user experience feel more natural.
var body: some View {
VStack(alignment: .leading, spacing: 0.0) {
CurrentConditionsView(
viewModel: viewModel.currentConditionsViewModel
)
Divider()
ForecastView(
viewModel: viewModel.forecastViewModel
)
}
}
Second, we display the name of the location at the top of the location view. Open LocationViewModel.swift and declare a computed property, locationName, of type String. In the body of the computed property, we return the name of the location.
var locationName: String {
location.name
}
Revisit LocationView.swift and apply the navigationTitle view modifier to the VStack. The view modifier accepts an argument of type String, the value of the computed locationName property.
var body: some View {
VStack(alignment: .leading, spacing: 0.0) {
CurrentConditionsView(
viewModel: viewModel.currentConditionsViewModel
)
Divider()
ForecastView(
viewModel: viewModel.forecastViewModel
)
}
.navigationTitle(viewModel.locationName)
}
The preview doesn't display the name of the location. We fix this by wrapping the LocationView in a NavigationView.
struct LocationView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
LocationView(viewModel: .init(location: .preview))
}
}
}
Seeding the Current Conditions View with Data
The current conditions view and the forecast view are responsible for displaying the weather data for a location. Let's start with the current conditions view. The current conditions view displays the current temperature, the current wind speed, and a summary of the current conditions. Because we adopt the MVVM pattern, the data the current conditions view displays is provided by its view model.
Open CurrentConditionsViewModel.swift. We declare three computed properties of type String, summary, windSpeed, and temperature. We return string literals for now.
import Foundation
struct CurrentConditionsViewModel {
// MARK: - Public API
var summary: String {
"Clear"
}
var windSpeed: String {
"10 mi/h"
}
var temperature: String {
"90 °F"
}
}
Revisit CurrentConditionsView.swift. We replace the Text view with a VStack with a leading alignment. We also add a bit of padding to the VStack by applying the padding view modifier.
var body: some View {
VStack(alignment: .leading) {
}
.padding()
}
The VStack displays a Text view at the top. The Text view displays the temperature provided by the view model. To make the temperature stand out, we apply the font view modifier to the Text view, passing in largeTitle.
var body: some View {
VStack(alignment: .leading) {
Text(viewModel.temperature)
.font(.largeTitle)
}
.padding()
}
The Text views that display the wind speed and a summary of the current conditions have the same typography. To avoid code duplication, we create a Group and apply the font view modifier to the Group, passing in body.
var body: some View {
VStack(alignment: .leading) {
Text(viewModel.temperature)
.font(.largeTitle)
Group {
}
.font(.body)
}
.padding()
}
To display the wind speed, we create an HStack that contains an Image view and a Text view. The Image view displays a system image with name wind. We set its foreground color to gray. The Text view displays the wind speed provided by the view model.
var body: some View {
VStack(alignment: .leading) {
Text(viewModel.temperature)
.font(.largeTitle)
Group {
HStack {
Image(systemName: "wind")
.foregroundColor(.gray)
Text(viewModel.windSpeed)
}
}
.font(.body)
}
.padding()
}
We add another Text view that displays the summary of the current conditions. I would like to add a bit of spacing between the wind speed and the summary of the current conditions by inserting a Spacer view between the HStack and the Text view that displays the summary of the current conditions. We apply the frame view modifier to the Spacer view to set its height to 10.0 points.
var body: some View {
VStack(alignment: .leading) {
Text(viewModel.temperature)
.font(.largeTitle)
Group {
HStack {
Image(systemName: "wind")
.foregroundColor(.gray)
Text(viewModel.windSpeed)
}
Spacer()
.frame(height: 10.0)
Text(viewModel.summary)
}
.font(.body)
}
.padding()
}
Seeding the Forecast View with Data
Open ForecastView.swift. As I mentioned earlier, the forecast view displays a scroll view that contains a vertical grid. We used this pattern earlier in this series to populate the locations view. Replace the content of the forecast view with a ScrollView.
The content of the scroll view is a LazyVGrid, a vertical grid. The initializer accepts an array of grid items as its first argument and the content of the vertical grid as its second argument. The array of grid items defines the size and position of the rows of the vertical grid. We apply a bit of padding to the vertical grid.
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem()]) {
}
.padding()
}
}
We apply the same pattern we used for the locations view. The view model of the forecast view provides an array of view models. Each view model in the array drives an item of the vertical grid. We add a SwiftUI view to the Location group and name it ForecastCell. We also add a Swift file to the View Models group and name it ForecastCellViewModel.swift. Declare a struct with name ForecastCellViewModel.
import Foundation
struct ForecastCellViewModel {
}
To populate the vertical grid of the forecast view, we use the ForEach struct. We pass the initializer a collection of ForecastCellViewModel objects. As you may remember from earlier in this series, the type of the items of the collection needs to conform to the Identifiable protocol. This means the ForecastCellViewModel struct needs to conform to the Identifiable protocol. A type conforming to the Identifiable protocol needs to implement a property with name id that uniquely identifies the object or instance. We return a UUID for now.
import Foundation
struct ForecastCellViewModel: Identifiable {
// MARK: - Properties
var id: UUID {
UUID()
}
}
Revisit ForecastCell.swift, declare a constant property, viewModel, of type ForecastCellViewModel, and update the static previews property of the ForecastCell_Previews struct.
import SwiftUI
struct ForecastCell: View {
// MARK: - Properties
let viewModel: ForecastCellViewModel
// MARK: - View
var body: some View {
Text("Forecast Cell")
}
}
struct ForecastCell_Previews: PreviewProvider {
static var previews: some View {
ForecastCell(viewModel: .init())
}
}
Let's keep it simple for now and display the temperature for each day. Open ForecastCellViewModel.swift and declare a computed property, temperature, of type String. We generate a random temperature for each forecast cell, using the static random(in:) method of the Int struct, and use string interpolation to convert the temperature to a string.
var temperature: String {
let temperature = Int.random(in: 50...80)
return "\(temperature) °F"
}
Open ForecastCell.swift and replace the string of the Text view with the value returned by the computed temperature property of the view model.
var body: some View {
Text(viewModel.temperature)
}
Revisit ForecastViewModel.swift and declare a computed property that returns an array of ForecastCellViewModel objects. We name the property forecastCellViewModels. In the body of the computed property, we create a closed range from 0 to 10. We apply the map(_:) method to the range and return a ForecastCellViewModel object from the closure the map(_:) method accepts.
import Foundation
struct ForecastViewModel {
// MARK: - Properties
var forecastCellViewModels: [ForecastCellViewModel] {
(0..<10).map { _ in ForecastCellViewModel() }
}
}
Open ForecastView.swift. As I mentioned earlier, we populate the vertical grid using the ForEach struct. We pass the initializer of the ForEach struct the array of ForecastCellViewModel objects. The initializer accepts a view builder as its second argument. The view builder, a closure, accepts an item of the array of view models. In the view builder, we create a ForecastCell, passing the initializer the ForecastCellViewModel object.
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem()]) {
ForEach(viewModel.forecastCellViewModels) { viewModel in
ForecastCell(viewModel: viewModel)
}
}
.padding()
}
}
Revisit LocationView.swift and take a look at the preview. The current conditions view is displayed at the top and the forecast view is displayed at the bottom. The views are separated by a divider and the name of the location is displayed at the top.
What's Next?
We have applied the MVVM pattern serval times in this series and I hope you agree that it isn't difficult to implement. Later in this series, we add a bit more complexity when the application fetches weather data from a remote service. The fundamentals won't change, though.