This problem has been haunting me for months and I believe it comes down to my using the wrong structure and procedure.
I'm trying to do an API call to Yelp's API and passing in the variables for the user's lat/long. I'm able to grab the lat/long based on my current LocationManager, however when it seems as though the lat/long only becomes available AFTER the API call has been made, so the API is getting default 0.0 values for both lat/long.
I'm very much a beginner when it comes to this, but is there a way that I could set up a loading screen that grabs the lat/long in the background and by the time my ExploreView shows, the real location information has been established?
Below is my LocationManager and ExploreView
LocationManager
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
let geoCoder = CLGeocoder()
#Published var location: CLLocation? = nil
#Published var placemark: CLPlacemark? = nil
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.distanceFilter = kCLDistanceFilterNone
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
func geoCode(with location: CLLocation) {
geoCoder.reverseGeocodeLocation(location) { (placemark, error) in
if error != nil {
print(error!.localizedDescription)
} else {
self.placemark = placemark?.first
}
}
}
func startUpdating() {
self.locationManager.delegate = self
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else {
return
}
self.location = location
self.geoCode(with: location)
}
}
ExploreView (The first view that shows upon launch)
import SwiftUI
import CoreLocation
import Foundation
struct ExploreView: View {
#ObservedObject var location = LocationManager()
#ObservedObject var fetcher: RestaurantFetcher
init() {
let location = LocationManager()
self.location = location
self.fetcher = RestaurantFetcher(locationManager: location)
self.location.startUpdating()
}
var body: some View {
ScrollView (.vertical) {
VStack {
HStack {
Text("Discover ")
.font(.system(size: 28))
.fontWeight(.bold)
+ Text(" \(location.placemark?.locality ?? "")")
.font(.system(size: 28))
.fontWeight(.bold)
Spacer()
}
HStack {
SearchBar(text: .constant(""))
}.padding(.top, 16)
HStack {
Text("Featured Restaurants")
.font(.system(size: 24))
.fontWeight(.bold)
Spacer()
NavigationLink(
destination: FeaturedView(),
label: {
Text("View All")
})
}.padding(.vertical, 30)
HStack {
Text("All Cuisines")
.font(.system(size: 24))
.fontWeight(.bold)
Spacer()
}
Spacer()
}.padding()
}
}
}
public class RestaurantFetcher: ObservableObject {
#Published var businesses = [RestaurantResponse]()
#ObservedObject var locationManager: LocationManager
let location = LocationManager()
var lat: String {
return "\(location.location?.coordinate.latitude ?? 0.0)"
}
var long: String {
return "\(location.location?.coordinate.longitude ?? 0.0)"
}
init(locationManager: LocationManager) {
let location = LocationManager()
self.locationManager = location
self.location.startUpdating()
load()
}
func load() {
print("\(location.location?.coordinate.latitude ?? 0.0)")
print("user latitude top of function")
//Returns default values of 0.0
let apikey = "APIKEY Here"
let url = URL(string: "https://api.yelp.com/v3/businesses/search?latitude=\(lat)&longitude=\(long)&radius=40000")!
var request = URLRequest(url: url)
request.setValue("Bearer \(apikey)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let d = data {
print("\(self.location.location?.coordinate.longitude ?? 0.0)")
let decodedLists = try JSONDecoder().decode(BusinessesResponse.self, from: d)
// Returns actual location coordinates
DispatchQueue.main.async {
self.businesses = decodedLists.restaurants
}
} else {
print("No Data")
}
} catch {
print ("Caught")
}
}.resume()
}
}
Try the following modified code (I needed to make some replications, so pay attention - some typos possible).
The main idea is to subscribe for LocationManager updated location publisher to listen for explicit changes of location and perform next API load only after location is really updated and not nil.
struct ExploreView: View {
#ObservedObject var location: LocationManager
#ObservedObject var fetcher: RestaurantFetcher
init() {
let location = LocationManager() // << use only one instance
self.location = location
self.fetcher = RestaurantFetcher(locationManager: location)
self.location.startUpdating() // << do this only once
}
var body: some View {
ScrollView (.vertical) {
VStack {
HStack {
Text("Discover ")
.font(.system(size: 28))
.fontWeight(.bold)
+ Text(" \(location.placemark?.locality ?? "")")
.font(.system(size: 28))
.fontWeight(.bold)
Spacer()
}
HStack {
SearchBar(text: .constant(""))
}.padding(.top, 16)
HStack {
Text("Featured Restaurants")
.font(.system(size: 24))
.fontWeight(.bold)
Spacer()
NavigationLink(
destination: FeaturedView(),
label: {
Text("View All")
})
}.padding(.vertical, 30)
HStack {
Text("All Cuisines")
.font(.system(size: 24))
.fontWeight(.bold)
Spacer()
}
Spacer()
}.padding()
}
}
}
import Combine
public class RestaurantFetcher: ObservableObject {
#Published var businesses = [RestaurantResponse]()
private var locationManager: LocationManager
var lat: String {
return "\(locationManager.location?.coordinate.latitude ?? 0.0)"
}
var long: String {
return "\(locationManager.location?.coordinate.longitude ?? 0.0)"
}
private var subscriber: AnyCancellable?
init(locationManager: LocationManager) {
self.locationManager = locationManager
// listen for available location explicitly
subscriber = locationManager.$location
.debounce(for: 5, scheduler: DispatchQueue.main) // wait for 5 sec to avoid often reload
.receive(on: DispatchQueue.main)
.sink { [weak self] location in
guard location != nil else { return }
self?.load()
}
}
func load() {
print("\(locationManager.location?.coordinate.latitude ?? 0.0)")
print("user latitude top of function")
//Returns default values of 0.0
let apikey = "APIKEY Here"
let url = URL(string: "https://api.yelp.com/v3/businesses/search?latitude=\(lat)&longitude=\(long)&radius=40000")!
var request = URLRequest(url: url)
request.setValue("Bearer \(apikey)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let d = data {
print("\(self.locationManager.location?.coordinate.longitude ?? 0.0)")
let decodedLists = try JSONDecoder().decode(BusinessesResponse.self, from: d)
// Returns actual location coordinates
DispatchQueue.main.async {
self.businesses = decodedLists.restaurants
}
} else {
print("No Data")
}
} catch {
print ("Caught")
}
}.resume()
}
}
Related
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.
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()
}
}
I have register screen and I am not able to register user info to firebase,
I put user info on simulator, but it just hold on on screen, and no any change on firebase, I guess maybe there is missed points on
CreateUser
function, any idea?
import SwiftUI
import Firebase
struct Register: View {
#State var name = ""
#State var about = ""
#Binding var show : Bool
var body: some View {
VStack(alignment: .center, spacing: 3){
TextField("Name",text:self.$name)
.padding()
TextField(" about", text: self.$about)
.padding()
if self.loading{
HStack{
Spacer()
Indicator()
Spacer()
}
}
else{
Button {
if self.name != "" && self.about != "" {
self.loading.toggle()
CreateUser(name: self.name, about: self.about) { (status) in
if status{
self.show.toggle()
}
}
}
else{
self.alert.toggle()
}
} label: {
Text("Next")
.padding()
}
}
}
user:
import Foundation
import Firebase
func CreateUser(name: String,about : String, completion : #escaping (Bool)-> Void){
let db = Firestore.firestore()
let storage = Storage.storage().reference()
let uid = Auth.auth().currentUser?.uid
db.collection("users").document(uid!).setData(["name":name,"about":about, "uid":uid!]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
completion(true)
UserDefaults.standard.set(true, forKey: "status")
UserDefaults.standard.set(name, forKey: "UserName")
NotificationCenter.default.post(name: NSNotification.Name("statusChange"), object: nil)
}
}
I have an observable object class that downloads an image from a url to display:
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(urlString:String){
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
and I show it using:
struct ShowImage1: View {
#ObservedObject var imageLoader:ImageLoader
#State var image:UIImage = UIImage()
init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
}
The problem I'm having is this is only capable of running once, If i click off the ShowImage1 view and then click back on to it, ImageLoader doesn't run again, and I'm left with a blank page.
How can I ensure that ImageLoader Runs every time the ShowImage1 view is accessed?
EDIT:
I access ShowImage1 like this:
struct PostCallForm: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
if true {
self.showImage1 = true
}
}){
Text("View Camera 1 Snapshot")
}.overlay(NavigationLink(destination: ShowImage1(withURL: "example.com/1.jpg"), isActive: self.$showImage1, label: {
EmptyView()
}))
}
}
Section {
Button(action: {
}){
Text("Submit")
}
}
}.disabled(!submission.isValid)
}
}
}
import SwiftUI
import Combine
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
func loadImage(urlString:String) {
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
}
struct ShowImage1Parent: View {
#State var url: String = ""
var sampleURLs: [String] = ["https://image.shutterstock.com/image-vector/click-here-stamp-square-grunge-600w-1510095275.jpg", "https://image.shutterstock.com/image-vector/certified-rubber-stamp-red-grunge-600w-1423389728.jpg", "https://image.shutterstock.com/image-vector/sample-stamp-square-grunge-sign-600w-1474408826.jpg" ]
var body: some View {
VStack{
Button("load-image", action: {
url = sampleURLs.randomElement()!
})
ShowImage1(url: $url)
}
}
}
struct ShowImage1: View {
#StateObject var imageLoader:ImageLoader = ImageLoader()
#State var image:UIImage = UIImage()
#Binding var url: String
var body: some View {
VStack{
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
.onChange(of: url, perform: { value in
imageLoader.loadImage(urlString: value)
})
}
}
}
I have the following code to load an image from a download url and display it as an UIImage.
I expected it to work, but somehow, solely the placeholder image 'ccc' is being displayed, and not the actual image from the download url. How so?
My urls are being fetched from a database and kind of look like this:
https://firebasestorage.googleapis.com/v0/b/.../o/P...alt=media&token=...-579f...da
struct ShelterView: View {
var title: String
var background: String
var available: Bool
var distance: Double
var gender: String
#ObservedObject private var imageLoader: Loader
init(title: String, background: String, available: Bool, distance: Double, gender: String) {
self.title = title
self.background = background
self.available = available
self.distance = distance
self.gender = gender
self.imageLoader = Loader(background)
}
var image: UIImage? {
imageLoader.data.flatMap(UIImage.init)
}
var body: some View {
VStack {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 0) {
Text(title)
.font(Font.custom("Helvetica Now Display Bold", size: 30))
.foregroundColor(.white)
.padding(15)
.lineLimit(2)
HStack(spacing: 25) {
IconInfo(image: "bed.double.fill", text: String(available), color: .white)
if gender != "" {
IconInfo(image: "person.fill", text: gender, color: .white)
}
}
.padding(.leading, 15)
}
Spacer()
IconInfo(image: "mappin.circle.fill", text: String(distance) + " miles away", color: .white)
.padding(15)
}
Spacer()
}
.background(
Image(uiImage: image ?? UIImage(named: "ccc")!) <-- HERE
.brightness(-0.11)
.frame(width: 255, height: 360)
)
.frame(width: 255, height: 360)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
}
final class Loader: ObservableObject {
var task: URLSessionDataTask!
#Published var data: Data? = nil
init(_ urlString: String) {
print(urlString)
let url = URL(string: urlString)
task = URLSession.shared.dataTask(with: url!, completionHandler: { data, _, _ in
DispatchQueue.main.async {
self.data = data
}
})
task.resume()
}
deinit {
task.cancel()
}
}
Your image is a plain old var which happens to be nil when the View is built. SwiftUI only rebuilds itself in response to changes in #ObservedObject, #State, or #Binding, so move your image to an #Published property on your imageLoader and it will work. Here is my caching image View:
import SwiftUI
import Combine
import UIKit
class ImageCache {
enum Error: Swift.Error {
case dataConversionFailed
case sessionError(Swift.Error)
}
static let shared = ImageCache()
private let cache = NSCache<NSURL, UIImage>()
private init() { }
static func image(for url: URL?) -> AnyPublisher<UIImage?, ImageCache.Error> {
guard let url = url else {
return Empty().eraseToAnyPublisher()
}
guard let image = shared.cache.object(forKey: url as NSURL) else {
return URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { (tuple) -> UIImage in
let (data, _) = tuple
guard let image = UIImage(data: data) else {
throw Error.dataConversionFailed
}
shared.cache.setObject(image, forKey: url as NSURL)
return image
}
.mapError({ error in Error.sessionError(error) })
.eraseToAnyPublisher()
}
return Just(image)
.mapError({ _ in fatalError() })
.eraseToAnyPublisher()
}
}
class ImageModel: ObservableObject {
#Published var image: UIImage? = nil
var cacheSubscription: AnyCancellable?
init(url: URL?) {
cacheSubscription = ImageCache
.image(for: url)
.replaceError(with: nil)
.receive(on: RunLoop.main, options: .none)
.assign(to: \.image, on: self)
}
}
struct RemoteImage : View {
#ObservedObject var imageModel: ImageModel
private let contentMode: ContentMode
init(url: URL?, contentMode: ContentMode = .fit) {
imageModel = ImageModel(url: url)
self.contentMode = contentMode
}
var body: some View {
imageModel
.image
.map { Image(uiImage:$0).resizable().aspectRatio(contentMode: contentMode) }
?? Image(systemName: "questionmark").resizable().aspectRatio(contentMode: contentMode)
}
}