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.
Adding a Delete Button
Open CurrentConditionsView.swift and navigate to the computed body property. Before we can add the delete button, we need to make a few changes to the layout of the CurrentConditionsView. Wrap the contents of the VStack in an HStack and add a Spacer below the VStack to push the contents of the VStack to the left.
var body: some View {
HStack {
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)
}
Spacer()
}
.padding()
}
Add a Button below the Spacer. The initializer of the Button accepts an action, a closure, and a view builder that returns a Label. In the action, we invoke the view model's delete() method. We implement this method in a moment.
In the view builder, we create an Image view that displays a system image with name trash. We set its foreground color to the application's accent color.
var body: some View {
HStack {
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)
}
Spacer()
Button {
viewModel.delete()
} label: {
Image(systemName: "trash")
.foregroundColor(.accentColor)
}
}
.padding()
}
Deleting Locations
Let's implement the view model's delete() method. Open CurrentConditionsViewModel.swift and declare a method with name delete().
// MARK: - Public API
func delete() {
}
In the delete() method, we remove the location with the help of a Store object. This is only possible if the CurrentConditionsViewModel struct has access to the Location object and a Store object. Declare a private, constant property with name location of type Location. Declare another private, constant property with name store of type Store.
import Foundation
struct CurrentConditionsViewModel {
// MARK: - Properties
private let location: Location
private let store: Store
private let currently: WeatherData.CurrentConditions
...
}
We use the initializer to inject the Location and Store objects into the view model. This should look familiar by now.
// MARK: - Initialization
init(
location: Location,
store: Store,
currently: WeatherData.CurrentConditions
) {
self.location = location
self.store = store
self.currently = currently
}
Before we can implement the delete() method, we need to extend the Store protocol. Open Store.swift and declare a method with name removeLocation(_:). The method accepts a Location object as its only argument and is throwing.
import Combine
protocol Store {
// MARK: - Properties
var locationsPublisher: AnyPublisher<[Location], Never> { get }
// MARK: - Methods
func addLocation(_ location: Location) throws
func removeLocation(_ location: Location) throws
}
Revisit the delete() method of the CurrentConditionsViewModel struct. The removeLocation(_:) method of the Store protocol is throwing so we first add a do-catch statement to the body of the delete() method. In the do clause, we invoke the removeLocation(_:) method of the Store object, passing in the Location object. In the catch clause, we print the error to the console.
// MARK: - Public API
func delete() {
do {
try store.removeLocation(location)
} catch {
print("Unable to Remove Location \(error)")
}
}
Conforming to the Store Protocol
The UserDefaults class and the PreviewStore struct no longer conform to the Store protocol. Let's fix that. Open UserDefaults+Helpers.swift and declare a method with name removeLocation(_:)
func removeLocation(_ location: Location) throws {
}
Copy the implementation of the addLocation(_:) method as a starting point. We need to make one change. Instead of appending a Location object to the array of Location objects, we remove the Location object that is passed to the removeLocation(_:) method by invoking the removeAll(where:) method on the array of Location objects. The removeAll(where:) method accepts a closure as its only argument. The method iterates through the array, invoking the closure for each element. If the closure returns true, the element is removed. If the closure returns false, the element isn't removed. In the closure, we compare the identifiers of the Location objects. If they match, the closure returns true and the Location object is removed from the array.
func removeLocation(_ location: Location) throws {
var locations = try decode(
[Location].self,
forKey: Keys.locations
) ?? []
locations.removeAll { $0.id == location.id }
try encode(
locations,
forKey: Keys.locations
)
}
Open PreviewStore.swift and implement the removeLocation(_:) method. The implementation is similar to that of the UserDefaults class. The only difference is that the PreviewStore object doesn't persist the array of Location objects. We invoke the removeAll(where:) method on the array of Location objects to remove the Location object that is passed to the removeLocation(_:) method.
func removeLocation(_ location: Location) throws {
locations.removeAll { $0.id == location.id }
}
Injecting the Store
Before we can test the implementation, we need to pass a Store object to the initializer of the CurrentConditionsViewModel class. That is something we have done several times in this series, but there is a problem. The LocationViewModel class is responsible for creating the CurrentConditionsViewModel object and it doesn't have access to a Store object either. That means we need to pass a Store object to the initializer of the LocationViewModel class.
It doesn't stop there, though. The LocationCellViewModel class is responsible for creating the LocationViewModel instance and it doesn't have access to a Store object either. Dependency injection is great and indispensable for a loosely coupled architecture, but the solution we have been using so far has outgrown the project.
What's Next?
In the next episode, I show you how to use Swinject, an open source library, to resolve the problem we are facing. Know that there are several solutions to this problem and Swinject is just one of them.