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.
Defining the Geocoding Service
The AddLocationViewModel class manages an array of locations, but it won't be responsible for forward geocoding the address the user entered in the TextField. That is the responsibility of a geocoding service. By extracting that functionality into a separate type, the AddLocationViewModel class remains testable and the preview of the AddLocationView isn't coupled to the Core Location framework.
We apply a pattern we used earlier in this series. We define a protocol for the geocoding service. We create two types that conform to the protocol, a client for the preview and a client for the application. Add a group and name it Geocoding. Add a Swift file to the Geocoding group and name it GeocodingService.swift. Declare a protocol with name GeocodingService.
import Foundation
protocol GeocodingService {
}
The API of the GeocodingService protocol is simple. It defines one method with name geocodeAddressString(_:). The method accepts one argument of type String, an address, and it returns an array of Location objects. The method is asynchronous and throwing.
import Foundation
protocol GeocodingService {
// MARK: - Methods
func geocodeAddressString(_ addressString: String) async throws -> [Location]
}
Add another Swift file to the Geocoding group and name it GeocodingPreviewClient.swift. Declare a struct with name GeocodingPreviewClient that conforms to the GeocodingService protocol.
import Foundation
struct GeocodingPreviewClient: GeocodingService {
}
To conform to the GeocodingService protocol, the GeocodingPreviewClient struct needs to implement the geocodeAddressString(_:) method. Because the GeocodingPreviewClient struct is only used for previews, we can keep its implementation simple. The geocodeAddressString(_:) method returns the stub data we defined earlier in this series.
import Foundation
struct GeocodingPreviewClient: GeocodingService {
// MARK: - Geocoding Service
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
Location.previews
}
}
We add one more Swift file to the Geocoding group and name it GeocodingClient.swift. Declare a final class with name GeocodingClient that conforms to the GeocodingService protocol and copy the implementation of the GeocodingPreviewClient struct. We integrate the Core Location framework into the GeocodingClient class in the next episode.
import Foundation
final class GeocodingClient: GeocodingService {
// MARK: - Geocoding Service
func geocodeAddressString(_ addressString: String) async throws -> [Location] {
Location.previews
}
}
Integrating the Geocoding Service
Open AddLocationViewModel.swift and add an import statement for the Combine framework at the top. We add two properties to the AddLocationViewModel class. Declare a private, constant property with name geocodingService and type GeocodingService.
import Combine
import Foundation
internal final class AddLocationViewModel: ObservableObject {
// MARK: - Properties
private let geocodingService: GeocodingService
...
}
We also implement an initializer that accepts an object that conforms to the GeocodingService protocol. In the body of the initializer, we store a reference to the GeocodingService object in the geocodingService property.
// MARK: - Initialization
init(geocodingService: GeocodingService) {
self.geocodingService = geocodingService
}
The second property we add is a private, variable property with name subscriptions and type Set<AnyCancellable>. We set its initial value to an empty set. The view model uses the subscriptions property to manage the subscriptions it creates.
private var subscriptions: Set<AnyCancellable> = []
Let's add a bit of reactive programming to the mix. In the initializer, we invoke a private helper method, setupBindings().
// MARK: - Initialization
init(geocodingService: GeocodingService) {
self.geocodingService = geocodingService
setupBindings()
}
The implementation of the setupBindings() method is short and simple. The view model attaches a subscriber to the query publisher by invoking the sink(_:) method. The value handler we pass to the sink(_:) method accepts the address the user entered as an argument. We weakly reference the view model in the value handler and invoke another helper method, geocodeAddressString(_:), that accepts an address as an argument. We invoke the store(in:) method to add the AnyCancellable object the sink(_:) method returns to the set of subscriptions.
// MARK: - Helper Methods
private func setupBindings() {
$query
.sink { [weak self] addressString in
self?.geocodeAddressString(addressString)
}.store(in: &subscriptions)
}
We can make two optimizations. First, we filter out empty strings using the filter operator. It makes no sense to forward geocode an empty string.
// MARK: - Helper Methods
private func setupBindings() {
$query
.filter { !$0.isEmpty }
.sink { [weak self] addressString in
self?.geocodeAddressString(addressString)
}.store(in: &subscriptions)
}
Second, we apply the throttle operator to limit the number of values the query publisher emits in a period of time. The throttle operator is nothing more than a function that accepts three arguments, a time interval, a scheduler, and a boolean value. The throttle operator guarantees that only a single value is published in the time interval we pass to the throttle operator, one second in this example. The last argument, a boolean value, defines whether the resulting publisher emits the first value or the last value that was emitted in the time interval.
// MARK: - Helper Methods
private func setupBindings() {
$query
.throttle(for: 1.0, scheduler: RunLoop.main, latest: true)
.filter { !$0.isEmpty }
.sink { [weak self] addressString in
self?.geocodeAddressString(addressString)
}.store(in: &subscriptions)
}
Let's implement the geocodeAddressString(_:) method next. In the geocodeAddressString(_:) method, we invoke the geocodeAddressString(_:) method of the geocoding service. Because that method is asynchronous and throwing, we invoke the method in a Task and wrap the method call in a do-catch statement. In the do clause, we assign the result of the geocodeAddressString(_:) method to the view model's locations property. In the catch clause, we print the error to the console.
private func geocodeAddressString(_ addressString: String) {
Task {
do {
locations = try await geocodingService.geocodeAddressString(addressString)
} catch {
print("Unable to Geocode \(addressString) \(error)")
}
}
}
Before we move forward, we set the initial value of the locations property to an empty array.
@Published private(set) var locations: [Location] = []
Injecting the Geocoding Service
The AddLocationViewModel class defines an initializer that accepts an object that conforms to the GeocodingService protocol and that means we need to make a few changes. Open AddLocationView.swift and navigate to the AddLocationView_Previews struct. In the body of the static previews property, we inject a GeocodingPreviewClient instance into the view model and use the view model to create an AddLocationView.
struct AddLocationView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = AddLocationViewModel(
geocodingService: GeocodingPreviewClient()
)
return AddLocationView(
viewModel: viewModel,
showsAddLocationView: .constant(true)
)
}
}
Open LocationsView.swift and navigate to the closure we pass to the sheet view modifier. As I mentioned earlier in this series, a view shouldn't be responsible for creating view models. Open LocationsViewModel.swift and declare a computed property with name addLocationViewModel of type AddLocationViewModel. In the body of the computed property, we create an AddLocationViewModel instance, passing a GeocodingClient instance to the initializer.
var addLocationViewModel: AddLocationViewModel {
AddLocationViewModel(geocodingService: GeocodingClient())
}
Revisit LocationsView.swift and navigate to the closure we pass to the sheet view modifier. The view no longer creates the view model for the AddLocationView. The view model is provided by the view's view model.
.sheet(isPresented: $showsAddLocationView) {
AddLocationView(
viewModel: viewModel.addLocationViewModel,
showsAddLocationView: $showsAddLocationView
)
}
Fixing a Threading Issue
Run the application in the simulator, navigate to the AddLocationView, and enter the name of a city. Tap the plus button of one of the locations. Everything seems to work fine. There is one problem, though. With the application still running, open the Issue Navigator on the left. Xcode reports several runtime issues.

