Swift - How to get OpenWeatherMap JSON Data? - ios

The following app should get the user's current location and then display the Name and Temperature of that location using OpenWeatherMap.
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var location: UILabel!
#IBOutlet weak var temperature: UILabel!
var locationManager: CLLocationManager = CLLocationManager()
var startLocation: CLLocation!
func extractData(weatherData: NSData) {
let json = try? NSJSONSerialization.JSONObjectWithData(weatherData, options: []) as! NSDictionary
if json != nil {
if let name = json!["name"] as? String {
location.text = name
}
if let main = json!["main"] as? NSDictionary {
if let temp = main["temp"] as? Double {
temperature.text = String(format: "%.0f", temp)
}
}
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let latestLocation: AnyObject = locations[locations.count - 1]
let lat = latestLocation.coordinate.latitude
let lon = latestLocation.coordinate.longitude
// Put together a URL With lat and lon
let path = "http://api.openweathermap.org/data/2.5/weather?lat=\(lat)&lon=\(lon)&appid=2854c5771899ff92cd962dd7ad58e7b0"
print(path)
let url = NSURL(string: path)
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { (data, response, error) in
dispatch_async(dispatch_get_main_queue(), {
self.extractData(data!)
})
}
task.resume()
}
func locationManager(manager: CLLocationManager,
didFailWithError error: NSError) {
}
override func viewDidLoad() {
super.viewDidLoad()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
startLocation = nil
}
}
I've been learning how to get data from OpenWeatherMap following this tutorial:
https://www.youtube.com/watch?v=r-LZs0De7_U
The app crashes at:
self.extractData(data!)
as data is equal to nil, this shouldn't be happening as when I copy and paste the printed path into my web browser, the data is there. I'm sure I've followed the tutorial correctly, so what's the problem and how do I fix it?

The problem is with Transport Security - which causes issues for lots of us. Here's one of the SO answers explaining how to resolve it Transport security has blocked a cleartext HTTP
If you make the setting in your plist - set the NSAllowsArbitraryLoads key to YES under NSAppTransportSecurity dictionary in your .plist file - then it works.

Related

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

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.

URLQuery item does not grab the location coordinate

