In this episode, we refactor the ForecastViewModel struct and the ForecastCellViewModel struct. The ForecastViewModel struct drives the ForecastView, the bottom section of the LocationView. Let's get to work.

Refactoring the Forecast Cell View Model

Open ForecastCellViewModel.swift and remove the forecastDate property. We no longer need it. Declare a private, constant property with name dayConditions of type WeatherData.DayConditions.

import Foundation

struct ForecastCellViewModel: Identifiable {

    // MARK: - Properties

    private let dayConditions: WeatherData.DayConditions

	...

}

Navigate to the initializer of the ForecastCellViewModel struct. Remove the forecastDate parameter and add a parameter with name dayConditions of type WeatherData.DayConditions. In the body of the initializer, we assign the WeatherData.DayConditions object to the dayConditions property.

import Foundation

struct ForecastCellViewModel: Identifiable {

    // MARK: - Properties

    private let dayConditions: WeatherData.DayConditions

	...

    // MARK: - Initialization

    init(dayConditions: WeatherData.DayConditions) {
        self.dayConditions = dayConditions
    }

}

In the computed day and date properties, we replace the references to the forecastDate property we removed. We access the date of the weather data through the time property of the WeatherData.DayConditions object.

var day: String {
    dateFormatter.dateFormat = "EEEE"
    return dateFormatter.string(from: dayConditions.time).capitalized
}

var date: String {
    dateFormatter.dateFormat = "MMMM d"
    return dateFormatter.string(from: dayConditions.time).capitalized
}

The next computed property we update is summary. In the body of the computed summary property, we return the value of the summary property of the WeatherData.DayConditions object.

var summary: String {
    dayConditions.summary
}

The computed imageName property requires a bit more work. In the body of the computed imageName property, we switch on the value of the icon property of the WeatherData.DayConditions object. The icon property is of type String. We use the switch statement to map the value of the icon property to the name of an SF symbol.

var imageName: String {
    switch dayConditions.icon {
    case "clear-day":
        return "sun.max"
    case "clear-night":
        return "moon"
    case "rain":
        return "cloud.rain"
    case "snow":
        return "cloud.snow"
    case "sleet":
        return "cloud.sleet"
    case "wind",
         "cloudy",
         "partly-cloudy-day",
         "partly-cloudy-night":
        return "cloud"
    default:
        return "sun.max"
    }
}

For the computed windSpeed, lowTemperature, and highTemperature properties, we need a ClearSkyFormatter object. Declare a private, constant property with name measurementFormatter. We create a ClearSkyFormatter object and assign the object to the measurementFormatter property.

private let measurementFormatter = ClearSkyFormatter()

In the computed windSpeed property, we invoke the formatWindSpeed(_:) method of the ClearSkyFormatter object, passing in the value of the windSpeed property of the WeatherData.DayConditions object.

var windSpeed: String {
    measurementFormatter.formatWindSpeed(dayConditions.windSpeed)
}

The implementations of the computed lowTemperature and highTemperature properties are similar. We invoke the formatTemperature(_:) method of the ClearSkyFormatter object, passing in the value of the respective property of the WeatherData.DayConditions object.

var lowTemperature: String {
    measurementFormatter.formatTemperature(dayConditions.temperatureLow)
}

var highTemperature: String {
    measurementFormatter.formatTemperature(dayConditions.temperatureHigh)
}

Refactoring the Forecast View Model

Open ForecastViewModel.swift and declare a private, constant property with name forecast of type [WeatherData.DayConditions].

import Foundation

struct ForecastViewModel {

    // MARK: - Properties

    private let forecast: [WeatherData.DayConditions]

	...

}

We declare an initializer that defines a parameter with name forecast of type [WeatherData.DayConditions]. In the body of the initializer, we store the array of WeatherData.DayConditions objects in the forecast property.

// MARK: - Initialization

init(forecast: [WeatherData.DayConditions]) {
    self.forecast = forecast
}

We also need to update the computed forecastCellViewModels property. In the body, we invoke the map(_:) method on the forecast property, passing in a reference to the initializer of the ForecastCellViewModel struct.

import Foundation

struct ForecastViewModel {

    // MARK: - Properties

    private let forecast: [WeatherData.DayConditions]

    // MARK: -

    var forecastCellViewModels: [ForecastCellViewModel] {
        forecast.map(ForecastCellViewModel.init(dayConditions:))
    }

    // MARK: - Initialization

    init(forecast: [WeatherData.DayConditions]) {
        self.forecast = forecast
    }

}

Updating the Location View Model

Open LocationViewModel.swift. We need to pass an array of WeatherData.DayConditions objects to the initializer of the ForecastViewModel struct. We use stub data for now.

import Foundation

struct LocationViewModel {

    // MARK: - Properties

    private let location: Location

    // MARK: -

    var currentConditionsViewModel: CurrentConditionsViewModel {
        .init(currently: WeatherData.preview.currently)
    }

    var forecastViewModel: ForecastViewModel {
        .init(forecast: WeatherData.preview.forecast)
    }

	...

}

Fixing the Previews

We also need to fix the previews of the ForecastView and the ForecastCell. Open ForecastView.swift and navigate to the ForecastView_Previews struct. We pass stub data to the initializer of the ForecastViewModel struct.

struct ForecastView_Previews: PreviewProvider {
    static var previews: some View {
        ForecastView(
            viewModel: .init(
                forecast: WeatherData.preview.forecast
            )
        )
    }
}

Open ForecastCell.swift and navigate to the ForecastCell_Previews struct. We pass stub data to the initializer of the ForecastCell struct.

struct ForecastCell_Previews: PreviewProvider {
    static var previews: some View {
        ForecastCell(
            viewModel: .init(
                dayConditions: WeatherData.preview.forecast[0]
            )
        )
    }
}

Build the Thunderstorm target and verify that no errors or warnings pop up.

What's Next?

In the next episode, we shift focus to the LocationViewModel struct. In that episode, we fetch weather data from the Clear Sky API and display it in the LocationView.