Геолокация в приложении Погода


#1

Здравствуйте!

В курсе по API мы создаем приложение “Погода” и координаты там вбиты как константы. Однако в 24 видео нам показывается как можно получить координаты устройства, но не объясняется как их применять, если просто подставлять их в функции получения геолокации то все падает. Я так понимаю из за того что геолокация запускается после прогрузки остального кода.
Как бы не пытался связать это приложение с геолокацией, все никак…
Может кто то делал уже именно с этим приложением или может объяснить как работать с геолокацией которая выполняет свою работу после всего остального кода.


#2

Скорее всего, надо сначала запросить у пользователя разрешение на получение геоданных, а затем уже их запрашивать.


#3

С этим все нормально, во viewDidLoad запрос есть


#4

Сама геолокация работает, координаты в консоль выводит если написать принт внутри функции. Но если передавать их в конструктор для формирования ссылки, то приложение падает, так как ссылка формируется раньше и там оказываются nil вместо координат.


#5

Ну, чую что там где-то force unwraped юзается.


#6

По всякому пробовал, никак не удалось завести приложение с координатами из геолокации.
Может все же есть кто проходил данный курс и у него получилось или примеры есть у кого с дальнейшим использованием координат?


#7

Покажите хоть кусок кода.


#8

Координаты подставляются в константу Coordinates в ViewController.
Coordinates является структурой в APIWeatherManager.

ViewController
import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

  @IBOutlet weak var locationLabel: UILabel!
  @IBOutlet weak var imageView: UIImageView!
  @IBOutlet weak var pressureLabel: UILabel!
  @IBOutlet weak var humidityLabel: UILabel!
  @IBOutlet weak var temperatureLabel: UILabel!
  @IBOutlet weak var appearentTemperatureLabel: UILabel!
  @IBOutlet weak var refreshButton: UIButton!
 @IBOutlet weak var activiryIndicator: UIActivityIndicatorView!

  let locationManager = CLLocationManager()

  @IBAction func refreshButtonTapped(_ sender: UIButton) {
    toggleActivityIndicator(on: true)
    fetchCurrentWeatherData()
  }

  func toggleActivityIndicator(on: Bool) {
    refreshButton.isHidden = on
    if on {
      activiryIndicator.startAnimating()
} else {
  activiryIndicator.stopAnimating()
}
  }

  lazy var weatherManager = APIWeatherManager(apiKey: "2a6d8e376a69c1ae07d4a52dd0c2dfdc")
  let coordinates = Coordinates(latitude: 54.741704, longitude: 55.984471)

  override func viewDidLoad() {
super.viewDidLoad()

locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()

fetchCurrentWeatherData()
  }

  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation = locations.last! as CLLocation

let geoCoder = CLGeocoder()

geoCoder.reverseGeocodeLocation(userLocation, completionHandler: { (placemarks, error) -> Void in
    if let city = placemarks?[0].locality {
        self.locationLabel.text = city
    }
})

print("my locayion latitude: \(userLocation.coordinate.latitude), longitude: \(userLocation.coordinate.longitude)")
  }

  func fetchCurrentWeatherData(){
weatherManager.fetchCurrentWeatherWith(coordinates: coordinates) { (result) in
  self.toggleActivityIndicator(on: false)
  
  switch result {
  case .Success(let currentWeather):
    self.updateUIWith(currentWeather: currentWeather)
  case .Failure(let error as NSError):
    
    let alertController = UIAlertController(title: "Unable to get data ", message: "\(error.localizedDescription)", preferredStyle: .alert)
    let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
    alertController.addAction(okAction)
    
    self.present(alertController, animated: true, completion: nil)
  default: break
  }
}
  }

  func updateUIWith(currentWeather: CurrentWeather) {

self.imageView.image = currentWeather.icon
self.pressureLabel.text = currentWeather.pressureString
self.temperatureLabel.text = currentWeather.temperatureString
self.appearentTemperatureLabel.text = currentWeather.appearentTemperatureString
self.humidityLabel.text = currentWeather.humidityString
  }
    }
APIManager
import Foundation

