Showing annotation items in a MapView in SwiftUI [duplicate] - swift

I am trying to build a small Map app where location for user changes all the time. In general I get latitude and longitude updates all the time. And I need to display them and show the change with sliding animation, simular to Apple FindMyFriend, when it slides over map when they are moving in live.
This is my view:
struct ContentView: View {
#StateObject var request = Calls()
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 56.946285, longitude: 24.105078), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
var body: some View {
Map(coordinateRegion: $mapRegion, annotationItems: $request.users){ $user in
withAnimation(.linear(duration: 2.0)) {
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: user.latitude, longitude: user.longitude)){
Circle()
}
}
}
}
}
And function call in view model, whitch changes user location, the response is just incoming string from API:
func collectUsers(_ response: String){
if users.count != 0{
var data = response.components(separatedBy: "\n")
data.removeLast()
let updates = self.users.map{ user -> User in
let newData = updateUserLocation(user: user, input: data)
return User(id: user.id, name: user.name, image: user.image, latitude: Double(newData[1])!, longitude: Double(newData[2])!)
}
DispatchQueue.main.async {
self.users = updates
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
return User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude)
}
DispatchQueue.main.async {
self.users = users
}
}
}
And ofcourse my #Published:
class Calls: ObservableObject{
#Published var users = [User]()
When I use the MapMarker instead of MapAnnotation the error does not appier. I would use marker, but I need each user view in map to be different.

If any one stumbles with the same issue. I spent entire day to solve this, but the awnser is that in Xcode 14 it is a bug. After I installer Xcode 13.4.1 error messages disappiered.

Related

SwiftUI async data receive from #escaping closure

I am having trouble with working over data I receive from server.
Each time server sends data it is new coordinates for each user. I am looping over each incoming data, and I want to send the data in completion to receive them on other end. And update model class with them. At the moment I have two users in server. And sometimes the closure passes data two times, but sometimes just one. And interesting thing is that class properties are not updated, at least I dont see them on UI.
This is function I call when data is received. Response is just string I split to get user data.
func updateUserAdrress(response: String, completion: #escaping (Int, Double, Double, String) -> Void){
var data = response.components(separatedBy: "\n")
data.removeLast()
data.forEach { part in
let components = part.components(separatedBy: ",")
let userID = components[0].components(separatedBy: " ")
let id = Int(userID[1])
let latitude = Double(components[1])!
let longitude = Double(components[2])!
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
DispatchQueue.main.async {
completion(id!, latitude, longitude, collectedAddress)
}
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
In this function I try to invoke the changes on objects. As the incoming data from server is different the first time I have splited the functionality, so the correct block of code would be called.
func collectUsers(_ response: String){
if users.count != 0{
updateUserAdrress(response: response) { id, latitude, longitude, address in
if let index = self.users.firstIndex(where: { $0.id == id }){
let user = self.users[index]
user.latitude = latitude
user.longitude = longitude
user.address = address
}
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
let collectedUser = User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude)
return collectedUser
}
DispatchQueue.main.async {
self.users = users
}
}
}
As I also need user address when app starts in model I have made simular function to call in init so it would get address for user. That seems to be working fine. But for more context I will add the model to.
class User: Identifiable {
var id: Int
let name: String
let image: String
var latitude: Double
var longitude: Double
var address: String = ""
init(id: Int, name: String, image: String, latitude: Double, longitude: Double){
self.id = id
self.name = name
self.image = image
self.latitude = latitude
self.longitude = longitude
getAddress(latitude: latitude, longitude: longitude) { address in
self.address = address
}
}
func getAddress(latitude: Double, longitude: Double, completion: #escaping (String) -> Void){
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
completion(collectedAddress)
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
And one interesting thing. That when closure receives two times data and I assign them to class, there are no changes on UI.
I made this project on Xcode 13.4.1 because on Xcode 14 there is a bug on MapAnnotations throwing purple warnings on view changes.

Make MapAnnotation slide to new location with animation

I want to want to slide the annotation over map to new position when coordinates are received. And the change each time coordinates are changed.
This is view I am using:
struct ContentView: View {
#StateObject var request = Requests()
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 56.946285, longitude: 24.105078), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
var body: some View {
Map(coordinateRegion: $mapRegion, annotationItems: request.users){ user in
withAnimation(.linear){
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: user.latitude, longitude: user.longitude)) {
AnnotationView(requests: request, user: user)
}
}
}
}
struct AnnotationView: View{
#ObservedObject var requests: Requests
#State private var showDetails = false
let user: User
var body: some View{
ZStack{
if showDetails{
withAnimation(.easeInOut) {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.white)
.frame(width: 180, height: 50)
.overlay{
VStack{
Text(user.name)
.foregroundColor(.black)
.font(.title3)
Text(user.address)
.foregroundColor(.gray)
}
}
.offset(y: -50)
}
}
Circle()
.frame(width: 35, height: 35)
.foregroundColor(.white)
AsyncImage(url: URL(string: user.image)) { image in
image
.resizable()
.frame(width: 30, height: 30)
.clipShape(Circle())
.scaledToFit()
} placeholder: {
Image(systemName: "person.circle")
}
.onTapGesture {
self.showDetails.toggle()
}
}
}
}
}
And in view model I have a function that work over incoming data. The response is just received data from server.
#Published var users = User
func collectUsers(_ response: String){
if users.count != 0{
var data = response.components(separatedBy: "\n")
data.removeLast()
for newData in data{
var components = newData.components(separatedBy: ",")
var userID = components[0].components(separatedBy: " ")
userID.removeFirst()
components[0] = userID[0]
if let index = self.users.firstIndex(where: { $0.id == Int(components[0]) }){
self.getAddress(latitude: Double(components[1])!, longitude: Double(components[2])!) { address in
DispatchQueue.main.async {
self.users[index].address = address
self.users[index].latitude = Double(components[1])!
self.users[index].longitude = Double(components[2])!
}
}
}
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
var collectedUser = User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude, address: "")
return collectedUser
}
DispatchQueue.main.async {
self.users = users
}
}
}
func getAddress(latitude: Double, longitude: Double, completion: #escaping (String) -> Void){
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + subThoroughfare
completion(collectedAddress)
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
And also I dont understand why calling getAdress does not invoke changes on view each time coordinates are received.

MapAnnotations Not Showing Up

I have a function that takes my coordnaties that are stored inside of my Firebase Storage, and turns them into MKPointAnnotations For Some Reason I keep on getting the error Type '()' cannot conform to 'View'
Here is my code for the function:
import SwiftUI
import MapKit
import CoreLocationUI
import Firebase
import FirebaseFirestore
struct Marker: Identifiable {
let id = UUID()
var coordinate : CLLocationCoordinate2D
}
struct MapView: View {
#StateObject private var viewModel = ContentViewModel()
//For GeoCoder
let geocoder = CLGeocoder()
#State private var result = "result of lat & long"
#State private var lat = 0.0
#State private var long = 0.0
#State private var country = "country name"
#State private var state = "state name"
#State private var zip = "zip code"
//For Map Annotations
#State var address = ""
#State var realLat = 0.00
#State var realLong = 0.00
#State var email = ""
//For TopBar
#State var goToAddress = ""
#State var filters = false
var body: some View {
let markers = [
Marker(coordinate: CLLocationCoordinate2D(latitude: realLat, longitude: realLong))
]
NavigationView {
VStack {
ZStack (alignment: .bottom) {
LocationButton(.currentLocation) {
viewModel.requestAllowOnceLocationPermission()
}
.foregroundColor(.white)
.cornerRadius(8)
.labelStyle(.iconOnly)
.symbolVariant(.fill)
.tint(.pink)
.padding(.bottom)
.padding(.trailing, 300)
getAnnotations { (annotations) in
if let annotations = annotations {
Map(coordinateRegion: $viewModel.region, showsUserLocation: true, annotationItems: MKPointAnnotation) { annotations in
MapAnnotation(coordinate: annotations.coordinate) {
Circle()
}
}
.ignoresSafeArea()
.tint(.pink)
} else {
print("There has been an error with the annotations")
}
}
}
}
}
}
func getAnnotations(completion: #escaping (_ annotations: [MKPointAnnotation]?) -> Void) {
let db = Firestore.firestore()
db.collection("annotations").addSnapshotListener { (querySnapshot, err) in
guard let snapshot = querySnapshot else {
if let err = err {
print(err)
}
completion(nil) // return nil if error
return
}
guard !snapshot.isEmpty else {
completion([]) // return empty if no documents
return
}
var annotations = [MKPointAnnotation]()
for doc in snapshot.documents {
if let lat = doc.get("lat") as? String,
let lon = doc.get("long") as? String,
let latitude = Double(lat),
let longitude = Double(lon) {
let coord = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let annotation = MKPointAnnotation()
annotation.coordinate = coord
annotations.append(annotation)
}
}
completion(annotations) // return array
}
}
func goToTypedAddress() {
geocoder.geocodeAddressString(goToAddress, completionHandler: {(placemarks, error) -> Void in
if((error) != nil){
print("Error", error ?? "")
}
if let placemark = placemarks?.first {
let coordinates:CLLocationCoordinate2D = placemark.location!.coordinate
print("Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)")
//added code
result = "Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)"
lat = coordinates.latitude
long = coordinates.longitude
}
})
print("\(lat)")
print("\(long)")
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
struct Item: Identifiable {
let id = UUID()
let text: String
}
//LocationButton
final class ContentViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40, longitude: 120), span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func requestAllowOnceLocationPermission() {
locationManager.requestLocation()
}
func locationManager( _ _manager:CLLocationManager, didUpdateLocations locations: [CLLocation]){
guard let latestLocation = locations.first else {
// show an error
return
}
DispatchQueue.main.async{
self.region = MKCoordinateRegion(
center: latestLocation.coordinate,
span:MKCoordinateSpan(latitudeDelta:0.05, longitudeDelta:0.05))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
The updated error is on line 50 now. Might be because of all of the new functions that I have. Also is the firebase function correct? I would like to make sure that it correct too.

ReverseGeocodeLocation always return error

When I use this method, for update annotation title,i always have error when I move map to another city or country:
Error is:
The operation couldn’t be completed. (kCLErrorDomain error 2.)
Yes, I have seen similar posts on this topic. However, there are no solutions to the problem, and yes, I checked the Internet connection, and tried to test on the device.
Method is:
and second problem, my "addressString" always empty, also when I have placemark, and "city"/"state" values printed, but addressString always empty.
But latitude and longitude always print, when I move map.
func convertLatLongToAddress(latitude: Double,longitude: Double) -> String {
var addressString: String = ""
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: latitude, longitude: longitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
if let error = error {
debugPrint("ERROR IS: \(error.localizedDescription)")
}
// Place details
var placeMark: CLPlacemark?
placeMark = placemarks?[0]
// City
if let city = placeMark?.locality {
print(city)
addressString = city
}
// State
if let state = placeMark?.administrativeArea {
print(state)
addressString += ", \(state)"
}
})
debugPrint("LATITUDE: \(latitude)")
debugPrint("LONGITUDE: \(longitude)")
return addressString
}
Where I use this method:
Map(coordinateRegion: $manager.region,
annotationItems: manager.annotationItems
) { place in
MapAnnotation(coordinate: manager.region.center) {
PinView(text: "\(manager.convertLatLongToAddress(latitude: manager.region.center.latitude, longitude: manager.region.center.longitude))")
.offset(x: 0, y: -15)
}
}
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .top)
.cornerRadius(8)

Retrieving coordinates from address retuning zero

I'm trying to play with SwiftUI and make a map with dropped pins from locations (generated from a database API).
I have my struct:
struct Locations: Decodable, Identifiable {
var id: Int { _id }
let _id: Int // the one used in the database
let streetaddress: String?
let suburb: String?
let state: String?
let postcode: String?
// get the co-ordinates now
var coordinates: CLLocationCoordinate2D? {
let geocoder = CLGeocoder()
var output = CLLocationCoordinate2D()
if let address = streetaddress,
let suburb = suburb,
let postcode = postcode,
let state = state {
let fullAddress = "\(address) \(suburb), \(state) \(postcode)"
geocoder.geocodeAddressString( String(fullAddress) ) { ( placemark, error ) in
if let latitude = placemark?.first?.location?.coordinate.latitude,
let longitude = placemark?.first?.location?.coordinate.longitude {
output = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
}
return output
}
}
However, whenever I call the coordinates I am getting a {"msg":"#NullIsland Received a latitude or longitude from getLocationForBundleID that was exactly zero", "latIsZero":0, "lonIsZero":0} error.
I have added the error snippet from here: https://stackoverflow.com/a/65837163/1086990 to dive deeper into the error, and it is returning network: network was unavailable or a network error occurred.
I am able to call Map() in the view, and permissions are set in the info.plist as well as see my current location, etc.
Is there something I'm missing or is it not calculating because the Strings are all optional? Been trying to understand how it's not generating the coordinates from the address.
If I try to debug in the view I tried this:
struct MapView: View {
var body: some View {
Text("Hello World")
.onAppear {
for l in modelData.locations {
print( String("\(l.coordinates)") )
}
}
}
}
// console
// Optional(__C.CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
// ...
// Optional(__C.CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
The issue there is that geocodeAddressString is an asynchronous method. You are returning the value before receiving the result. What you need is a method instead of a computed property and a completion handler.
func coordinate(completion: #escaping (CLLocationCoordinate2D?, Error?) -> Void) {
let streetAddress = streetAddress ?? ""
let suburb = suburb ?? ""
let postCode = postCode ?? ""
let state = state ?? ""
let fullAddress = "\(streetAddress) \(suburb), \(state) \(postCode)"
CLGeocoder().geocodeAddressString(fullAddress) { completion($0?.first?.location?.coordinate, $1) }
}
Usage:
#State var location = Location(id: 1, streetAddress: "One Infinite Loop", suburb: "Cupertino", state: "CA", postCode: "95014")
var body: some View {
Text("Hello, world!")
.padding()
.onAppear {
location.coordinate { coordinate, error in
guard let coordinate = coordinate else {
print("error:", error ?? "nil")
return
}
print("coordinate", coordinate)
}
}
}
This will print
coordinate CLLocationCoordinate2D(latitude: 37.331656, longitude: -122.0301426)
as mentioned geocodeAddressString is an asynchronous function. That means you have to use some completion handler. So you cannot use it like you do with coordinates.
Here is some very basic code using a function instead:
struct Locations: Decodable, Identifiable {
var id: String = UUID().uuidString
// let _id: Int // the one used in the database
let streetaddress: String? = "1 Infinite Loop"
let suburb: String? = "Cupertino"
let state: String? = "CA"
let postcode: String? = ""
func getCoordinates(handler: #escaping ((CLLocationCoordinate2D) -> Void)) {
if let address = streetaddress, let suburb = suburb, let postcode = postcode, let state = state {
let fullAddress = "\(address) \(suburb), \(state) \(postcode)"
CLGeocoder().geocodeAddressString(fullAddress) { ( placemark, error ) in
handler(placemark?.first?.location?.coordinate ?? CLLocationCoordinate2D())
}
}
}
}
struct ContentView: View {
let locs = Locations()
#State var coordString = ""
var body: some View {
Text(locs.streetaddress ?? "no address")
Text(coordString)
.onAppear {
locs.getCoordinates() { coords in
coordString = "\(coords.latitude), \(coords.longitude)"
}
}
}
}