So I have this Cloud Firestore Setup, where each of the Image Documents have the same 4 fields, but with different values.
How can I then iterate over all of the Image Docs and get the GeoPint? So I later can display the Latitude and Longitude?
I Guess the iteration part isn’t THAT important, so antoher way of asking is, how can I get the Geopoint and then display it in my project?
Building on my last answer (https://stackoverflow.com/a/66377922/560942):
struct ImageModel {
var id = UUID()
var quote : String
var url: String
var position: GeoPoint
}
images = snapshot.documents.compactMap { documentSnapshot -> ImageModel? in
let documentData = documentSnapshot.data()
if let quote = documentData["Quote"] as? String,
let url = documentData["Url"] as? String,
let position = documentData["Position"] as? GeoPoint
{
return ImageModel(quote: quote, url: url, position: position)
} else {
return nil
}
}
Then, to display the text coordinates later on:
ForEach(images, id: \.id) { image in
Text("Location: \(image.position.latitude), \(image.position.longitude)")
}
Update for displaying the GeoPoint:
struct GeoPointView : View {
var position : GeoPoint
struct IdentifiablePoint: Identifiable {
var id = UUID()
var position : GeoPoint
}
var body: some View {
Map(coordinateRegion: .constant(
MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: position.latitude, longitude: position.longitude), span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))), annotationItems: [position].map { IdentifiablePoint(position: $0)}) { point in
MapPin(coordinate: CLLocationCoordinate2D(latitude: point.position.latitude, longitude: point.position.longitude))
}
.frame(width: 200, height: 200)
}
}
Usage:
ForEach(images, id: \.id) { image in
Text("Location: \(image.position.latitude), \(image.position.longitude)")
GeoPointView(position: image.position)
}
In the event you mean on a map, I can update this answer if needed, or you can refer to the following post on the Apple Developer Forums: https://developer.apple.com/forums/thread/651668
Related
I've been stuck trying to debug this for a while so i thought i would ask for some advice as i've got to the point where i've really confused myself. I'm trying to get data from a local api that i've created and use mapkit to display the events i get from my api on the map.
In my apps entry point i have this error "Type CommunityEventsApp does not conform to protocol 'App', it appears on the same line as struct CommunityEventsApp: App. I've trying adding an initialiser above my body in this file as this is what the error suggested as the fix however this didn't resolve the error. I have no other errors in my app. This is the code:
struct CommunityEventsApp: App {
#StateObject var viewModel: ContentViewModel
var event: Event
var body: some Scene {
WindowGroup {
TabView {
//rest of the tab view code
}
.environmentObject(viewModel)
}
}
}
I'm trying to get data from a local api i've made here in my ContentViewModel:
var eventsList: [Event]
var primary: Event
init() {
self.eventsList = []
self.primary = eventsList[0]
}
func getEvents() async throws {
guard let url = URL(string: "http://localhost:5172/events") else {fatalError("Missing URL")}
let urlRequest = URLRequest(url:url)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { fatalError("Error while fetching data")}
eventsList = try JSONDecoder().decode([Event].self, from:data)
print("Async decodedEvent", eventsList)
}
}
This is my Event struct
struct Event: Decodable, Identifiable {
let id: String
let title: String
let date: String
let time: String
let location: String
let latitude: Double
let longitude: Double
let price: Double
let description: String
let link: String?
let imageUrl: String
init(id: String,
title: String,
date: String,
time: String,
location: String,
latitude: Double,
longitude: Double,
price: Double,
description: String,
link: String,
imageUrl: String) {
self.id = id
self.title = title
self.date = date
self.time = time
self.location = location
self.latitude = latitude
self.longitude = longitude
self.price = price
self.description = description
self.link = link
self.imageUrl = imageUrl
}
}
I call getEvents inside the onAppear in the ContentView:
let event: Event
let viewModel: ContentViewModel
var body: some View {
// UI formatting here
.onAppear {
Task {
do {
try await viewModel.getEvents()
} catch {
print("Error", error)
}
}
}
}
.navigationTitle("Event Information")
}
}
}
MapView where i use my api data to display events on the map:
#EnvironmentObject var events: ContentViewModel
#State var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 53.483959, longitude: -2.244644),
span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
)
var body: some View {
Map(coordinateRegion: $region,
annotationItems: events.eventsList) {
event in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude)) {
NavigationLink(destination: ContentView(event: event, viewModel: ContentViewModel())) {
Image(systemName: "pin.fill")
.onHover { hover in
print(event.title)
}
}
}
}
.navigationTitle("Events Near You")
}
}
I don't have anyone else that i can ask for help so any help or information would be greatly appreciated! I'm still a beginner with Swift development and don't really feel comfortable with it yet. I'm using xcode version 13 and swift version 5
Your properties need default values.
The App struct is the entry point in your application. So every property needs to be properly initialized.
Especially your #StateObject var.
I pulled the data from the mapmarker, when I print, I see all of them, but I cannot get them from the example ( quake.latitude ). Error. Even if it was a list, I had taken it by typing "(quakes) { quake in" in parentheses, but now I don't know how to do it through mapview.
struct MapAnnotationsView: View {
#State var quakes: [EarthQuake] = []
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.9520281, longitude: 35.6980142), span: MKCoordinateSpan(latitudeDelta: 30, longitudeDelta: 10))
let placeArray: [Place] = [Place(title: {quake.latitude}, coordinate: CLLocationCoordinate2D(latitude: 37.8008, longitude: 27.2465))]
var body: some View {
Map(coordinateRegion: $region, annotationItems: placeArray) { annotation in
// This makes a generic annotation that takes a View
MapAnnotation(coordinate: annotation.coordinate) {
// This is your custom view
AnnotationView(placeName: annotation.title)
}
} .onAppear {
Api().getEarthQuake { (quakes) in
self.quakes = quakes
}
}
}
}
Error code screenshot
Try this:
struct MapAnnotationsView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.9520281, longitude: 35.6980142), span: MKCoordinateSpan(latitudeDelta: 30, longitudeDelta: 10))
#State private var placeArray: [Place] = []
var body: some View {
Map(coordinateRegion: $region, annotationItems: placeArray) { annotation in
// This makes a generic annotation that takes a View
MapAnnotation(coordinate: annotation.coordinate) {
// This is your custom view
AnnotationView(placeName: annotation.title)
}
} .onAppear {
Api().getEarthQuake { (quakes) in
let tempArray = quakes.map{ quake in
Place(title: "\(quake.latitude)", coordinate: CLLocationCoordinate2D(latitude: 37.8008, longitude: 27.2465))
}
self.placeArray = tempArray
}
}
}
}
As this is not a reproducible example there might be some typos involved here which you would have to address yourself.
Explenation:
Your property placeArray cannot depend on another property. To get an array of places to provide to your map you need to create these after you loaded the earthquakes from your API and assign them. Using a #State var ensures your View gets updated.
I have a simple map made with "pure" SwiftUI. There is a search bar to search a place and when I click the "Go" button it shows the instructions of how to go that place from a particular location. It shows it below the map, on the "Enter a destination" field. What I want to do is, I want these instructions to be clickable. When I click each of the instructions it should zoom in that particular place where the instruction takes place. Right now it's only a list of text. Is it possible to do it without using UIViewRepresentable? And how can I do it?
I tried with
.onTapGesture {
region.span = MKCoordinateSpan(latitudeDelta: region.span.latitudeDelta/2, longitudeDelta: region.span.longitudeDelta/2)
}
but it zooms in the same location on every instruction I click.
ContentView
struct Location: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
struct RouteSteps: Identifiable {
let id = UUID()
let step: String
}
struct ContentView: View {
#State private var searchBar: String = ""
#State private var home = CLLocationCoordinate2D(latitude: 39.90068, longitude: 32.86081)
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 39.90068, longitude: 32.86081), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
#State var routeSteps: [RouteSteps] = [RouteSteps(step: "Enter a destination")]
#State var annotations = [Location(name: "Ankara", coordinate: CLLocationCoordinate2D(latitude: 39.90068, longitude: 32.86081))]
var body: some View {
VStack{
HStack {
TextField("", text: $searchBar)
Button("Go") {
findNewLocation()
}
.frame(width: 35, height: 35)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}.textFieldStyle(.roundedBorder).colorInvert()
Map(coordinateRegion: $region, annotationItems: annotations){ item in
MapMarker(coordinate: item.coordinate)
}.frame(width: 400, height: 300)
List(routeSteps) { r in
Text(r.step)
}
route function in ContentView
func findNewLocation(){
let searchResult = searchBar
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(searchResult, completionHandler:
{(placemarks, error) -> Void in
if((error) != nil){
print("error at geocode")
}
if let placemark = placemarks?.first {
let coordinates : CLLocationCoordinate2D = placemark.location!.coordinate
region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
annotations.append(Location(name: placemark.name!, coordinate: coordinates))
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: home, addressDictionary: nil))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: coordinates, addressDictionary: nil))
request.requestsAlternateRoutes = false
request.transportType = .automobile
let directions = MKDirections(request: request)
directions.calculate(completionHandler: { response, error in
for route in (response?.routes)! {
self.routeSteps = []
for step in route.steps {
self.routeSteps.append(RouteSteps(step: step.instructions))
}
}
})
}
})
}
Yes, but you have to stop throwing away the data. MKRoute.Steps actually gives you all of the information you need, so first, set up RouteSteps to accept MKRoute.Steps:
struct RouteSteps: Identifiable {
let id = UUID()
let step: MKRoute.Step
}
Then, in your findNewLocation() function:
func findNewLocation(){
...
directions.calculate(completionHandler: { response, error in
// I used a guard statement to get rid of your force unwrap.
// NEVER force unwrap in a situation like this.
// Implement error handling here
guard let response = response else { return }
for route in response.routes {
// Map the MKRoute.Steps to your struct
self.routeSteps = route.steps.map( { RouteSteps(step: $0) } )
}
})
...
}
Then in the main part of your ContentView:
struct ContentView: View {
...
var body: some View {
...
List(routeSteps) { routeStep in
// Pull your instructions out here
Text(routeStep.step.instructions)
.onTapGesture {
// The MKRoute.Step contains an overlay polyline for MKMapKit.
// We can't use that in SwiftUI maps yet, but the polyline contains a
// boundingMapRect that we can use to change the region shown on the map
// with the MKCoordinateRegion(_ MKMapRect) initializer. Setting the region
// to the insturction's polyline boundingMapRect does exactly what you want.
region = MKCoordinateRegion(routeStep.step.polyline.boundingMapRect)
}
}
...
}
I'm trying to use the new Map in SwiftUI. I would like to change the visible region
programmatically to include all of the annotations that are added programmatically,
similar to the way you can show a bounding box for a route overlay in the older UIMap.
I have not found anything in the documentation to do this. I created my own approach
by getting the min/max values for the latitude and longitude of each annotation and
then creating a center for the new region and a span. Both of these involve some math.
My result works for my North America location, but I will need to add significant
complexity to handle the cases where the annotation area encompasses the equator, the
date line or the prime meridian. Before I do so, I'm hoping someone has a solution
that I have missed.
Here is my solution:
struct MyMapView: View {
#StateObject var myMapVM = MyMapViewModel()
var body: some View {
VStack {
let m = Map(coordinateRegion: $myMapVM.region, annotationItems: myMapVM.centers) { site in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: site.lat, longitude: site.long)) {
GroupAnnotationPinView(title: site.name)
.onTapGesture {
openMapWithCoordinate(coordinate: CLLocationCoordinate2D(latitude: site.lat, longitude: site.long), name: site.name)
}//on tap
}//map annotation
}//map
m.onAppear {
//this works, so reference to Map seems to work ok
//print(m.body)
}
Button(action: {
myMapVM.createCoordinateRegion()
}, label: {
Image(systemName: "square.and.pencil")
.resizable()
.frame(width: 44, height: 44)
.padding()
})
.padding(.bottom, 20)
}//v
.ignoresSafeArea()
}//body
func openMapWithCoordinate(coordinate: CLLocationCoordinate2D, name: String) {
let place = MKPlacemark(coordinate: coordinate)
let mapItem = MKMapItem(placemark: place)
mapItem.name = name
mapItem.openInMaps(launchOptions: nil)
}//open
}//my map view
struct GroupAnnotationPinView: View {
#State private var showTitle = true
let title: String
var body: some View {
VStack(spacing: 0) {
Image(systemName: "mappin")
.font(.title)
.foregroundColor(.red)
}//v
}//body
}//group anno view
And the View Model
class MyMapViewModel: ObservableObject {
#Published var annotations: [MKAnnotation] = []
#Published var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.334_900,
longitude: -122.009_020),
latitudinalMeters: 10000,
longitudinalMeters: 10000
)
let centers: [Center] = [
.init(name: "One", lat: 37.334, long: -122.009),
.init(name: "Two", lat: 37.380, long: -122.010),
.init(name: "Three", lat: 37.400, long: -122.010),
.init(name: "Four", lat: 40.000, long: -120.000)
//.init(name: "Four", lat: 37.600, long: -121.800)
]
func createCoordinateRegion() {
//you need to fix this to account for dateline, prime meridian and equator in span
let maxX = centers.max(\.lat)
let maxY = centers.max(\.long)
let minX = centers.min(\.lat)
let minY = centers.min(\.long)
//print("minX.lat is ", minX?.lat ?? "nil")
//print("maxX.lat is ", maxX?.lat ?? "nil")
//print("minY.long is ", minY?.long ?? "nil")
//print("maxY.long is ", maxY?.long ?? "nil")
guard let minXS = minX?.lat, let maxXS = maxX?.lat, let minYS = minY?.long, let maxYS = maxY?.long else { return }
let deltaX = maxXS - minXS
let deltaY = maxYS - minYS
let newCenterLat = minXS + deltaX / 2
let newCenterLong = maxYS - abs(deltaY / 2)
//print(newCenterLat)
//print(newCenterLong)
let newRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: newCenterLat, longitude: newCenterLong), span: MKCoordinateSpan(latitudeDelta: max(deltaX, deltaY), longitudeDelta: max(deltaX, deltaY)))
region = newRegion
}//create coord region
}//class
extension Sequence {
func max<T: Comparable>(_ predicate: (Element) -> T) -> Element? {
self.max(by: { predicate($0) < predicate($1) })
}
func min<T: Comparable>(_ predicate: (Element) -> T) -> Element? {
self.min(by: { predicate($0) < predicate($1) })
}
}// ext seq
struct Center: Identifiable, Hashable {
let id = UUID()
let name: String
let lat: Double
let long: Double
}//center
Any guidance would be appreciated: Xcode 13.2.1 iOS 15.2
I do this by converting each annotation coordinate to an MKMapRect:
let rect = MKMapRect(origin: MKMapPoint(coordinate), size: MKMapSize(width: 0, height: 0))
MKMapRect is able to calculate a union and knows how to handle the 180th Meridian (see method spans180thMeridian:
private(set) var union: MKMapRect? = nil
func union(_ rect: MKMapRect) {
guard let union = self.union else {
self.union = rect
return
}
self.union = union.union(rect)
}
After you're done, you can convert it into a region and use it:
let region = MKCoordinateRegion(union)
Be aware that this cuts the AnnotationViews at the edge in half, so you have to calculate some space around it.
Alternative solution:
This is one of MANY reasons to prefer the old MKMapKit where you just call
mapView.showAnnotations(specificAnnotations, animated: true)
This changes the region to show specificAnnotations while keeping the other annotations on the map and not cutting AnnotationViews in half.
I'm trying to add all the addresses returned from an API on to a map. Unfortunately the API doesn't return long/lat coordinates but I can retrieve them from the geocodeAddressString which when I place in the List outputs it correctly.
struct Locations: Decodable {
let _id: Int
let streetaddress: String?
let suburb: String?
let state: String?
let postcode: String?
func getCoordinates(handler: #escaping ((CLLocationCoordinate2D) -> Void)) {
if let address = streetaddress, let suburb = suburb, let postcode = postcode, let state = state {
CLGeocoder().geocodeAddressString("\(address) \(suburb), \(state) \(postcode)") { ( placemark, error ) in
handler(placemark?.first?.location?.coordinate ?? CLLocationCoordinate2D())
}
}
}
I have the network call going into a class (so I can use and call the data from other screens):
// minimised info
final class ModelData: ObservableObject {
#Published var locations: [ModelRecord] = []
func getLocationData() {
// call the network
self.locations = locations
}
}
So in my main view I have a map which if I use normally is working okay (without annotations). But when I try to cycle the annotations in from the getCoordinates() function then it says it doesn't conform - which I'm assuming because of the loop in the loop.
struct MapView: View {
#StateObject var mapViewModel = MapViewModel() // loads the map init
#StateObject var modelData = ModelData() // loads the api data
var body: some View {
Map(
coordinateRegion: $mapViewModel.region,
interactionModes: .all,
showsUserLocation: true,
annotationItems: modelData.locations,
annotationContent: { location in
location.getCoordinates() { i in
MapPin(coordinate: i)
}
}
)
.onAppear { modelData. getLocationData() } // load data
}
}
Is there any way to fix this so I can have locations appearing on the map? Everything I've read and watched is all about the reverse (having coordinates and getting address names).
In your code, coordinates are only useful for MapPin (otherwise I imagine you would have created a coordinates property in your Locations struct).
In this case you could create a Pin struct (Identifiable to be able to be used by Map):
struct Pin: Identifiable {
var coordinate: CLLocationCoordinate2D
let id = UUID()
}
Now your View MapView, no longer displays an array of Locations, but an array of Pin. Its State is therefore a [Pin] (1). Which is first empty (we have no coordinates), and which is filled as we get the results of GeoCoder (2).
struct MapView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 48.862725, longitude: 2.287592), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
#StateObject var modelData = ModelData() // loads the api data
#State private var pins: [Pin] = [] // (1)
var body: some View {
Map(
coordinateRegion: $region,
interactionModes: .all,
showsUserLocation: true,
annotationItems: pins,
annotationContent: { pin in
MapPin(coordinate: pin.coordinate)
}
)
.onAppear { modelData.getLocationData() } // load data
.onChange(of: modelData.locations.isEmpty) { _ in
for location in modelData.locations {
location.getCoordinates { coordinate in
print("et voilà !")
pins.append(Pin(coordinate: coordinate)) // (2)
}
}
}
}
}
The struct Locations remains unchanged.
I tested with these data :
final class ModelData: ObservableObject {
#Published var locations: [Locations] = []
func getLocationData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.locations = [.init(_id: 1, streetaddress: "11 rue Vineuse", suburb: "", state: "France", postcode: "75016"), .init(_id: 2, streetaddress: "11 rue Chardin", suburb: "", state: "France", postcode: "75016"), .init(_id: 3, streetaddress: "11 avenue Kléber", suburb: "", state: "France", postcode: "75016")]
}
}
}
EDIT : IOS15
For the first version, I was careful not to modify your Model. But we can now try to do better.
First, we will make sure that Map can take an array of Locations directly. We make Locations identifiable, and we add a coordinate property to it :
struct Locations: Decodable, Identifiable {
let _id: Int
let streetaddress: String?
let suburb: String?
let state: String?
let postcode: String?
var id: Int { _id }
var coordinate: CLLocationCoordinate2D? = nil
private enum CodingKeys: CodingKey {
case _id, streetaddress, suburb, state, postcode
}
}
We take this opportunity to remove the function that uses Geocoder. Rather, it has its place in the class that fetch your data.
So I move it, and I take the opportunity to use async / await. This version will only work with Xcode13 / iOS15:
#available(iOS 15.0, *)
final class ModelData: ObservableObject {
#Published var locations: [Locations] = []
#MainActor
func fetchLocationsWithCoordinates() async {
let locations = await getLocationData()
return await withTaskGroup(of: Locations.self) { group in
for location in locations {
group.async {
await self.updateCoordinate(of: location)
}
}
for await location in group {
self.locations.append(location)
}
}
}
private func updateCoordinate(of location: Locations) async -> Locations {
var newLoc = location
newLoc.coordinate = try? await CLGeocoder().geocodeAddressString(
"\(location.streetaddress ?? "") \(location.suburb ?? ""), \(location.state ?? "") \(location.postcode ?? "")"
).first?.location?.coordinate
//await Task.sleep(1_000_000_000)
return newLoc
}
private func getLocationData() async -> [Locations] {
//await Task.sleep(4_000_000_000)
return [.init(_id: 1, streetaddress: "11 rue Vineuse", suburb: "", state: "France", postcode: "75016"), .init(_id: 2, streetaddress: "11 rue Chardin", suburb: "", state: "France", postcode: "75016"), .init(_id: 3, streetaddress: "11 avenue Kléber", suburb: "", state: "France", postcode: "75016")]
}
}
Now in the View, I can call the function that retrieves the Locations and their coordinates, with the .task() modifier.
#available(iOS 15.0, *)
struct SwiftUIView15: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 48.862725, longitude: 2.287592), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
#StateObject var modelData = ModelData() // loads the api data
var body: some View {
Map(
coordinateRegion: $region,
interactionModes: .all,
showsUserLocation: true,
annotationItems: modelData.locations,
annotationContent: { pin in
MapPin(coordinate: pin.coordinate ?? CLLocationCoordinate2D())
}
)
.task {
await modelData.fetchLocationsWithCoordinates()
}
}
}
It's cleaner.