typealias JSONTask = URLSessionDataTask
typealias JSONCompletionHandler = ([String: AnyObject]?, HTTPURLResponse?, Error?) -> Void

protocol JSONDecodable {
  init?(JSON: [String: AnyObject])
}

protocol FinalURLPoint {
  var baseURL: URL { get }
  var path: String { get }
  var request: URLRequest { get }
}

enum APIResult<T> {
  case Success(T)
 case Failure(Error)
}

protocol APIManager {
  var sessionConfiguration: URLSessionConfiguration { get }
 var session: URLSession { get }

  func JSONTaskWith(request: URLRequest, completionHandler: @escaping JSONCompletionHandler) -> JSONTask
  func fetch<T: JSONDecodable>(request: URLRequest, parse: @escaping ([String: AnyObject]) -> T?, completionHandler: @escaping (APIResult<T>) -> Void)

}

extension APIManager {
  func JSONTaskWith(request: URLRequest, completionHandler: @escaping JSONCompletionHandler) -> JSONTask {

let dataTask = session.dataTask(with: request) { (data, response, error) in
  
  guard let HTTPResponse = response as? HTTPURLResponse else {
    
    let userInfo = [
      NSLocalizedDescriptionKey: NSLocalizedString("Missing HTTP Response", comment: "")
    ]
    let error = NSError(domain: SWINetworkingErrorDomain, code: 100, userInfo: userInfo)
    
    completionHandler(nil, nil, error)
    return
  }

  if data == nil {
    if let error = error {
      completionHandler(nil, HTTPResponse, error)
    }
  } else {
    switch HTTPResponse.statusCode {
    case 200:
      do {
        let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: AnyObject]
        completionHandler(json, HTTPResponse, nil)
      } catch let error as NSError {
        completionHandler(nil, HTTPResponse, error)
      }
    default:
      print("We have got response status \(HTTPResponse.statusCode)")
    }
  }
}
return dataTask
  }

  func fetch<T>(request: URLRequest, parse: @escaping ([String: AnyObject]) -> T?, completionHandler: @escaping (APIResult<T>) -> Void) {

let dataTask = JSONTaskWith(request: request) { (json, response, error) in
  DispatchQueue.main.async(execute: {
    guard let json = json else {
      if let error = error {
        completionHandler(.Failure(error))
      }
      return
    }
    
    if let value = parse(json) {
      completionHandler(.Success(value))
    } else {
      let error = NSError(domain: SWINetworkingErrorDomain, code: 200, userInfo: nil)
      completionHandler(.Failure(error))
    }
    
  })
}
dataTask.resume()
  }
    }
APIWeatherManager
import Foundation

struct Coordinates {
  let latitude: Double
  let longitude: Double
}

enum ForecastType: FinalURLPoint {
  case Current(apiKey: String, coordinates: Coordinates)

  var baseURL: URL {
    return URL(string: "https://api.forecast.io")!
  }

  var path: String {
    switch self {
    case .Current(let apiKey, let coordinates):
      return "/forecast/\(apiKey)/\(coordinates.latitude),\(coordinates.longitude)"
    }
  }

  var request: URLRequest {
    let url = URL(string: path, relativeTo: baseURL)
    return URLRequest(url: url!)
  }
}



final class APIWeatherManager: APIManager {
  var sessionConfiguration: URLSessionConfiguration

  lazy var session: URLSession = {
    return URLSession(configuration: self.sessionConfiguration)
  } ()

  let apiKey: String

  init(sessionConfiguration: URLSessionConfiguration, apiKey: String) {
    self.sessionConfiguration = sessionConfiguration
    self.apiKey = apiKey
  }

  convenience init(apiKey: String) {
    self.init(sessionConfiguration: URLSessionConfiguration.default, apiKey: apiKey)
  }

  func fetchCurrentWeatherWith(coordinates: Coordinates, completionHandler: @escaping      (APIResult<CurrentWeather>) -> Void) {
    let request = ForecastType.Current(apiKey: self.apiKey, coordinates: coordinates).request

fetch(request: request, parse: { (json) -> CurrentWeather? in
  if let dictionary = json["currently"] as? [String: AnyObject] {
    return CurrentWeather(JSON: dictionary)
  } else {
    return nil
  }
  
  }, completionHandler: completionHandler)
  }
}
CurrentWeather
import Foundation
import UIKit

