How to get latitude and longitude from Model into my url link - ios

I am trying to get city weather in current location. I have model, where is getting weather data from JSON, and also I have model, where I am getting my location (latitude and longitude). But I don't know, how I can get this latitude and longitude in my link.
WeatherModel: where I use url link. I want use latitude and longitude from my LocationModel below
import Foundation
import CoreLocation
protocol IWeatherService {
func getCitiesWeather(forCoordinates coordinates: CLLocationCoordinate2D, completion: #escaping (Result<CitiesWeather, Error>) -> Void)
}
enum WeatherServiceError: Error {
case badUrl
}
final class WeatherService: IWeatherService {
func weatherURLString(forCoordinates coordinates: CLLocationCoordinate2D) -> String {
return "https://api.openweathermap.org/data/2.5/weather?lat=\(coordinates.latitude)&lon=\(coordinates.longitude)&units=metric&appid=b382e4a70dfb690b16b9381daac545ac&lang=ru"
}
func getCitiesWeather(forCoordinates coordinates: CLLocationCoordinate2D, completion: #escaping (Result<CitiesWeather, Error>) -> Void) {
//weatherURLString(forCoordinates: CLLocationCoordinate2D) ???
//LocationManager.shared.locationManager(CLLocationManager, didUpdateLocations: [CLLocation]) ???
//Проверка, что у нас есть url адрес
guard let url = URL(string: .url) else {
return completion(.failure(WeatherServiceError.badUrl))
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
do {
let result = try JSONDecoder().decode(CitiesWeather.self, from: data)
completion(.success(result))
}
catch {
print("failed to convert \(error)")
}
}
task.resume()
}
}
LocationModel:
import Foundation
import CoreLocation
protocol ILocationService {
func getUserLocation(completion: #escaping ((CLLocation) -> Void))
}
class LocationManager: NSObject, ILocationService, CLLocationManagerDelegate {
static let shared = LocationManager()
let manager = CLLocationManager()
public func getUserLocation(completion: #escaping ((CLLocation) -> Void)) {
self.completion = completion
manager.requestWhenInUseAuthorization()
manager.delegate = self
manager.startUpdatingLocation()
}
public func resolveLocationName(with location: CLLocation, completion: #escaping ((String?) -> Void)) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location, preferredLocale: .current) { placemarks, error in
guard let place = placemarks?.first, error == nil else {
completion(nil)
return
}
print(location)
var name = ""
if let locality = place.locality {
name += locality
}
if let adminRegion = place.administrativeArea {
name += ", \(adminRegion)"
}
completion(name)
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
completion?(location)
//Эти значения надо передать в WeatherService
print(location.coordinate.latitude)
print(location.coordinate.longitude)
manager.stopUpdatingLocation()
}
var completion: ((CLLocation) -> Void)?
}

As vadian said in his comment, this bit can't work:
private extension String {
static let url = "https://api.openweathermap.org/data/2.5/weather?lat=\(coordinates.latitude)&lon=\(coordinates.longitude)&units=metric&appid=b382e4a70dfb690b16b9381daac545ac&lang=ru"
}
You should create a function that takes coordinates and returns a URL string for a weather update:
func weatherURLString(forCoordinates coordinates: CLLocationCoordinate2D) {
return "https://api.openweathermap.org/data/2.5/weather?lat=\(coordinates.latitude)&lon=\(coordinates.longitude)&units=metric&appid=b382e4a70dfb690b16b9381daac545ac&lang=ru"
Rewrite your getCitiesWeather() function to take a parameter coordinates of type CLLocationCoordinate2D, and have it call the above function to generate the URL string it will use.
You have code that asks to update the user's location, and implements the locationManager(_:didUpdateLocations:) function. In your locationManager(_:didUpdateLocations:) function, you call a completion handler. Have that completion handler call the weather service with the user's updated location.

Related

Directing the user to the device settings if access to the location is denied

I have an application, it is necessary to access the location to use the application
But I don't know how to direct it to the settings about denying access to the Location
I put check when I run the application that the application could not access the site, it directs it to this view
struct LocationRequestView: View {
var body: some View {
ZStack {
Button(action: { LocationManager.shared.requestAllowLocation()}) {
Text("Allow access to the Location")
}
}
}
}
I want when the status is case .denied: in didChangeAuthorization, it directs it to the settings to activate the Location
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var userLocation: CLLocation?
let manager = CLLocationManager()
static let shared = LocationManager()
#Published var cityName = ""
#Published var countryName = ""
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
requestAllowLocation()
print("Not1")
case .restricted:
print("Not2")
case .denied:
print("Not3")
///Here
case .authorizedAlways:
print("Not4")
case .authorizedWhenInUse:
print("Not5")
case .authorized:
break
}
}
func requestAllowLocation() {
manager.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let lastLocation = locations.last else { return}
DispatchQueue.main.async {
let geocoder = CLGeocoder()
self.userLocation = CLLocation(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)
geocoder.reverseGeocodeLocation(self.userLocation!) { (placemarks, error) in
self.processResponse(withPlacemarks: placemarks, error: error)
}
}
}
func processResponse(withPlacemarks placemarks: [CLPlacemark]?, error: Error?) {
if let error = error {
print("Unable to Reverse Geocode Location (\(error))")
} else {
if let placemarks = placemarks, let placemark = placemarks.first {
self.cityName = placemark.locality ?? ""
self.countryName = placemark.country ?? ""
} else {
print("No matching address found")
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func fetchCountryAndCity(for location: CLLocation?) {
guard let location = location else { return }
}
}
Try this:
if let url = URL.init(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

Why does the Array return empty in Swift?

I am trying to add annotations to my MapKit, however the array I append to is returning nil. I used this method before to populate CollectionView in previous ViewControllers and the array had values, now if I print the array it returns 0 values, I am sure that there should be data because It is printed in the console, however not being added to the array. Thoughts please?
My Manager Class:
class GoogleMapsAPIManager {
var bloodBanksArray = [BloodBanksModel]()
func fetchCity(latitude: CLLocationDegrees, longitude: CLLocationDegrees, raduis: Double){
let urlString = "\(K.googleMapsAPI)&location=\(latitude),\(longitude)&radius=\(raduis)&key=\(K.apiKey)"
print(urlString)
performRequest(with: urlString)
}
//MARK: - Decoding JSON
func performRequest(with urlString: String){
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
self.parseJSON(bloodBankData: safeData)
}
}
task.resume()
}
}
func parseJSON(bloodBankData: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(BloodBanksData.self, from: bloodBankData)
for i in 0...decodedData.results.count - 1 {
bloodBanksArray.append(BloodBanksModel(name: decodedData.results[i].name, photo: decodedData.results[i].photos?[0].photoReference ?? "ATtYBwJjqXlw3DMdL74SRtgG_GRA3LkAET6hDJXHWtkQMaOTo1B3Gx9jrDTXFLFabGStSxX8BiYdLAnknF7A9ynw33KKyUh5Oc55A9vXzo_6nd4mnk5Sx-iOqMNaw21dk0C424PWaio9GiogQaKecYxOT1q-bmj30syypZmyxkjF7r3-gFyC", open_now: decodedData.results[i].openingHours?.openNow ?? false, vincinity: decodedData.results[i].vicinity, rating: decodedData.results[i].rating, placeId: decodedData.results[i].placeId, lat: decodedData.results[i].geometry.location.lat, lng: decodedData.results[i].geometry.location.lng))
}
print("bossins \(bloodBanksArray)")
} catch {
print(error)
}
}
}
Model Class:
struct BloodBanksModel {
let name: String
let photo: String
let open_now: Bool
let vincinity: String
let rating: Double
let placeId: String
let lat: Double
let lng: Double
}
Data:
import Foundation
// MARK: - BloodBanksData
struct BloodBanksData: Codable {
let results: [Result]
enum CodingKeys: String, CodingKey {
case results
}
}
// MARK: - Result
struct Result: Codable {
let geometry: Geometry
let name: String
let openingHours: OpeningHours?
let photos: [Photo]?
let rating: Double
let vicinity: String
let placeId: String
enum CodingKeys: String, CodingKey {
case geometry, name
case openingHours = "opening_hours"
case photos
case rating
case vicinity
case placeId = "place_id"
}
}
// MARK: - Geometry
struct Geometry: Codable {
let location: Location
}
// MARK: - Location
struct Location: Codable {
let lat, lng: Double
}
// MARK: - OpeningHours
struct OpeningHours: Codable {
let openNow: Bool
enum CodingKeys: String, CodingKey {
case openNow = "open_now"
}
}
// MARK: - Photo
struct Photo: Codable {
let photoReference: String
enum CodingKeys: String, CodingKey {
case photoReference = "photo_reference"
}
}
In my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
//mapView.delegate = self
locationManager.requestWhenInUseAuthorization()
var currentLocation: CLLocation!
if
CLLocationManager.authorizationStatus() == .authorizedWhenInUse ||
CLLocationManager.authorizationStatus() == .authorizedAlways
{
currentLocation = locationManager.location
googleMapsAPIManager.fetchCity(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude, raduis: 100000.0)
}
for location in googleMapsAPIManager.bloodBanksArray {
let annotation = MKPointAnnotation()
annotation.title = location.name
annotation.coordinate = CLLocationCoordinate2D(latitude: location.lat, longitude: location.lng)
annotations.append(annotation)
}
mapView.addAnnotations(annotations)
}
As I said in your previous question fetchCity works asynchronously, you have to add a completion handler
For convenience reasons I merged fetchCity, performRequest and parseJSON() into one method.
func fetchCity(latitude: CLLocationDegrees, longitude: CLLocationDegrees, raduis: Double, completion: #escaping () -> Void){
let urlString = "\(K.googleMapsAPI)&location=\(latitude),\(longitude)&radius=\(raduis)&key=\(K.apiKey)"
print(urlString)
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(BloodBanksData.self, from: data!)
for i in 0..<decodedData.results.count {
bloodBanksArray.append(BloodBanksModel(name: decodedData.results[i].name, photo: decodedData.results[i].photos?[0].photoReference ?? "ATtYBwJjqXlw3DMdL74SRtgG_GRA3LkAET6hDJXHWtkQMaOTo1B3Gx9jrDTXFLFabGStSxX8BiYdLAnknF7A9ynw33KKyUh5Oc55A9vXzo_6nd4mnk5Sx-iOqMNaw21dk0C424PWaio9GiogQaKecYxOT1q-bmj30syypZmyxkjF7r3-gFyC", open_now: decodedData.results[i].openingHours?.openNow ?? false, vincinity: decodedData.results[i].vicinity, rating: decodedData.results[i].rating, placeId: decodedData.results[i].placeId, lat: decodedData.results[i].geometry.location.lat, lng: decodedData.results[i].geometry.location.lng))
}
print("bossins \(bloodBanksArray)")
completion()
} catch {
print(error)
}
}
task.resume()
}
}
And call it
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse ||
CLLocationManager.authorizationStatus() == .authorizedAlways {
currentLocation = locationManager.location
googleMapsAPIManager.fetchCity(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude, raduis: 100000.0) {
for location in googleMapsAPIManager.bloodBanksArray {
let annotation = MKPointAnnotation()
annotation.title = location.name
annotation.coordinate = CLLocationCoordinate2D(latitude: location.lat, longitude: location.lng)
annotations.append(annotation)
}
mapView.addAnnotations(annotations)
}
}
Be aware that Core Location works asynchronously, too.

