Home > OS >  Changes made to environment variable in view not reflecting in class extension Swift
Changes made to environment variable in view not reflecting in class extension Swift

Time:08-28

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:

enter image description here Here's all the relevant code:

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