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)
}
}
}
}
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)
}