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.