swift - How to perform task completion

I'm trying to do reverse geocoding for multiple locations at the same time. So I create a function performReverseGeoLocation. The problem is, that since CLGeocoder().reverseGeocodeLocation a closure, the completionHandlerLocations will get executed first. How do I change these functions so that the caller will get completion handler after all CLGeocoder().reverseGeocodeLocation inside the for loop is done?
Code I have tried:
private func getImageLocation() {
performReverseGeoLocation(completionHandlerLocations: { (cities, countries) in
print("***** This is executed before the reverse geo code location is done")
})
}
private func performReverseGeoLocation(completionHandlerLocations: #escaping (_ cities: [String], _ countries: [String]) -> Void) {
var cities = [String]()
var countries = [String]()
for image in self.images {
let longitude = image.longitude
let latitude = image.latitude
let location = CLLocation(latitude: latitude, longitude: longitude)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print("***** This is executed after completionHandlerLocations is done")
if error != nil {
self.alertError("Reverse geocoder failed with error" + (error?.localizedDescription)!)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
let country = pm.country
let city = pm.locality
if (!cities.contains(city!)) {
cities.append(city!)
}
if (!countries.contains(country!)) {
countries.append(country!)
}
}
else {
self.alertError("Fail to perform reverse geo location")
}
})
}
// THIS IS WILL EXECUTED FIRST
completionHandlerLocations(cities, countries)
}
You can do something like this:
var count = 0
for image in self.images {
...
CLGeocoder().reverseGeocodeLocation(location) {
// get result
counter ++
if count == self.images.count { // finish all requests
completionHandlerLocations(cities, countries)
}
}
}
That's the most simple way to do.
As i-am-jorf mentioned, you can create a DispatchGroup and wait for the notification when all reverse geocoding tasks are complete:
private func performReverseGeoLocation(completionHandlerLocations: #escaping (_ cities: [String], _ countries: [String]) -> Void) {
let group = DispatchGroup()
var cities = [String]()
var countries = [String]()
self.images.forEach { (location) in
group.enter()
let longitude = image.longitude
let latitude = image.latitude
let location = CLLocation(latitude: latitude, longitude: longitude)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (placemark, error) in
// do all your checks...
if placemark != nil && placemark!.count > 0 {
cities.append(placemark!.first!.locality!)
countries.append(placemark!.first!.country!)
}
group.leave()
})
}
group.notify(queue: DispatchQueue.main) {
completionHandlerLocations(cities, countries)
}
}

