BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

Need help binding a map view to EnvironmentObject Properties

Forums > SwiftUI

Hey,

I have an API that spits out some GPS data to a GET endpoint.

I created a decodable struct that holds this information, and have a method that decodes the information correctly.

I have a class of type @ObservableObject that has an @Published property of this struct type. I set an instance of this class as an environmentObject in my @main view.

This looks like this

struct ApiResponse: Decodable
{
    let validData: Bool
    let data: LocationData
}

struct LocationData: Decodable, Identifiable
{
    var id: UUID
    let mode: Int?
    let fixStatus: Int?
    let utcTime: String?
    var latitude: Double?
    var longitude: Double?
    let altitude: Double?
    let speed: Double?
    let course: Double?
    let fixMode: Int?
    let hDOP: Double?
    let pDOP: Double?
    let vDOP: Double?
    let sattelitesInView: Int?
    let sattelitesInUse: Int?
    let cN0Max: String?
    let hPA: Int?
    let vPA: Int?
    let hasData: Bool
}

class DeviceData: ObservableObject {

    @Published var mostRecentLocation: LocationData = LocationData(id: UUID(), mode: nil, fixStatus: nil, utcTime: nil, latitude: 0, longitude: 0, altitude: 0, speed: nil, course: nil, fixMode: nil, hDOP: nil, pDOP: nil, vDOP: nil, sattelitesInView: nil, sattelitesInUse: nil, cN0Max: nil, hPA: nil, vPA: nil, hasData: false)

    func getMostRecentLocation() { ... //does the api call }
}

@main
struct Location_ViewerApp: App {

    //Source of truth for GPS Data
    var deviceGPSData = DeviceData()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(deviceGPSData)
        }
    }
}

I am really struggling with binding this information to a mapview in the application. My initial hack was to call the 'getMostRecentLocation()' method when the Map() view is displayed... then update an @state property in that view. But as that call happens all async and whatnot... the method wasn't updating the view.

I did work around this with a hacky little button that updates the view, then re-calls the method.... so we are always one call behind. This is setting the location to the data gathered when the view loaded. If I hit the button again, it will show the data loaded when the button was pressed this time etc... which is pretty trash. I'd like the view to update automatically if that EnvironmentObject updates, so I can add code to poll the API in the background and wanted the map to 'bind' to that information so that the map will follow the updated data. That view looks like this now.

EDIT: I've removed some unneeded code, and just put it all here for clarity.

@EnvironmentObject var deviceGPSData: DeviceData
@State private var pins: [LocationData] = []
@State private var mapData: MKCoordinateRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 0, longitude: 0),
                                                                    span: MKCoordinateSpan(latitudeDelta: 1,longitudeDelta: 1))

var body: some View {
    NavigationView {
        VStack {
            Map(coordinateRegion: $mapData,
                            annotationItems: pins,
                            annotationContent: {
                            datapoint in  MapPin(coordinate: CLLocationCoordinate2D(
                                latitude: datapoint.latitude!,
                                longitude:datapoint.longitude!), tint: .red)})
                .onAppear {
                    //Kick off the async operation that updates the @EnvironmentObject
                    deviceGPSData.getMostRecentLocation();
                }
            Button(action: {
                withAnimation{
                    //Set the map data to the (now updated) environment object values
                    mapData.center = CLLocationCoordinate2D(latitude: deviceGPSData.mostRecentLocation.latitude ?? 0, longitude: deviceGPSData.mostRecentLocation.longitude ?? 0)
                    mapData.span = MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1)

                    //Sort pins
                    if(pins.count > 0) { pins.remove(at: 0) }
                    pins.append(deviceGPSData.mostRecentLocation)

                    //Kick off a new update - So next time we hit this button we will have the latest data
                    deviceGPSData.getMostRecentLocation();
                }
            }, label: { Text("Update")
                    .bold()
                    .frame(width: 250, height: 50, alignment: .center)
                    .cornerRadius(8)
            })
        }
    }
}

I can't seem to use the environment object information for the map as it expects Binding<> objects/properties. Any help here would be great. I must just be using some wrong pattern or something.

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.