Adding MapAnnotations in SwiftUI 2 not conforming to MapAnnotationProtocol - ios

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.

Related

How do I get a Swift UI Picker to update to the selected value when pulling these values from a database?

Problem
My Picker won't update when I select a new value.
When I click/tap another value, the picker closes and displays "Austin", no matter what I choose.
I am using:
Xcode Version 14.2
I have provided three pieces of code below:
Swift UI View that displays the Picker
Data Model
JSON Data File
Below is a screenshot of the picker:
Here is the code with the Picker:
import SwiftUI
struct AddEditExpenseView: View {
#EnvironmentObject var destinationsModelData: DestinationsModelData
var destinationIndex: Int {
destinationsModelData.destinations.firstIndex(where: { $0.id == destination.id })!
}
var destination: Destination
#State var selectedDestination: String = ""
#State var saveDestinationFieldTextArray: [String] = []
var body: some View {
NavigationView {
Form {
Section(header: Text("Destination")) {
Picker("Destination", selection: $selectedDestination) {
ForEach(destinationsModelData.destinations) { destination in
Text(destination.name)
}
}
.onAppear {
selectedDestination = destination.name
}
.foregroundColor(.black)
}
}
.formStyle(GroupedFormStyle())
.accentColor(.black)
.navigationBarTitle(Text("Add, Edit Expense"))
.navigationBarBackButtonHidden(true)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct AddEditExpenseView_Previews: PreviewProvider {
static let destinationsModelData = DestinationsModelData()
static var previews: some View {
AddEditExpenseView(destination: destinationsModelData.destinations[0])
.environmentObject(destinationsModelData)
}
}
Here is the Destination Model:
import Foundation
import SwiftUI
import CoreLocation
struct Destination: Hashable, Codable, Identifiable {
var id: Int
var name: String
var city: String
var state: String
var country: String
var description: String
var isOpen: Bool
var isCompared: Bool
private var imageName: String
var image: Image {
Image(imageName)
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D (
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
I have a JSON file that stores the data:
[
{
"name": "Austin",
"category": "Cities",
"city": "Austin",
"state": "Texas",
"country": "USA",
"id": 1001,
"isOpen": true,
"isCompared": true,
"coordinates": {
"longitude": -97.743057,
"latitude": 30.267153
},
"description": "placeholder text",
"imageName": "Austin_TX"
},
{destination2},
{destination3}
]
I tried to use a #FetchRequest but I get an error message that #FetchRequest is not supported.
I tried .onChange with no luck. I couldn't get to the compiling stage here.
To make your Picker display the selection, the #State var selectedDestination: String must
match the type of the .tag() of the Picker element. Such as
Picker("Destination", selection: $selectedDestination) {
ForEach(destinationsModelData.destinations) { destination in
Text(destination.name).tag(destination.name) // <-- here
}
}
This is assuming all destination.name are unique.
A better approach would be to use the .tag(destination.id), and #State var selectedDestination: Int

How do i resolve the error Type 'CommunityEventsApp' does not conform to protocol 'App'

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.

SwiftUI Map Change Region Programmatically to Show Any Annotations Added

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.

SwiftUI: How to fetch GeoPoint from Cloud Firestore and display it?

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

Creating MapView from decoded coordinates - swift

I am setting up a MapView with custom coordinates. Here is my code thus far
var coordModel: AirportModel?
struct MapView: View {
#Binding private var lat: Double
#Binding private var lon: Double
private let initialLatitudinalMetres: Double = coordModel?.airportLat ?? 0
private let initialLongitudinalMetres: Double = coordModel?.airportLong ?? 0
#State private var span: MKCoordinateSpan?
init(lat: Binding<Double>, lon: Binding<Double>) {
_lat = lat
_lon = lon
}
private var region: Binding<MKCoordinateRegion> {
Binding {
let centre = CLLocationCoordinate2D(latitude: lat, longitude: lon)
if let span = span {
return MKCoordinateRegion(center: centre, span: span)
} else {
return MKCoordinateRegion(center: centre, latitudinalMeters: initialLatitudinalMetres, longitudinalMeters: initialLongitudinalMetres)
}
} set: { region in
lat = region.center.latitude
lon = region.center.longitude
span = region.span
}
}
var body: some View {
Map(coordinateRegion: region)
}
}
In the AirportModel, there is a piece of data decoded in Double form called "latitude" and "longitude." I am simply trying to pass these data into the view after they are decoded. Here is my view body:
var body: some View {
ScrollView{
MapView(lat: <#Binding<Double>#>, lon: <#Binding<Double>#>)
.frame(height: 250)
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
I have tried putting coordModel?.latitude and coordModel?.longitude in for lat and lon, as well as setting variables above the call. I'm not quite sure where to go now.
Note: I am decoding a bunch of JSON data and every user query results in a different lat/lon pull (over 10000 potential responses). Thanks again!
Use a model that is an ObservableObject:
import Combine
import MapKit
class MapModel: ObservableObject {
var region: MKCoordinateRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 48.687330584, longitude: 9.219832454),
latitudinalMeters: 1000000,
longitudinalMeters: 1000000
) {
willSet {
self.objectWillChange.send()
}
}
// MARK: - test code change center evey 10 seconds
init() {
self.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
self.testIndex += 1
self.testIndex %= self.testCoordinates.count
let newCenter = self.testCoordinates[self.testIndex]
let oldSpan = self.region.span
DispatchQueue.main.async {
self.region = MKCoordinateRegion(center: newCenter, span: oldSpan)
}
}
}
let testCoordinates = [
CLLocationCoordinate2D(latitude: 48.687330584, longitude: 9.219832454), // STR
CLLocationCoordinate2D(latitude: 41.297445, longitude: 2.0832941), // BCN
]
var testIndex: Int = 0
var timer: Timer? = nil
}
then create a View that observes your model
import SwiftUI
import MapKit
struct ContentView: View {
#ObservedObject private var mapModel = MapModel()
var body: some View {
Map(coordinateRegion: $mapModel.region)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
For test reasons I used a timer that simulates updated data from a server.
If you need Annotations, add them to the model and don't forget self.objectWillChange.send() if you change them.

Resources