Passing in values to a closure function

Check out this code:
func getReversedGeocodeLocation(location: CLLocation, completionHandler: #escaping ()->()) {
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks != nil {
if placemarks!.count > 0 {
let pm = placemarks![0]
if let addressDictionary: [AnyHashable: Any] = pm.addressDictionary,
let addressDictionaryFormatted = addressDictionary["FormattedAddressLines"] {
let address = (addressDictionaryFormatted as AnyObject).componentsJoined(by: ", ")
self.addressInViewController = address
}
completionHandler()
}
} else {
print("Problem with the data received from geocoder")
}
})
}
In the viewController
override func viewDidLoad() {
var addressInViewController = String()
getReversedGeocodeLocation(location: location, completionHandler: {
print("After geo finished")
})
}
This is a simple case for using closures. As you can see, when the reverse geo finishes, it updates the addressInViewController variable which is defined outside the function itself. I'm a bit confused when it comes to closures but I do know it's essentially passing in another function as a parameter, into a function. So can I pass in something like (_ String: x)->() instead of ()->() where the address variable would be populated from the main reverse geo function and passed along? I tried doing that but it says "x" is undefined. If this is achieve-able then I guess I can decouple my code in a better way using closures.
Thanks and have a great day :)
define your methods like this
func getReversedGeocodeLocation(location: CLLocation, completionHandler: #escaping (_ value : Any)->()) {
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks != nil {
if placemarks!.count > 0 {
let pm = placemarks![0]
if let addressDictionary: [AnyHashable: Any] = pm.addressDictionary,
let addressDictionaryFormatted = addressDictionary["FormattedAddressLines"] {
let address = (addressDictionaryFormatted as AnyObject).componentsJoined(by: ", ")
self.addressInViewController = address
}
completionHandler(address)
}
} else {
print("Problem with the data received from geocoder")
}
})
}
override func viewDidLoad() {
var addressInViewController = String()
getReversedGeocodeLocation(location: location, completionHandler: { (_ values : Any) in
self. addressInViewController = values
})
}
Make dataType of Value according to your need.

