UIViewRepresentable & MVVM

I am trying to get my head around how to implement a MapKit view using UIViewRepresentable (I want the map to rotate to align with heading, which Map() can't handle yet to my knowledge). I am also playing with making my LocationManager an Actor and setting up a listener. But when combined with UIViewRepresentable this seems to create a rather convoluted data flow since the @State var of the vm needs to then be passed and bound in the UIViewRepresentable. And the listener having this for await location in await lm.$lastLocation.values seems at least like a code smell. That double await just feels wrong. But I am also new to Swift so perhaps what I have here actually is a good approach?

struct MapScreen: View {
    @State private var vm = ViewModel()
    
    var body: some View {
        VStack {
            MapView(vm: $vm)
        }
        .task {
            vm.startWalk()
        }
    }
}

extension MapScreen {
    @Observable
    final class ViewModel {
        private var lm = LocationManager()
        private var listenerTask: Task<Void, Never>?
        
        var course: Double = 0.0
        var location: CLLocation?
        
        func startWalk() {
            Task {
                await lm.startLocationUpdates()
            }
            listenerTask = Task {
                for await location in await lm.$lastLocation.values {
                    await MainActor.run {
                        if let location {
                            withAnimation {
                                self.location = location
                                self.course = location.course
                            }
                        }
                    }
                }
            }
            Logger.map.info("started Walk")
        }
        
    }
    
    struct MapView: UIViewRepresentable {
        @Binding var vm: ViewModel
        
        func makeCoordinator() -> Coordinator {
            Coordinator(parent: self)
        }
        
        func makeUIView(context: Context) -> MKMapView {
            let view = MKMapView()
            view.delegate = context.coordinator
            view.preferredConfiguration = MKHybridMapConfiguration()
            return view
        }
        
        func updateUIView(_ view: MKMapView, context: Context) {
            context.coordinator.parent = self
            if let coordinate = vm.location?.coordinate {
                if view.centerCoordinate != coordinate {
                    view.centerCoordinate = coordinate
                }
            }
        }
    }
    
    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView
        init(parent: MapView) {
            self.parent = parent
        }
    }
}

actor LocationManager{
    private let clManager = CLLocationManager()
    private(set) var isAuthorized: Bool = false
    
    private var backgroundActivity: CLBackgroundActivitySession?
    private var updateTask: Task<Void, Never>?
    
    @Published var lastLocation: CLLocation?
    
    func startLocationUpdates() {
        updateTask = Task {
            do {
                backgroundActivity = CLBackgroundActivitySession()
                let updates = CLLocationUpdate.liveUpdates()
                for try await update in updates {
                    if let location = update.location {
                        lastLocation = location
                    }
                }
            } catch {
                Logger.location.error("\(error.localizedDescription)")
            }
        }
    }
    
    func stopLocationUpdates() {
        updateTask?.cancel()
        updateTask = nil
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch clManager.authorizationStatus {
        case .authorizedAlways, .authorizedWhenInUse:
            isAuthorized = true
            // clManager.requestLocation() // ??
        case .notDetermined:
            isAuthorized = false
            clManager.requestWhenInUseAuthorization()
        case .denied:
            isAuthorized = false
            Logger.location.error("Access Denied")
        case .restricted:
            Logger.location.error("Access Restricted")
        @unknown default:
            let statusString = clManager.authorizationStatus.rawValue
            Logger.location.warning("Unknown Access status not handled: \(statusString)")
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        Logger.location.error("\(error.localizedDescription)")
    }
}
Answered by DTS Engineer in 815461022

And the listener having this for await location in await lm.$lastLocation.values seems at least like a code smell. That double await just feels wrong.

On the specific point, I don't think you need to be concerned about it. An await keyword represents a potential suspension point, and there are two separate suspension point locations in an asynchronous for construct.

The first is the await <collection to iterate over> which is a straightforward async expression, evaluated once before the loop itself is entered.

The second is the for await <loop variable> syntax, which is a special declaration syntax. In effect, it signals that the loop body implicitly begins with a suspension point, which is hit during every iteration.

It seems conceptually correct that there are two different "sources" of extension points here and therefore two await keywords to signal that. It just looks weird when everything is written in a single line.

Accepted Answer

And the listener having this for await location in await lm.$lastLocation.values seems at least like a code smell. That double await just feels wrong.

On the specific point, I don't think you need to be concerned about it. An await keyword represents a potential suspension point, and there are two separate suspension point locations in an asynchronous for construct.

The first is the await <collection to iterate over> which is a straightforward async expression, evaluated once before the loop itself is entered.

The second is the for await <loop variable> syntax, which is a special declaration syntax. In effect, it signals that the loop body implicitly begins with a suspension point, which is hit during every iteration.

It seems conceptually correct that there are two different "sources" of extension points here and therefore two await keywords to signal that. It just looks weird when everything is written in a single line.

UIViewRepresentable &amp; MVVM
 
 
Q