We invoke the geocodeAddressString(_:) method of the geocoding service in a Task and update the view model's locations property in that same Task. This takes place on a background thread. Because the locations publisher indirectly drives the AddLocationView, the view is updated from that background thread and that is a red flag.
The solution is surprisingly simply thanks to the main actor. Open AddLocationViewModel.swift and annotate the declaration of the AddLocationViewModel class with the MainActor attribute. This ensures that the locations property is updated on the main thread and, as a result, the view is also updated on the main thread.
import Combine
import Foundation
@MainActor
internal final class AddLocationViewModel: ObservableObject {
...
}
We also need to apply the MainActor attribute to the declaration of the LocationsViewModel class. Why is that? Open LocationsViewModel.swift and navigate to the computed addLocationViewModel property. In the computed property, the view model creates an instance of the AddLocationViewModel class. To ensure that this too occurs on the main thread, we apply the MainActor attribute to the declaration of the LocationsViewModel class. It is very common for view models to run on the main actor because they drive the user interface.
import Foundation
@MainActor
struct LocationsViewModel {
...
}
Run the application one more time in the simulator, navigate to the AddLocationView, and enter the name of a city. Tap the plus button of one of the locations. Open the Issue Navigator on the left. You should no longer see runtime issues.
What's Next?
In the next episode, we integrate the Core Location framework into the GeocodingClient class. We use the framework to forward geocode the address the user enters in the TextField of the AddLocationView to a collection of Location objects.