MapAnnotations Not Showing Up - swift

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.

Related

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.

Error with Geocoding - Error 1 and Error 8

I am pulling all of the users locations inside of Firebase storage, and when I try to geocode them into lat and long coordinates, I am getting the error 2022-07-26 15:40:39.676845-0400 Polygon[96779:3864577] [MKCoreLocationProvider] CLLocationManager(<CLLocationManager: 0x6000004c2c90>) for <MKCoreLocationProvider: 0x6000034f0870> did fail with error: Error Domain=kCLErrorDomain Code=1 "(null) and Error Error Domain=kCLErrorDomain Code=8 "(null)"
My code seems to be working fine, and when I take one of the addresses and geocode them in a different project, they work well. Please take a look at the code below and tell me of any errors or if I missed something?
import SwiftUI
import MapKit
import CoreLocationUI
import Firebase
struct Place: Identifiable {
let id = UUID()
var name: String
var coordinate: CLLocationCoordinate2D
}
struct MapView: View {
var empireStateBuilding =
Place(name: "Empire State Building", coordinate: CLLocationCoordinate2D(latitude: 40.748433, longitude: -73.985656))
#StateObject private var viewModel = ContentViewModel()
#State var address = ""
#State var addresses:[String] = []
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"
var body: some View {
NavigationView {
ZStack (alignment: .bottom) {
Map(coordinateRegion: $viewModel.region, showsUserLocation: true)
.ignoresSafeArea()
.tint(.pink)
LocationButton(.currentLocation) {
viewModel.requestAllowOnceLocationPermission()
}
.foregroundColor(.white)
.cornerRadius(8)
.labelStyle(.iconOnly)
.symbolVariant(.fill)
.tint(.pink)
.padding(.bottom)
.padding(.trailing, 300)
Button {
print(address)
geocoder.geocodeAddressString(address, completionHandler: {(placemarks, error) -> Void in
if((error) != nil){
print("Error", error ?? "")
}
if let placemark = placemarks?.first {
if 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
}
}
})
} label: {
Image(systemName: "arrow.clockwise")
}
.padding(.leading, 300)
.padding(.bottom)
.cornerRadius(8)
Text("\(result)")
}
List(0..<addresses.count, id: \.self) {i in Text(addresses[i]) }
}
.onAppear(perform: {
downloadServerData()
})
}
func downloadServerData() {
let db = Firestore.firestore()
db.collection("UserInfo").addSnapshotListener {(snap, err) in
if err != nil{
print("There is an error")
return
}
for i in snap.documentChanges {
let documentId = i.document.documentID
if let address = i.document.get("address") as? String {
DispatchQueue.main.async {
addresses.append("\(address)")
print(address)
}
}
}
}
}
}
struct AnnotatedItem: Identifiable {
let id = UUID()
var name: String
var coordinate: CLLocationCoordinate2D
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
//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)
}
}
struct MapView1_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}

Passing data from UIViewRepresentable function to SwiftUI View

