The LocationView still displays stub data. That is something we change in this episode. The LocationViewModel struct is responsible for fetching the weather data the LocationView displays. It uses that weather data to create view models for the CurrentConditionsView and the ForecastView.

Fetching Weather Data

Open LocationViewModel.swift and declare a private, constant property with name weatherService of type WeatherService.

import Foundation

struct LocationViewModel {

    // MARK: - Properties

    private let location: Location

    // MARK: -

    private let weatherService: WeatherService

	...

}

We inject the WeatherService object through the initializer of the LocationViewModel struct. Add a parameter to the initializer with name weatherService of type WeatherService. In the body of the initializer, we store a reference to the WeatherService object in the weatherService property.

// MARK: - Initialization

init(
    location: Location,
    weatherService: WeatherService
) {
    self.location = location
    self.weatherService = weatherService
}

Declare a method with name start(). The method is asynchronous. The view model fetches weather data by invoking the weather(for:) method of WeatherService object. That method is throwing so we wrap it in a do-catch statement. We store the return value of the weather(for:) method in a constant with name data. In the catch clause, we print the error to the console.

// MARK: - Public API

func start() async {
    do {
        let data = try await weatherService.weather(for: location)
    } catch {
        print("Unable to Fetch Weather Data \(error)")
    }
}

The view model uses the weather data to create view models for the CurrentConditionsView and the ForecastView. For that to work, we need to make a few changes to the currentConditionsViewModel and forecastViewModel properties. We change the properties from computed properties to stored properties, declaring their setter privately. We apply the Published property wrapper and change the type of the properties to an optional type.

@Published private(set) var currentConditionsViewModel: CurrentConditionsViewModel?
@Published private(set) var forecastViewModel: ForecastViewModel?

The compiler throws two errors that should look familiar. Remember that the we can only apply the Published property wrapper to properties of classes. We resolve this issue by making the LocationViewModel struct a final class.

import Foundation

final class LocationViewModel {

	...

}

We also conform the LocationViewModel class to the ObservableObject protocol and annotate the class declaration with the MainActor attribute to make sure the Published properties we declared a moment ago are guaranteed to be updated on the main thread. Remember that we ran into a similar issue earlier in this series.

import Foundation

@MainActor
final class LocationViewModel: ObservableObject {

	...

}

Revisit the start() method. In the do clause, we use the WeatherData object to create a CurrentConditionsViewModel object and a ForecastViewModel object, and update the currentConditionsViewModel and forecastViewModel properties.

// MARK: - Public API

func start() async {
    do {
        let data = try await weatherService.weather(for: location)

        currentConditionsViewModel = .init(currently: data.currently)
        forecastViewModel = .init(forecast: data.forecast)
    } catch {
        print("Unable to Fetch Weather Data \(error)")
    }
}

Updating the Location View

Open LocationView.swift. Apply the ObservedObject property wrapper to the viewModel property and make the viewModel property a variable.

import SwiftUI

struct LocationView: View {

    // MARK: - Properties

    @ObservedObject var viewModel: LocationViewModel

	...

}

In the closure we pass to the VStack, we use an if statement to safely access the values of the view model's currentConditionsViewModel and forecastViewModel properties. We move the initialization of the CurrentConditionsView and the ForecastView to the if clause and pass the view models to the initializers. In the else clause, we create a ProgressView.

var body: some View {
    VStack(alignment: .leading, spacing: 0.0) {
        if
            let currentConditionsViewModel = viewModel.currentConditionsViewModel,
            let forecastViewModel = viewModel.forecastViewModel
        {
            CurrentConditionsView(
                viewModel: currentConditionsViewModel
            )

            Divider()

            ForecastView(
                viewModel: forecastViewModel
            )
        } else {
            ProgressView()
        }
    }
    .navigationTitle(viewModel.locationName)
}

To start the view model, we create a Task using the task view modifier. In the closure we pass to the task view modifier, we invoke the start() method of the view model.

var body: some View {
    VStack(alignment: .leading, spacing: 0.0) {
        ...
    }
    .navigationTitle(viewModel.locationName)
    .task {
        await viewModel.start()
    }
}

Injecting the Weather Service

Navigate to the LocationView_Previews struct. We need to update the initialization of the LocationViewModel instance. Create a WeatherPreviewClient object and pass it to the initializer of the LocationViewModel class.

struct LocationView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            LocationView(
                viewModel: .init(
                    location: .preview,
                    weatherService: WeatherPreviewClient()
                )
            )
        }
    }
}

Open LocationCellViewModel.swift and navigate to the computed locationViewModel property. In the body of the computed property, we pass the WeatherService object to the initializer of the LocationViewModel class.

var locationViewModel: LocationViewModel {
    .init(
        location: location,
        weatherService: weatherService
    )
}

Build and run the application. Tap one of the locations to navigate to the LocationView. The application fetches weather data from the Clear Sky API, displaying the weather data in the CurrentConditionsView at the top and the ForecastView at the bottom.

What's Next?

In this episode, we completed the integration with the Clear Sky API. The application now fetches and displays weather data using a weather service.