I'm relatively new to Swift and have been working on an app that tracks a user's location and displays the route they took on a map.
I've gotten it to a stage where location updates are published to a CoreData database and the route is being traced on a map, however when I try to toggle the location publisher using a variable named trackingState, I've found that this change is not reflected by the view, even though changing its initial value in my code does work to change between active and inactive tracking states.
Sorry for my lack of knowledge on this topic, but any help at all would be greatly appreciated :)
Here is what happens in the simulator:
MyApp.swift
import Combine
import SwiftUI
@main
struct MyApp: App {
var cancellables = [AnyCancellable]()
init() {
RouteManager()
.sink(receiveValue: PersistenceController.shared.add).store(in: &cancellables)
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
}
}
ContentView.swift
@StateObject var routeManager: RouteManager = RouteManager()
var body: some View {
ZStack(alignment: .bottom) {
switch selectedTab {
case .home:
HomeView()
case .route:
RouteView(model: model)
case .trips:
HomeView()
}
TabBar()
.offset(y: model.fullScreen ? 200 : 0)
}
.environmentObject(routeManager)
}
RouteView.swift
import SwiftUI
import MapKit
import CoreData
struct RouteView: View {
@ObservedObject var model: Model
@EnvironmentObject var routeManager: RouteManager
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Location.timestamp, ascending: true)], animation: .default)
private var locations: FetchedResults<Location>
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0),
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
var body: some View {
ZStack {
Color("Background").ignoresSafeArea()
VStack {
Spacer()
.frame(height: 150)
Map(coordinateRegion: $region, interactionModes: [.zoom], showsUserLocation: true, userTrackingMode: .constant(.follow), annotationItems: locations) { location in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)) {
Circle().fill(Color.blue).frame(width: 10, height: 10)
}
}
.cornerRadius(50, corners: [.topLeft, .topRight])
.shadow(color: .primary.opacity(0.15), radius: 20, x: -5, y: -5)
.edgesIgnoringSafeArea(.all)
}
Button {
routeManager.toggleTrip()
} label: {
ZStack {
Rectangle()
.foregroundColor(.red)
Text("toggle")
.foregroundColor(.white)
}
}
.frame(width: 100, height: 50)
}
.overlay(NavigationBar(title: "New Trip", hasScrolled: .constant(false)))
}
}
RouteManager.swift
import Foundation
import MapKit
import Combine
import CoreLocation
import SwiftUI
class RouteManager: NSObject, ObservableObject {
@Published var trackingState: TrackingState = .active
private var locationManager: CLLocationManager!
typealias Output = (longitude: Double, latitude: Double)
typealias Failure = Never
private let wrapped = PassthroughSubject<(Output), Failure>()
override init() {
super.init()
locationManagerConfig()
}
private func locationManagerConfig() {
locationManager = CLLocationManager()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
public func startRoute() {
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.requestAlwaysAuthorization()
self.locationManager.allowsBackgroundLocationUpdates = true
self.trackingState = .active
}
public func stopRoute() {
locationManager.allowsBackgroundLocationUpdates = false
trackingState = .inactive
}
public func toggleTrip() {
if trackingState == .active {
stopRoute()
} else {
startRoute()
}
}
}
extension RouteManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// This is the code to control whether or not to send the location updates
if trackingState != .active { return }
guard let location = locations.last else { return }
wrapped.send((longitude: location.coordinate.longitude, latitude: location.coordinate.latitude))
}
}
extension RouteManager: Publisher {
func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
wrapped.subscribe(subscriber)
}
}
TrackingState.swift
enum TrackingState {
case inactive
case active
}
CodePudding user response:
You are using two different instances of RouteManager.
For your code to work properly, you shall inject one instance to the Content View like so:
MyApp.swift
@main
struct MyApp: App {
@StateObject private var routeManager: RouteManager
var cancellables = [AnyCancellable]()
init() {
// Creates a new instance of RouterManager (the one and only)
let routeManager = RouteManager()
// Stores the newly created instance in the StateObject property
_routeManager = .init(wrappedValue: routeManager)
// Subscribes to the events of RouteManager
routeManager
.sink(receiveValue: PersistenceController.shared.add).store(in: &cancellables)
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
.environmentObject(routeManager) // important! This passes down the one and only instance further down as an EnvironmentObject that can be retrieved as shown in ContentView.swift
}
}
}
ContentView.swift
@EnvironmentObject private var routeManager: RouteManager // Here we are accessing the passed down instance as an EnvironmentObject from MyApp struct.
var body: some View {
ZStack(alignment: .bottom) {
switch selectedTab {
case .home:
HomeView()
case .route:
RouteView(model: model)
case .trips:
HomeView()
}
TabBar()
.offset(y: model.fullScreen ? 200 : 0)
}
.environmentObject(routeManager)
}