The user looks for the delivery address on the map, then the address is identified by the marker located in the middle of the screen. And then the address is obtained through this marker. How to display an address in the user interface ?
struct MapView: UIViewRepresentable {
#Binding var centerCoordinate: CLLocationCoordinate2D
var currentLocation: CLLocationCoordinate2D?
var withAnnotation: MKPointAnnotation?
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
var addressLabel: String = "222"
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if !mapView.showsUserLocation {
parent.centerCoordinate = mapView.centerCoordinate
}
}
...
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
let center = getCenterLocation(for: mapView)
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(center) { [weak self] (placemarks, error) in
guard let self = self else { return }
if let _ = error {
//TODO: Show alert informing the user
print("error")
return
}
guard let placemark = placemarks?.first else {
//TODO: Show alert informing the user
return
}
let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""
DispatchQueue.main.async {
self.addressLabel = String("\(streetName) | \(streetNumber)")
print(self.addressLabel)
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = false
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if let currentLocation = self.currentLocation {
if let annotation = self.withAnnotation {
uiView.removeAnnotation(annotation)
}
uiView.showsUserLocation = true
let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
uiView.setRegion(region, animated: true)
} else if let annotation = self.withAnnotation {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotation(annotation)
}
}
}
I am trying to pass the address to the UI.
What's the most correct way to do this?
In the interface, I want to get the address from an ever-changing variable addressLabel
import SwiftUI
import MapKit
fileprivate let locationFetcher = LocationFetcher()
struct LocationView: View {
#State var centerCoordinate = CLLocationCoordinate2D()
#State var currentLocation: CLLocationCoordinate2D?
#State var annotation: MKPointAnnotation?
var body: some View {
ZStack {
MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation)
.edgesIgnoringSafeArea([.leading, .trailing, .bottom])
.onAppear(perform: {
locationFetcher.start()
})
}
.overlay(
ZStack {
Text("\(*MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation).makeCoordinator().addressLabel OMG????*)")
.offset(y: 44)
}
)
}
struct LocationView_Previews: PreviewProvider {
static var previews: some View {
LocationView()
}
}
How can i do this ?
Thanks in advance
Here is one approach. Have a single source of truth that both UIKit and SwiftUI can access.
#available(iOS 15.0, *)
struct LocationView: View {
//It is better to have one source of truth
#StateObject var vm: MapViewModel = MapViewModel()
var body: some View {
ZStack {
MapView(vm: vm)
.edgesIgnoringSafeArea([.leading, .trailing, .bottom])
.onAppear(perform: {
//locationFetcher.start() //No Code provided
})
}
.overlay(
HStack{
Spacer()
Text(vm.addressLabel)
Spacer()
//Using offset is subjective since screen sizes change just center it
}
)
//Sample alert that adapts to what is
.alert(isPresented: $vm.errorAlert.isPresented, error: vm.errorAlert.error, actions: {
if vm.errorAlert.defaultAction != nil{
Button("ok", role: .none, action: vm.errorAlert.defaultAction!)
}
if vm.errorAlert.cancelAction != nil{
Button("cancel", role: .cancel, action: vm.errorAlert.cancelAction!)
}
if vm.errorAlert.defaultAction == nil && vm.errorAlert.cancelAction == nil {
Button("ok", role: .none, action: {})
}
})
}
}
//UIKit and SwiftUI will have access to this ViewModel so all the data can have one souce of truth
class MapViewModel: ObservableObject{
//All the variables live here
#Published var addressLabel: String = "222"
#Published var centerCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D()
#Published var currentLocation: CLLocationCoordinate2D? = nil
#Published var withAnnotation: MKPointAnnotation? = nil
#Published var annotation: MKPointAnnotation?
//This tuple variable allows you to have a dynamic alert in the view
#Published var errorAlert: (isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?) = (false, MapErrors.unknown, nil, nil)
//The new alert requires a LocalizedError
enum MapErrors: LocalizedStringKey, LocalizedError{
case unknown
case failedToRetrievePlacemark
case failedToReverseGeocode
case randomForTestPurposes
//Add localizable.strings to you project and add these keys so you get localized messages
var errorDescription: String?{
switch self{
case .unknown:
return "unknown".localizedCapitalized
case .failedToRetrievePlacemark:
return "failedToRetrievePlacemark".localizedCapitalized
case .failedToReverseGeocode:
return "failedToReverseGeocode".localizedCapitalized
case .randomForTestPurposes:
return "randomForTestPurposes".localizedCapitalized
}
}
}
//Presenting with this will ensure that errors keep from gettting lost by creating a loop until they can be presented
func presentError(isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?, count: Int = 1){
//If there is an alert already showing
if errorAlert.isPresented{
//See if the current error has been on screen for 10 seconds
if count >= 10{
//If it has dismiss it so the new error can be posted
errorAlert.isPresented = false
}
//Call the method again in 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let newCount = count + 1
self.presentError(isPresented: isPresented, error: error, defaultAction: defaultAction, cancelAction: cancelAction, count: newCount)
}
}else{
errorAlert = (isPresented, error, defaultAction, cancelAction)
}
}
}
struct MapView: UIViewRepresentable {
#ObservedObject var vm: MapViewModel
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if !mapView.showsUserLocation {
parent.vm.centerCoordinate = mapView.centerCoordinate
}
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
getAddress(center: mapView.centerCoordinate)
//Just to demostrate the error
//You can remove this whenever
#if DEBUG
if Bool.random(){
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.randomForTestPurposes, defaultAction: nil, cancelAction: nil)
}
#endif
}
//Gets the addess from CLGeocoder if available
func getAddress(center: CLLocationCoordinate2D){
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(CLLocation(latitude: center.latitude, longitude: center.longitude)) { [weak self] (placemarks, error) in
guard let self = self else { return }
if let _ = error {
//TODO: Show alert informing the user
print("error")
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToReverseGeocode, defaultAction: nil, cancelAction: nil)
return
}
guard let placemark = placemarks?.first else {
//TODO: Show alert informing the user
self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToRetrievePlacemark, defaultAction: nil, cancelAction: nil)
return
}
let streetNumber = placemark.subThoroughfare ?? ""
let streetName = placemark.thoroughfare ?? ""
DispatchQueue.main.async {
self.parent.vm.addressLabel = String("\(streetName) | \(streetNumber)")
print(self.parent.vm.addressLabel)
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = false
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if let currentLocation = vm.currentLocation {
if let annotation = vm.withAnnotation {
uiView.removeAnnotation(annotation)
}
uiView.showsUserLocation = true
let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
uiView.setRegion(region, animated: true)
} else if let annotation = vm.withAnnotation {
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotation(annotation)
}
}
}

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)"
}
}
}
}