I am wondering what can the be reason of my issue. I am using core location in order to get the my coordinates location, which I use in the network method as a URLQueryItem in order to get a response from the API. But the console output shows that the latitude query and longitude query are both equal to 0 while I have my a latitude and longitude value. I use the network method inside my viewdidload.
Thanks for all responses and explanations.
var queryLattitudeItem : Double = 0
var queryLongitudeItem : Double = 0
func network () {
let configuration = URLSessionConfiguration.default
configuration.waitsForConnectivity = true
let session = URLSession(configuration: configuration)
guard let urls = URL(string:"https://api.yelp.com/v3/businesses/search") else { return }
var urlcomponent = URLComponents(string: "\(urls)")
let queryLat = URLQueryItem(name:"latitude" , value: "\(queryLattitudeItem)")
let queryLong = URLQueryItem(name: "longitude", value: "\(queryLongitudeItem)")
let queryItemterm = URLQueryItem(name: "term", value: "restaurant")
let queryLimit = URLQueryItem(name: "limit", value: "10")
urlcomponent?.queryItems = [queryItemterm,queryLat,queryLong,queryLimit]
print(urlcomponent!)
print(queryLat)
print(queryLong)
var request = URLRequest(url: urlcomponent!.url!)
request.httpMethod = "GET"
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = session.dataTask(with: request) { (data, response, error) in
if let response = response as? HTTPURLResponse {
print(response)
} else{
print("error")
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[locations.count - 1]
if location.horizontalAccuracy > 0 {
locationManager.stopUpdatingLocation()
print("\(location.coordinate.longitude), \(location.coordinate.latitude)")
}
let latitude : Double = (location.coordinate.latitude)
let longitude : Double = location.coordinate.longitude
print("This is lat: \(latitude), et long\(longitude)")
queryLattitudeItem = latitude
queryLongitudeItem = longitude
}
Console output
https://api.yelp.com/v3/businesses/search?term=restaurant&latitude=0.0&longitude=0.0&limit=10
latitude=0.0
longitude=0.0
-73.984638, 40.759211
This is lat: 40.759211, et long-73.984638
<NSHTTPURLResponse: 0x600003a91ec0> { URL: https://api.yelp.com/v3/businesses/search?term=restaurant&latitude=0.0&longitude=0.0&limit=10 } { Status Code: 200, Headers {
"Accept-Ranges" = (
One stylistic thing I'd do with your code is utilize some sort of structure for storing strings so they're not littered throughout your code. When something fouls up, you can go to one spot to debug it rather than plowing through a bunch of code. Here, I store the string in an enum as a static let (b/c I hate rawValues):
enum Endpoint {
static let yelp = "https://api.yelp.com/v3/businesses/search"
}
Next, I'd ditch the var declarations for latitude and longitude:
var queryLattitudeItem : Double = 0 // 🚫 nuke
var queryLongitudeItem : Double = 0 // 🚫 nuke
Instead, I'd update your network request method to accept a CLLocationCoordinate2D straight from the delegate method, like so:
func getYelpInfo(for coordinate: CLLocationCoordinate2D) {
// omitted your networking code...this is just the URL creation code
var components = URLComponents(string: Endpoint.yelp)
let queryLat = URLQueryItem(name: "latitude", value: String(coordinate.latitude))
let queryLong = URLQueryItem(name: "longitude", value: String(coordinate.latitude))
let queryLimit = URLQueryItem(name: "limit", value: "10")
components?.queryItems = [queryLat, queryLong, queryLimit]
// You could use a guard statement here if you want to exit out, too
if let url = components?.url {
var request = URLRequest(url: url)
// do your networking request
}
print(components!.url!.absoluteString)
}
Next, in your didUpdateLocations, I'd call the updated method, like so:
getYelpInfo(for: location.coordinate)
Your updated method looks like this:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[locations.count - 1]
if location.horizontalAccuracy > 0 {
locationManager.stopUpdatingLocation()
getYelpInfo(for: location.coordinate)
print("\(location.coordinate.longitude), \(location.coordinate.latitude)")
}
}

Swift- Pass textfield from swift file to other swift file

I am a newbie
and I try to explain what my problem is:
I have a swift file its name is feedmodel.swift:
import Foundation
protocol FeedmodelProtocol: class {
func itemsDownloaded(items: NSArray)
}
class Feedmodel: NSObject, URLSessionDataDelegate {
weak var delegate: FeedmodelProtocol!
func downloadItems() {
let myUrl = URL(string: "http://example.net/stock_service4.php");
let defaultSession = Foundation.URLSession(configuration: URLSessionConfiguration.default)
var request = URLRequest(url:myUrl!)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let postString = "Latitudee=19.4&Longitudee=-99.1";
request.httpBody = postString.data(using: .utf8)
let task = defaultSession.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(String(describing: error))")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
}
let responseString = String(data: data, encoding: .utf8)
print("responseString = \(String(describing: responseString))")
self.parseJSON(data)
}
task.resume()
}
and I have a swift file its name is NeuerBeitragViewController:
import UIKit
import CoreLocation
class NeuerBeitragViewController: UIViewController,CLLocationManagerDelegate {
#IBOutlet weak var Tankstelle: UITextField!
#IBOutlet weak var Kraftstoff1: UITextField!
#IBOutlet weak var Preis1: UITextField!
#IBOutlet weak var Kraftstoff2: UITextField!
#IBOutlet weak var Preis2: UITextField!
#IBOutlet weak var Notiz: UITextField!
#IBOutlet weak var Longitude: UITextField!
#IBOutlet weak var Latitude: UITextField!
var locationManager: CLLocationManager = CLLocationManager()
var startLocation: CLLocation!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
startLocation = nil
}
#IBAction func startWhenInUse(_ sender: Any) {
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
let latestLocation: CLLocation = locations[locations.count - 1]
Latitude.text = String(format: "%.4f",
latestLocation.coordinate.latitude)
Longitude.text = String(format: "%.4f",
latestLocation.coordinate.longitude)
}
func locationManager(_ manager: CLLocationManager,
didFailWithError error: Error) {
print(error.localizedDescription)
}
In my NeuerBeitragViewController.Swift
I have this line:
Latitude.text = String(format: "%.4f",
latestLocation.coordinate.latitude)
Longitude.text = String(format: "%.4f",
latestLocation.coordinate.longitude)
And I want to get the Value for Latitude.text and Longitude.text
in my Feedmodel.swift in this line here:
let postString = "firstName=19.4&lastName=-99.1";
So that I can do this here:
let postString = "firstName=\(Latitude.text)&lastName=\Longitude.text";
Hope you guys understood what I need and can help.
Thank You!
To pass your information from one view to another you have several options :
pass it through the segue (one to one)
use protocols & delegates (one to one)
use events & observers (one to many)
use a third class responsible for holding the current data (one to many)
In your case, using Protocols & Deleguates is the proper option to choose.
Example provided here.

For loop keeps iteratively parsing JSON even after condition is complete

I'm putting JSON data into a table view, and I'm trying to parse through the data using a for loop. However, when the loop is done parsing through the JSON data and has placed the 20 items into the table view, it restarts the process, parses the JSON again, and the same data appears in the table view again. This process repeats for a long time as well.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else{ return }
var searchURL = NSString(format: "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=%f,%f&radius=50000&types=night_club&key=MY_API_KEY", (location.coordinate.latitude),(location.coordinate.longitude)) as? String
var cityInfo = NSString(format: "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=%f,%f&radius=50000&types=locality&key=MY_API_KEY", (location.coordinate.latitude),(location.coordinate.longitude)) as? String
manager.stopUpdatingLocation()
getCityInfo(url: cityInfo!)
callAlamo(url: searchURL!)
}
func getCityInfo(url:String){
Alamofire.request(url).responseJSON(completionHandler: { response in
self.parseJSON(JSONData: response.data!)
})
}
func parseJSON(JSONData:Data){
do{
var readableJSON = try JSONSerialization.jsonObject(with: JSONData) as! JSONStandard
// PARSING THROUGH JSON DATA TO GET CITY NAME
if let results = readableJSON["results"] as? [JSONStandard]{
for i in 0..<results.count{
let item = results[i]
let cityInfo = item["name"] as! String
cityName.append(cityInfo)
// GETTING PHOTO URL WITH photo_reference AND PUTTING THEM INTO imageURL ARRAY
if let photos = item["photos"] as? [JSONStandard]{
for j in 0..<photos.count{
let photo = photos[j] as JSONStandard
let photoRef = photo["photo_reference"] as! String
let photoURL = NSString(format: "https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=%#&key=MY_API_KEY", photoRef) as? String
cityURL.append(photoURL!)
}
}
}
}
cityLabel.text = cityName[0]
cityImage.sd_setImage(with: URL(string:cityURL[0]), placeholderImage: #imageLiteral(resourceName: "cityOfCalgary"))
}
catch{
print(error)
}
}
func callAlamo(url:String){
Alamofire.request(url).responseJSON(completionHandler: { response in
self.parseData(JSONData: response.data!)
})
}
func parseData(JSONData:Data){
do{
var myReadableJSON = try JSONSerialization.jsonObject(with: JSONData, options: .mutableContainers) as! JSONStandard
// PARSING THROUGH JSON DATA TO GET NAMES AND PICTURES OF PLACES, THEN PUTTING
// THEM INTO AN ARRAY AND OUTPUTTING THEM ONTO TABLE VIEW CELL
if let results = myReadableJSON["results"] as? [JSONStandard]{
for i in 0..<results.count{ //results.count = 20
let item = results[i]
let names = item["name"] as! String
placeNames.append(names)
// GETTING PHOTO URL WITH photo_reference AND PUTTING THEM INTO imageURL ARRAY
if let photos = item["photos"] as? [JSONStandard]{
let photoRef = photos[0]
let photoReference = photoRef["photo_reference"] as! String
let photoURL = NSString(format: "https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=%#&key=MY_API_KEY", photoReference) as? String
imageURL.append(photoURL!)
}
if let geometry = item["geometry"] as? JSONStandard{
if let location = geometry["location"] as? [String : Any]{
let latitude = location["lat"] as? Double
let longitude = location["lng"] as? Double
}
}
}
}
// SHOULD BE PLACED AT THE END OF GATHERING DATA
locationManager.stopUpdatingLocation()
self.tableView.reloadData()
}
catch{
print(error)
}
}
UPDATE:
As vadian had mentioned in one of his first comments, parseData() was getting called multiple times. So I added
locationManager.delegate = nil
after I stop updating the location in the locationManager delegate function.
`
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else{ return }
searchURL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(location.coordinate.latitude),\(location.coordinate.longitude)&radius=50000&types=night_club&key=MY_API_KEY"
cityInfo = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(location.coordinate.latitude),\(location.coordinate.longitude)&radius=50000&types=locality&key=MY_API_KEY"
locationManager.stopUpdatingLocation()
locationManager.delegate = nil
getCityInfo(url: cityInfo)
callAlamo(url: searchURL)
}
`
Everything else remains the same after this.
As I suspected you are calling parseData multiple times. A solution is to stop monitoring the location right in the delegate method.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
let searchURL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(location.coordinate.latitude),\(location.coordinate.longitude)&radius=50000&types=night_club&key=AIzaSyA2LQsGK_I1ETnKPGbjWgFW9onZlHog6dg"
// var cityInfo = NSString(format: "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=%f,%f&radius=50000&types=locality&key=AIzaSyA2LQsGK_I1ETnKPGbjWgFW9onZlHog6dg", (location?.coordinate.latitude)!,(location?.coordinate.longitude)!) as? String
manager.stopUpdatingLocation()
callAlamo(url: searchURL)
}
I edited the body of the method a bit to avoid all question and exclamation marks.
Side-note: Basically do not annotate types the compiler can infer.

