UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

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.

1      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.