SwiftUI: View does not update after image changed asynchronous

As mentioned in the headline, I try to load images to a custom object
I’ve got the custom object “User” that contains the property “imageLink” that stores the location within the Firebase Storage.
First I load the users frome the Firestore db and then I try to load the images for these users asynchronous from the Firebase Storage and show them on the View. As long as the image has not been loaded, a placeholder shall be shown.
I tried several implementations and I always can see in the debugger that I am able to download the images (I saw the actual image and I saw the size of some 100kb), but the loaded images don’t show on the view, I still see the placeholder, it seems that the view does not update after they loaded completely.
From my perspective, the most promising solution was:
FirebaseImage
import Combine
import FirebaseStorage
import UIKit
let placeholder = UIImage(systemName: "person")!
struct FirebaseImage : View {
init(id: String) {
self.imageLoader = Loader(id)
}
#ObservedObject private var imageLoader : Loader
var image: UIImage? {
imageLoader.data.flatMap(UIImage.init)
}
var body: some View {
Image(uiImage: image ?? placeholder)
}
}
Loader
import SwiftUI
import Combine
import FirebaseStorage
final class Loader : ObservableObject {
let didChange = PassthroughSubject<Data?, Never>()
var data: Data? = nil {
didSet { didChange.send(data) }
}
init(_ id: String){
// the path to the image
let url = "profilepics/\(id)"
print("load image with id: \(id)")
let storage = Storage.storage()
let ref = storage.reference().child(url)
ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
self.data = data
}
}
}
}
User
import Foundation
import Firebase
import CoreLocation
import SwiftUI
struct User: Codable, Identifiable, Hashable {
var id: String?
var name: String
var imageLink: String
var imagedata: Data = .init(count: 0)
init(name: String, imageLink: String, lang: Double) {
self.id = id
self.name = name
self.imageLink = imageLink
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let name = data["name"] as? String else {
return nil
}
guard let imageLink = data["imageLink"] as? String else {
return nil
}
id = document.documentID
self.name = name
self.imageLink = imageLink
}
}
extension User {
var image: Image {
Image(uiImage: UIImage())
}
}
extension User: DatabaseRepresentation {
var representation: [String : Any] {
var rep = ["name": name, "imageLink": imageLink] as [String : Any]
if let id = id {
rep["id"] = id
}
return rep
}
}
extension User: Comparable {
static func == (lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: User, rhs: User) -> Bool {
return lhs.name < rhs.name
}
}
UserViewModel
import Foundation
import FirebaseFirestore
import Firebase
class UsersViewModel: ObservableObject {
let db = Firestore.firestore()
let storage = Storage.storage()
#Published var users = [User]()
#Published var showNewUserName: Bool = UserDefaults.standard.bool(forKey: "showNewUserName"){
didSet {
UserDefaults.standard.set(self.showNewUserName, forKey: "showNewUserName")
NotificationCenter.default.post(name: NSNotification.Name("showNewUserNameChange"), object: nil)
}
}
#Published var showLogin: Bool = UserDefaults.standard.bool(forKey: "showLogin"){
didSet {
UserDefaults.standard.set(self.showLogin, forKey: "showLogin")
NotificationCenter.default.post(name: NSNotification.Name("showLoginChange"), object: nil)
}
}
#Published var isLoggedIn: Bool = UserDefaults.standard.bool(forKey: "isLoggedIn"){
didSet {
UserDefaults.standard.set(self.isLoggedIn, forKey: "isLoggedIn")
NotificationCenter.default.post(name: NSNotification.Name("isLoggedInChange"), object: nil)
}
}
func addNewUserFromData(_ name: String, _ imageLing: String, _ id: String) {
do {
let uid = Auth.auth().currentUser?.uid
let newUser = User(name: name, imageLink: imageLing, lang: 0, long: 0, id: uid)
try db.collection("users").document(newUser.id!).setData(newUser.representation) { _ in
self.showNewUserName = false
self.showLogin = false
self.isLoggedIn = true
}
} catch let error {
print("Error writing city to Firestore: \(error)")
}
}
func fetchData() {
db.collection("users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.map { queryDocumentSnapshot -> User in
let data = queryDocumentSnapshot.data()
let id = data["id"] as? String ?? ""
let name = data["name"] as? String ?? ""
let imageLink = data["imageLink"] as? String ?? ""
let location = data["location"] as? GeoPoint
let lang = location?.latitude ?? 0
let long = location?.longitude ?? 0
Return User(name: name, imageLink: imageLink, lang: lang, long: long, id: id)
}
}
}
}
UsersCollectionView
import SwiftUI
struct UsersCollectionView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var usersViewModel: UsersViewModel
let itemWidth: CGFloat = (screenWidth-30)/4.2
let itemHeight: CGFloat = (screenWidth-30)/4.2
var fixedLayout: [GridItem] {
[
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2))
]
}
func debugUserValues() {
for user in usersViewModel.users {
print("ID: \(user.id), Name: \(user.name), ImageLink: \(user.imageLink)")
}
}
var body: some View {
VStack() {
ScrollView(showsIndicators: false) {
LazyVGrid(columns: fixedLayout, spacing: 15) {
ForEach(usersViewModel.users, id: \.self) { user in
VStack() {
FirebaseImage(id: user.imageLink)
HStack(alignment: .center) {
Text(user.name)
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.black)
.lineLimit(1)
}
}
}
}
.padding(.top, 20)
Rectangle()
.fill(Color .clear)
.frame(height: 100)
}
}
.navigationTitle("Find Others")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
.foregroundColor(.black)
.padding()
.offset(x: -15)
}
})
}
}
You're using an old syntax from BindableObject by using didChange -- that system changed before SwiftUI 1.0 was out of beta.
A much easier approach would be to use #Published, which your view will listen to automatically:
final class Loader : ObservableObject {
#Published var data : Data?
init(_ id: String){
// the path to the image
let url = "profilepics/\(id)"
print("load image with id: \(id)")
let storage = Storage.storage()
let ref = storage.reference().child(url)
ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
self.data = data
}
}
}
}