Swift Combine's CombineLatest does not fire in response to an update to one of its publishers
Asked Answered
T

1

5

I am combining two publishers to determine what the center coordinate of a map view should be. The two publishers are:

  1. The user's initial location determined by a CLLocationManager (the first location reported once the CLLocationManager begins sending location updates).
  2. The user's current location if the "center map on current location" button is tapped.

In code:

    class LocationManager: NSObject, ObservableObject {

        // The first location reported by the CLLocationManager.
        @Published var initialUserCoordinate: CLLocationCoordinate2D?
        // The latest location reported by the CLLocationManager.
        @Published var currentUserCoordinate: CLLocationCoordinate2D?
        // What the current map view center should be.
        @Published var coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 42.35843, longitude: -71.05977) // Default coordinate.

        // A subject whose `send(_:)` method is being called elsewhere every time the user presses a button to center the map on the user's location.
        var centerButtonTappedPublisher: PassthroughSubject<Bool, Never> = PassthroughSubject<Bool, Never>()

        // The combined publisher that is where all my troubles lie.
        var coordinatePublisher: AnyPublisher<CLLocationCoordinate2D, Never> {
            Publishers.CombineLatest($initialUserCoordinate, centerButtonTappedPublisher)
                .map { initialCoord, centerButtonTapped in
                    var latestCoord = initialCoord
                    if centerButtonTapped {
                        latestCoord = self.currentUserCoordinate
                    }
                    return latestCoord
                }
                .replaceNil(with: CLLocationCoordinate2D(latitude: 42.35843, longitude: -71.05977))
                .eraseToAnyPublisher()
        }

        private var cancellableSet: Set<AnyCancellable> = []

        //... Other irrelevant properties

        private override init() {
            super.init()

            coordinatePublisher
                .receive(on: RunLoop.main)
                .assign(to: \.coordinate, on: self)
                .store(in: &cancellableSet)

            //... CLLocationManager set-up
        }
    }

    extension LocationManager: CLLocationManagerDelegate {

        //...

        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            // We are only interested in the user's most recent location.
            guard let location = locations.last else { return }
            let latestCoord = location.coordinate
            if initialUserCoordinate == nil {
                initialUserCoordinate = latestCoord
            }
            currentUserCoordinate = latestCoord
        }

        //...

    }

Both publishers, $initialUserCoordinate and centerButtonTappedPublisher, publish updates - I have confirmed this. However, the combined publisher coordinatePublisher only fires when the "center map on current location" button is tapped. It never fires when the initialUserCoordinate property is first set.

This question suggests adding a .receive(on: RunLoop.main) after the Publishers.CombineLatest($initialUserCoordinate, centerButtonTappedPublisher) but this does not work for me.

What am I doing wrong?

Thirtyeight answered 25/3, 2020 at 7:9 Comment(0)
D
7

You need to use Publishers.Merge rather than CombineLatest, see the documentation:

For Publishers.CombineLatest:

A publisher that receives and combines the latest elements from two publishers.

For Publishers.Merge

A publisher that emits an event when either upstream publisher emits an event.

Deficient answered 25/3, 2020 at 15:9 Comment(1)
Thanks @Sajjon! That does the trick. I had thought that CombineLatest would publish when any of its publishers publish, but apparently all need to publish a new value first. Thanks again!Thirtyeight

© 2022 - 2024 — McMap. All rights reserved.