Combine & CoreLocation, Part 2 - Receiving & Handling Events

leogdion - Aug 31 '20 - - Dev Community

In our previous post, we talked about how to build a Publisher Factory or Publicist. In this post, we'll talk about how receiving and handling Events in our ObservableObject from our new Publicist class.

Receiving and Handling Events in our ObservableObject

Our ObservableObject needs to accomplish a few things to update our SwiftUI while listening to our CoreLocation publishers. First of all we need to:

  1. Create the CoreLocation Manager
  2. Create our CoreLocation Publicist
  3. Connect the Publicist as the Delegate of the Manager
  4. Create our Authorization Status and Location Publishers

With this in mind, here is how that looks:

class CoreLocationObject: ObservableObject {
  @Published var authorizationStatus = CLAuthorizationStatus.notDetermined
  @Published var location: CLLocation?

  let manager: CLLocationManager
  let publicist: CLLocationManagerCombineDelegate

  var cancellables = [AnyCancellable]()

  init() {
    let manager = CLLocationManager()
    let publicist = CLLocationManagerPublicist()

    manager.delegate = publicist

    self.manager = manager
    self.publicist = publicist

    let authorizationPublisher = publicist.authorizationPublisher()
    let locationPublisher = publicist.locationPublisher()
    ...

Next, we can actually deal with receiving and handling events from our Publishers. Firstly, let’s deal with the CLAuthorizationStatus.

Receiving and Handling Events with Sink and Assign

As far the CLAuthorizationStatus, there are two things we need to do when the authorization status changes:

  • Trigger Location Update when CLAuthorizationStatus is valid
  • Assign the CLAuthorizationStatus to our @Published CLAuthorizationStatus Property authorizationStatus

Using Sink For Calling Methods

Let’s first deal with triggering location updates by creating a method to begin those updates:

class CoreLocationObject: ObservableObject {
  @Published var authorizationStatus = CLAuthorizationStatus.notDetermined
  @Published var location: CLLocation?

  let manager: CLLocationManager

...

  func beginUpdates(_ authorizationStatus: CLAuthorizationStatus) {
    if authorizationStatus == .authorizedAlways || authorizationStatus == .authorizedWhenInUse {
      manager.startUpdatingLocation()
    }
  }
}

Our beginUpdate method will take in the CLAuthorizedStatus as it comes in from the Publisher. As a result, if the status allows for location update then it calls startUpdatingLocation on the CLLocationManager.

With this in mind, let’s connect the authorizationPublisher to the beginUpdates method:

class CoreLocationObject: ObservableObject {

  ...
  var cancellables = [AnyCancellable]()

  init() {
    let publicist = CLLocationManagerPublicist()

    ...

    let authorizationPublisher = publicist.authorizationPublisher()

    // trigger an update when authorization changes
    authorizationPublisher
      .sink(receiveValue: beginUpdates)
      .store(in: &cancellables)
    ...

In this case, we are using sink and passing a reference to our new method. sink returns a Cancellable which we’ll need to make sure is disposed properly. Therefore, we use .store to make sure that Cancellable is stored correctly in our cancelables\ Array.

Now we should be correctly starting location updates when permission is granted. Next, let’s store the CLAuthorizationStatus in our @Published property so we can display it in our SwiftUI view.

Receiving and Handling Events with Assign (in iOS 13 and iOS 14)

One of the first things you learn as an iOS developer using UIKit is the importance of making UI updates on the main dispatch queue. In other words, failure to do this results in incorrect UI updates or even crashing your app. Likewise, in SwiftUI, you need to specify UI updates on the main dispatch queue. Therefore we can use receive to ensure updates are on the main DispatchQueue.

...
      authorizationPublisher
        // since this is used in the UI,
        //  it needs to be on the main DispatchQueue
        .receive(on: DispatchQueue.main)
        ...

As far as assigning we have a few options, we can stick with sink to assign our value the published property:

...
      authorizationPublisher
        // since this is used in the UI,
        //  it needs to be on the main DispatchQueue
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: {
          self.authorizationStatus = $0
        })
...

However, we could simplify this with assign:

...
      authorizationPublisher
        // since this is used in the UI,
        //  it needs to be on the main DispatchQueue
        .receive(on: DispatchQueue.main)
        // store the value in the authorizationStatus property
        .assign(to: &$authorizationStatus)
...

This new assign available in all the new 2020 OSes (iOS 14, macOS 11, watchOS 7, etc…) allows us to assign the value directly to the publisher using the & (for an in-out parameter) and $ (for the publisher).

Additionally no Cancellable is returned and therefore not needed to be stored.

Sink vs Assign when Receiving and Handling Events

Choosing the correct method can be difficult but here's some guidelines:

In the next blog post, I'll cover some more advance functional programming techniques in Combine when we transform our CLLocation to something more useful.

. . . . . . . . . . . . . . . . . . . . .