struct CurrentWeather {
let temperature: Double
let apparentTemperature: Double
let humidity: Double
let pressure: Double
let icon: UIImage
}

extension CurrentWeather: JSONDecodable {
init?(JSON: [String : AnyObject]) {
    guard let temperature = JSON["temperature"] as? Double,
        let apparentTemperature = JSON["apparentTemperature"] as? Double,
        let humidity = JSON["humidity"] as? Double,
        let pressure = JSON["pressure"] as? Double,
        let iconString = JSON["icon"] as? String else {
            return nil
    }
    
    let icon = WeatherIconManager(rawValue: iconString).image
    
    self.temperature = temperature
    self.apparentTemperature = apparentTemperature
    self.humidity = humidity
    self.pressure = pressure
    self.icon = icon
    
    }
    }


extension CurrentWeather {
var pressureString: String {
    return "\(Int(pressure * 0.750062)) mm"
}

var humidityString: String {
    return "\(Int(humidity * 100)) %"
}

var temperatureString: String {
    return "\(Int(5 / 9 * (temperature - 32)))˚C"
}

var appearentTemperatureString: String {
    return "Feels like: \(Int(5 / 9 * (apparentTemperature - 32)))˚C"
}
}

#9

Отредактируйте пожалуйста код со специальной разметкой для удобного чтения.


#11

Отредактировал(20 символов)


#12

А в каком месте падает?


#13

Это вариант рабочий, но координаты тут вбиты константами, хотелось бы понять как использовать координаты которые мы получаем в locationManager


#14
  var coordinates: Coordinates!

  override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestAlwaysAuthorization()
        locationManager.startUpdatingLocation()
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let userLocation = locations.last! as CLLocation
        let geoCoder = CLGeocoder()

        geoCoder.reverseGeocodeLocation(userLocation, completionHandler: { (placemarks, error) -> Void in
            if let city = placemarks?[0].locality {
                self.locationLabel.text = city
            }
        })

        coordinates = userLocation.coordinate // если это нужный тип, иначе поправьте сами
        fetchCurrentWeatherData()

        print("my locayion latitude: \(userLocation.coordinate.latitude), longitude: \(userLocation.coordinate.longitude)")
      }

Попробуйте так


#15

То есть весь секрет был в переносе fetchCurrentWeatherData() в locationManager… просто выносить инициализацию coordinates как я только не пробовал.
Значит при работе с геолокацией финальную функцию всегда добавляем в locationManager, будем знать.
Спасибо, все работает!


#16

Я чисто так на шару по кнопочкам стучу и что-то получается… шучу :slight_smile:
Была тут недавно тема, аналогичная проблема с порядком действий.
Нужно понимать в какой момент времени вызывать нужные методы.


#17

Когда есть скилл, “стучать по кнопочкам” и все работает, как у вас, уже не такая уж и шутка:)
То что геолокация отрабатывает после всего кода я понял, но как бороться с этим- не очень, мысли уже были в сторону многопоточности с очередями:)


#18

Как вариант, можете сделать LocationManager singleton, в нем создать переменную coordinates = nil.
Запускать LocationManager при старте приложения из AppDelegate и вызывать определение локации, при определении записывать в coordinates.
В вашем VC в методе viewDidLoad обратиться к LocationManager, проверить не пуста ли переменная coordinates, если не пустая, вызывать ваш метод fetch, если пустая, заного вызвать определение локации и в замыкании вызвать fetch с полученными координатами.
Либо в LocationManager при определении локации вызывать NotificationCenter.post(). В вашем VC подписаться на конкретный post. В методе viewDidLoad опять проверяете переменную из LocationManager’a coordinaes на nil, только если она nil, ничего не делаете и ждете когда сработает NotificationCenter. Если она уже не nil, вызываете fetch и передаете coordinates.

Если я все правильно описал, то 2й способ лучше, т.к. не требует лишнего определения локации.


#19

Идеи интересные и могут пригодиться в будущем, обязательно по экспериментирую на днях, спасибо!