If you have been reading or watching Cocoacasts for some time, then you know that the Model-View-ViewModel pattern is one of my preferred patterns for architecting applications. I have been using MVVM for several years in various projects and it continues to prove its value in every project I work on.
With this and the next episode, I want to make sure you choose for the Model-View-ViewModel pattern for the right reasons. I want to avoid that you adopt MVVM in a project because you were told it is a sound architecture or because you think you need view models if you use SwiftUI. In this episode, I want to highlight a few problems a typical SwiftUI application can suffer from. Those problems can be resolved by the Model-View-ViewModel pattern with relative ease.
In the previous episode, I highlighted a few problems a typical SwiftUI application can suffer from. These problems can be resolved in several ways. In this series, we explore how the Model-View-ViewModel pattern solves these problems. The goal is to put the views of the application on a diet and decouple view logic from business logic.
In the previous episode, we solved some of the problems we discussed earlier in this series. At the same time, we introduced a few code smells. We address those code smells in this episode.
Remember that a view should be dumb. It doesn't care what it displays. The notes view doesn't match that description. It can access the array of notes through its view model. That is a code smell and something we need to change. You learn how to do that in this episode.
In the past few episodes, you learned how the Model-View-ViewModel pattern can transform a project and its architecture. In the remainder of this series, we build an application from scratch. Through that process, you deepen your understanding of the MVVM pattern and how it affects a project's architecture, testability, and structure.
In this episode, we set up the project for Thunderstorm, the weather application we build in this series. We make a few changes to prepare the project for the MVVM pattern.
With the project set up, we can focus on adding features to the weather application we are building. In this episode, we populate the locations view.
In the previous episode, we wrote quite a bit of code that we wouldn't have written if we were building a SwiftUI application without the Model-View-ViewModel pattern. This is fine since we have laid a foundation we can take advantage of in the next few episodes. In this episode, we continue to build out the user interface of the locations view by displaying weather data.
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.
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.
In this episode, we focus on the forecast view, a subview or child view of the location view. The forecast view displays the temperature for each day of the weather forecast. Let's add a few more details to the items of the vertical grid.
The locations view displays a static list of locations. While that has been useful to implement the user interface of the locations view, in the next few episodes we focus on adding locations.
In the next few episodes, we populate the AddLocationView. We break this task up into several smaller steps. We first populate the AddLocationView with stub data. Later in this series, we replace the stub data with data provided by the Core Location framework.
The AddLocationView displays stub data for the time being. We change that in this and the next episode by integrating the Core Location framework. Apple's Core Location framework defines an API for forward geocoding addresses. We use that API to convert an address to a collection of placemarks. The application converts the placemarks to Location objects the AddLocationView can display.
The GeocodingClient class returns stub data for the time being. In this episode, we integrate the Core Location framework and take advantage of the geocoding APIs it offers. We use the geocoding APIs to forward geocode the address the user enters in the TextField of the AddLocationView to a collection of `Location` objects.
In the previous episode, we made the Core Location framework a dependency of the project. That is fine, but we don't want a dependency to compromise the testability of the project. In this episode, we use a proven and familiar pattern to improve the testability of the GeocodingClient class.
The user can add locations using the AddLocationView, but there is room for improvement. The user experience isn't optimal at the moment. The AddLocationView doesn't show a progress view when the application is forward geocoding the address the user entered and the user is faced with an empty view if no matches are found. That is something we address in this episode.
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.
You may remember that the LocationsView displays a static list of locations. We need to change that if we want to put the AddLocationView to use. We keep it simple and store the list of locations in the user's defaults database. Let's get to work.
In this episode, we revisit the addLocation(with:) method of the AddLocationViewModel class. In that method, the view model stores the location the user selected in the user's defaults database.
The user's defaults database acts as the store of the weather application we are building. That is fine, but most objects shouldn't be aware of that implementation detail. The LocationsViewModel and AddLocationViewModel classes are tightly coupled to the UserDefaults class, but that isn't necessary. In this episode, we decouple the LocationsViewModel and AddLocationViewModel classes from the UserDefaults class.
It is time to focus on one of the core aspects of the weather application we are building, fetching and displaying weather data. Thunderstorm displays placeholder data for the time being. That is something we change in this and the next episodes.
In this episode, we add the ability to fetch weather data from the Clear Sky API and decode a WeatherData object from the API response. We apply a pattern that should feel familiar by now.
In the previous episode, we hard-coded the base URL and the API key of the Clear Sky API in the WeatherClient class. I'm not a fan of hard-coding configuration details. In this episode, we explore an alternative approach.
With the WeatherService protocol in place, we can refactor the view models that need to provide their views with weather data. In this episode, we focus on the LocationCell by refactoring the LocationCellViewModel class.
In the previous episode, we used the MeasurementFormatter class to format the raw values the Clear Sky API returns. This is convenient and we use the MeasurementFormatter class several more times in the next few episodes. In this episode, we create an elegant API that wraps around the MeasurementFormatter API to avoid code duplication.
In this episode, we refactor the CurrentConditionsViewModel struct. Remember that the CurrentConditionsViewModel struct drives the CurrentConditionsView, the top section of the LocationView. The changes we need to make are small.
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.
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.
The LocationView is always in one of two states, fetching weather data or displaying weather data. It is the LocationView itself that implicitly defines these states and that is limiting. Not only is it limiting, we cannot write unit tests for the states of the LocationView. In this episode, I show you a solution that is testable and extensible.
The last feature we need to add is the ability to delete locations. The changes we need to make are small, but there is a problem we need to address. Let's start by adding a button to the CurrentConditionsView.
We hit a roadblock in the previous episode. The CurrentConditionsViewModel struct needs access to a Store object, but it is tedious to pass a Store object from one view model to the next. In this episode, we use a dependency injection library, Swinject, to make this much less of a problem.
One of the key benefits of the Model-View-ViewModel pattern is improved testability. It isn't possible to write unit tests for the SwiftUI views we created, but we can unit test the view models that drive those views. The good news is that writing unit tests for a well-designed view model isn't difficult.
In the previous episode, we wrote unit tests for a simple view model, the AddLocationCellViewModel struct. In this episode, we take it up a notch and write unit tests for a complex view model. We take the AddLocationViewModel class as an example.
In this episode, we continue where we left off in the previous episode, that is, writing unit tests for the AddLocationViewModel class. This episode is an important one because it illustrates the impact asynchronous code can have on the unit tests you write.
In this episode, we finish the unit test we started in the previous episode. Even though the implementation of the addLocation(with:) method of the AddLocationViewModel class isn't overly complex, writing a unit test for it is quite the challenge. Let's continue where we left off.
I'd like to end this series by taking advantage of the recently introduced Observable macro. The changes we need to make are small and focused, but we also run into a few issues. Let's get started.