CLGeocoder error. GEOErrorDomain Code=-3

When I tried to use reverse geocoding,this error message showed up.
Geocode error: Error Domain=GEOErrorDomain Code=-3 "(null)"
My code is below:
import CoreLocation
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if let placemarks = placemarks {
reverseGeocodeLocations[hash] = placemarks
}
callback(placemarks, error)
}
This works only time to time, and I request reverseGeocode several times per seconds. So I guess this error message is related to the limit of request or something?
Is there any documentation about apple’s geocode request?
Thanks for advance.
Updated
here is my entire code for requesting
import CoreLocation
fileprivate struct ReverseGeocodeRequest {
private static let geocoder = CLGeocoder()
private static var reverseGeocodeLocations = [Int: [CLPlacemark]]()
private static let reverseGeocodeQueue = DispatchQueue(label: "ReverseGeocodeRequest.reverseGeocodeQueue")
private static var nextPriority: UInt = 0
fileprivate static func request(location: CLLocation, callback: #escaping ([CLPlacemark]?, Error?)->Void) {
let hash = location.hash
if let value = reverseGeocodeLocations[hash] {
callback(value, nil)
} else {
reverseGeocodeQueue.async {
guard let value = reverseGeocodeLocations[hash] else {
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if let placemarks = placemarks {
reverseGeocodeLocations[hash] = placemarks
}
callback(placemarks, error)
}
return
}
callback(value, nil)
}
}
}
let priority: UInt
let location: CLLocation
let handler : ([CLPlacemark]?, Error?)->Void
private init (location: CLLocation, handler: #escaping ([CLPlacemark]?, Error?)->Void) {
ReverseGeocodeRequest.nextPriority += 1
self.priority = ReverseGeocodeRequest.nextPriority
self.location = location
self.handler = handler
}
}
extension ReverseGeocodeRequest: Comparable {
static fileprivate func < (lhs: ReverseGeocodeRequest, rhs: ReverseGeocodeRequest) -> Bool {
return lhs.priority < rhs.priority
}
static fileprivate func == (lhs: ReverseGeocodeRequest, rhs: ReverseGeocodeRequest) -> Bool {
return lhs.priority == rhs.priority
}
}
extension CLLocation {
func reverseGeocodeLocation(callback: #escaping ([CLPlacemark]?, Error?)->Void) {
ReverseGeocodeRequest.request(location: self, callback: callback)
}
func getPlaceName(callback: #escaping (Error?, String?)->Void) {
self.reverseGeocodeLocation { (placemarks, error) in
guard let placemarks = placemarks, error == nil else {
callback(error, nil)
return
}
guard let placemark = placemarks.first else {
callback(nil, "Mysterious place")
return
}
if let areaOfInterest = placemark.areasOfInterest?.first {
callback(nil, areaOfInterest)
} else if let locality = placemark.locality {
callback(nil, locality)
} else {
callback(nil, "On the Earth")
}
}
}
}
After searching everywhere for the answer it was in Apples docs! :/
https://developer.apple.com/reference/corelocation/clgeocoder/1423621-reversegeocodelocation
Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value network to your completion handler.
When checking the error code in the completion handler it is indeed Network Error:2
Hope this helps someone!

Resources