Home > Blockchain >  Stop subscriber (sink) from running multiple times
Stop subscriber (sink) from running multiple times

Time:10-28

In my CoordinatesViewModel file i have function addWaypoint that gets called when user presses the button. In LocationManager file i have function that gets users location and then stops receiving location updates. To access location from LocationManager file i use .sink (i am new to Swift so i don’t know if .sink is the right way about doing this). Now the problem i noticed is that .sink sometimes runs twice or more so the same result gets added to array. This usually happens the second time i press the button.

Here is the example what i get from print into console when i run the app:

hello from ViewModel
hello from LocationManager
hello from ViewModel

LocationManager:

    private let manager = CLLocationManager()
    @Published var userWaypoint: CLLocation? = nil
 
    override init(){
        super.init()
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.requestWhenInUseAuthorization()
        manager.delegate = self
    }

    func getCurrentUserWaypoint(){
        wasWaypointButtonPressed = true
        manager.requestWhenInUseAuthorization()
        manager.startUpdatingLocation()
    }

extension LocationManager: CLLocationManagerDelegate{
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let userWaypoint = locations.last  else {return}
        print("hello from LocationManager")
        DispatchQueue.main.async {
            self.userWaypoint = userWaypoint
        }
        self.manager.stopUpdatingLocation()
    }
}

CoordinatesViewModel:

private let locationManager: LocationManager
var cancellable: AnyCancellable?
@Published var waypoints: [CoordinateData] = []


init() {
    locationManager = LocationManager()
}

func addWaypoint(){
    locationManager.getCurrentUserWaypoint()
    cancellable = locationManager.$userWaypoint.sink{ userWaypoint in
        if let userWaypoint = userWaypoint{
            print("hello from ViewModel")
            DispatchQueue.main.async{
                let newWaypoint = CoordinateData(coordinates: userWaypoint.coordinate)
                self.waypoints.append(newWaypoint)
            }
        }
    }
}

CodePudding user response:

If you only want to receive one value you can use:

func first() -> Publishers.First<Self>`

Publishes the first element of a stream, then finishes.

Because $userWaypoint publishes CLLocation? you also need to use:

func compactMap<T>((Self.Output) -> T?) -> Publishers.CompactMap<Self, T>

Calls a closure with each received element and publishes any returned optional that has a value.

With all of the above:

cancellable = locationManager
    .$userWaypoint
    .compactMap { $0 } // remove nils
    .first() // ensure that only one value gets emitted
    .map { CoordinateData(coordinates: $0.coordinate) } // map to the type you need
    .receive(on: RunLoop.main) // switch to the main thread
    .sink { [weak self] waypoint in
        self?.waypoints.append(waypoint)
    }

CodePudding user response:

First of all rather than dispatching the result to the main thread insert the Combine operator

.receive(on: DispatchQueue.main)

You could simply cancel the pipeline after getting the first result


For a one-shot-publisher I recommend a different approach with Swift Concurrency. The Continuation allows to adopt async/await in a delegate environment

class LocationManager: NSObject, ObservableObject {
    
    enum LocationError : Error {  case noLocationFound }
    
    private let manager = CLLocationManager()
    
    private var locationContinuation: CheckedContinuation<CLLocation, Error>?
    
    override init() {
        super.init()
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.requestWhenInUseAuthorization()
        manager.delegate = self
    }
    
    func getCurrentUserWaypoint() async throws -> CLLocation {
        try await withCheckedThrowingContinuation { continuation in
            locationContinuation = continuation
            manager.startUpdatingLocation()
        }
    }
}

extension LocationManager: CLLocationManagerDelegate {
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let userWaypoint = locations.last else {
            locationContinuation?.resume(throwing: LocationError.noLocationFound)
            return
        }
        print("hello from LocationManager")
        manager.stopUpdatingLocation()
        locationContinuation?.resume(returning: userWaypoint)
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        manager.stopUpdatingLocation()
        locationContinuation?.resume(throwing: error)
    }
}

And CoordinateViewModel is

@MainActor
class CoordinateViewModel : ObservableObject {
    private let locationManager: LocationManager
    @Published var waypoints: [CoordinateData] = []

    init() {
        locationManager = LocationManager()
    }

    func addWaypoint(){
        Task {
            do {
                let newLocation = try await locationManager.getCurrentUserWaypoint()
                print("hello from ViewModel")
                self.waypoints.append(CoordinateData(coordinates: newLocation.coordinate))
            } catch {
                print(error) // show it to the user
            }
        }
    }
}

Another benefit is that you don't need to care about the main thread, the @MainActor does it on your behalf.

Always handle errors and show them to the user.

  • Related