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.