cellForRowAtIndexPath being called very late

I am trying to fetch JSON data from OpenWeatherMap API depending on user location and display it in a tableview.
First, I just init my table view :
init(_ coder: NSCoder? = nil) {
self.tableView = UITableView()
self.locationManager = CLLocationManager()
}
Then in viewDidLoad I call a function launchLocationOperations() to get user's location :
func launchLocationOperations() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
}
In my delegate :
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.locationManager.stopUpdatingLocation()
let locationArray = locations as NSArray
let currentLocation = locationArray.lastObject as! CLLocation
if self.currentCoordinates == nil ||
(self.currentCoordinates?.latitude != currentLocation.coordinate.latitude
||
self.currentCoordinates?.longitude != currentLocation.coordinate.longitude) {
self.currentCoordinates = currentLocation.coordinate
self.hasLoadedCoordinates = true
self.fetchWeatherInformations()
}
}
Then calling fetchWeatherInformation() :
func fetchWeatherInformations() {
// I build my path
let urlPath = StringUtils.buildUrl(self.currentCoordinates)
guard let url = URL(string: urlPath) else { return }
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, _, error in
do {
let jsonResponse = try data?.toJSON() as? [String : Any]
self.openWeatherMapResponse = OpenWeatherMapResponse.convert(jsonResponse: jsonResponse)
self.displayUI()
} catch {
print("Error while fetching JSON response : \(error.localizedDescription)")
}
}.resume()
}
And in displayUI() :
func displayUI() {
tableView.delegate = self
tableView.dataSource = self
}
So I have two problems here :
First, didUpdateLocations is being called many times even if I ask to stopUpdatingLocation() when entering the function.
Second, cellForRowAtIndexPath is being called very, very late, like 5 seconds late. I don't know why it is happening because the other dataSource/delegate methods are called instantly after entering displayUI()...
Thanks for your help.

Resources