I have a class designed to take the temperature data from an API for a specific date and add it to a dictionary. The URL for the API is stored in a global variable called baseURL. It is defined at the beginning as an empty string, but is later changed. My class is below:
import UIKit
import Foundation
typealias ServiceResponse = (JSON, NSError?) -> Void
class WeatherManager: NSObject {
var baseURL: String = ""
var data: String = ""
static let sharedInstance = WeatherManager()
func getRandomUser(onCompletion: (JSON) -> Void) {
println("Starting getRandomUser")
let route = self.baseURL
println(self.baseURL)
makeHTTPGetRequest(route, onCompletion: { json, err in
onCompletion(json as JSON)
})
}
func makeHTTPGetRequest(path: String, onCompletion: ServiceResponse) {
let request = NSMutableURLRequest(URL: NSURL(string: path)!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
let json:JSON = JSON(data: data)
onCompletion(json, error)
if error != nil {
println("No Error")
} else {
println("Error")
}
})
task.resume()
}
func addData() {
WeatherManager.sharedInstance.getRandomUser { json in
var jsonData = json["response"]["version"]
self.data = "\(jsonData)"
dispatch_async(dispatch_get_main_queue(),{
let alert = UIAlertView()
alert.title = "Weather Data Update"
if self.data != "null" {
println("Value:\(self.data)")
alert.message = "The weather data was updated successfully."
alert.addButtonWithTitle("OK")
alert.show()
} else {
println("Error Reading Data")
println(self.data)
alert.message = "HealthTrendFinder encountered an error while updating data."
alert.addButtonWithTitle("OK")
alert.show()
}
})
}
}
func updateWeatherHistory() {
println(self.baseURL)
let calendar: NSCalendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
println("Weather Updating...")
// This sets the start date to midnight of the current date if no start date has been set.
if StorageManager.getValue(StorageManager.StorageKeys.WeatherStartDate) == nil {
let startDate: NSDate = calendar.startOfDayForDate(NSDate())
StorageManager.setValue(startDate, forKey: StorageManager.StorageKeys.WeatherStartDate)
}
// This adds a data array if it hasn't been created yet.
if StorageManager.getValue(StorageManager.StorageKeys.WeatherData) == nil {
StorageManager.setValue([:], forKey: StorageManager.StorageKeys.WeatherData)
}
var weatherData: [NSDate: NSObject] = StorageManager.getValue(StorageManager.StorageKeys.WeatherData)! as! [NSDate : NSObject]
let startMidnight: NSDate = StorageManager.getValue(StorageManager.StorageKeys.WeatherStartDate) as! NSDate
let currentMidnight: NSDate = calendar.startOfDayForDate(NSDate())
let daysFromStartDate: Int = calendar.components(NSCalendarUnit.CalendarUnitDay, fromDate: startMidnight, toDate: currentMidnight, options: nil).day
println("Starting Loop")
for i: Int in 0..<daysFromStartDate {
let dateToBeExamined: NSDate = calendar.dateByAddingUnit(NSCalendarUnit.CalendarUnitDay, value: i, toDate: startMidnight, options: nil)!
if weatherData[dateToBeExamined] == nil {
let calendarUnits: NSCalendarUnit = .CalendarUnitDay | .CalendarUnitMonth | .CalendarUnitYear
let components = NSCalendar.currentCalendar().components(calendarUnits, fromDate: dateToBeExamined)
var month: String
var day: String
if components.month < 10 {
month = "0\(components.month)"
} else {
month = "\(components.month)"
}
if components.day < 10 {
day = "0\(components.day)"
} else {
day = "\(components.day)"
}
var dateString = "\(components.year)\(month)\(day)"
self.baseURL = "http://api.wunderground.com/api/91e65f0fbb35f122/history_\(dateString)/q/OR/Portland.json"
println(self.baseURL)
var get: () = WeatherManager.sharedInstance.addData()
println(get)
weatherData[dateToBeExamined] = self.data
// There is no data for the NSDate dateForInspection. You need to pull data and add it to the dictionary.
} else {
// Data exists for the specified date, so you don't need to do anything.
}
}
println("Loop has finished or been skipped")
}
}
The problem is, baseURL reverts to an empty string when getRandomUser is executed, after baseURL is set to the URL. Why is this happening, and how do I fix it?
Your code is unnecessarily complex, making it hard to diagnose the problem without more information. But here is a suggestion:
Try making it impossible to instantiate more than one instance of your WeatherManager singleton:
class WeatherManager {
private static let _sharedInstance = WeatherManager()
private init() { super.init() }
static func sharedInstance() -> WeatherManager {
return _sharedInstance
}
}
When you are working from outside WeatherManager, you access it by calling:
let wm = WeatherManager.sharedInstane()
Then, when you are working inside WeatherManager, make sure that all your references are to self - i.e., self.baseURL = ... or self.updateWeatherHistory(), instead of WeatherManager.sharedInstance.baseURL = ..., etc.
Though your code is complicated, I think what is going on is you actually have two instances of WeatherManager in play. You are setting the value of baseURL on one, but not the other. If you want it to be a singleton, you need to make it impossible to create more than one.
Related
I tried the below method on an AVAsset URL and all I am getting is nil. Is there anything wrong in my code?
func getYear(musicName: String) -> Date? {
let url = FileManager.customFolderURL.appendingPathComponent(musicName)
let playerItem = AVPlayerItem(url: url)
let metadataList = playerItem.asset.metadata
var date: Date?
for item in metadataList {
switch item.commonKey {
case .commonKeyCreationDate?:
date = item.dateValue
default: break
}
}
return date
}
func getYear(musicName: String) -> Date? {
let url = FileManager.customFolderURL.appendingPathComponent(musicName)
var date: Date?
let asset = AVAsset(url: url)
let metaData = asset.metadata
if let artist = metaData.first(where: {$0.commonKey == .commonKeyCreationDate}), let value = artist.dateValue {
print(value)
date = value
}
return date
}
This Code will work if your file contains value for property .commonKeyCreationData. I didn't have appropriate file for testing. But it was returning value for property .commonKeyArtist and if we would just replace artist.dateValue to artist.stringValue.
I'm trying to update data in my viewModel here is my viewModel;
import SwiftUI
import CoreLocation
final class LocationViewViewModel: ObservableObject {
static let previewWeather: Response = load("Weather.json")
let weatherManager = WeatherManager()
let locationManager = LocationManager.shared
#Published var weather: Response
init(weather: Response) { // Remove async
DispatchQueue.main.async { // Here, you enter in an async environment
let data = await fetchData() // Read the data and pass it to a constant
DispatchQueue.main.async { // Get on the main thread
self.weather = data // Here, change the state of you app
}
}
}
func fetchData() async -> Response {
guard let weather = try? await weatherManager.getWeather(latitude: weatherManager.latitude!, longitude: weatherManager.latitude!) else { fatalError("Network Error.") }
return weather
}
var city: String {
return locationManager.getCityName()
}
var date: String {
return dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(weather.current.dt)))
}
var weatherIcon: String {
if weather.current.weather.count > 0 {
return weather.current.weather[0].icon
}
return "sun.max"
}
var temperature: String {
return getTempFor(temp: weather.current.temp)
}
var condition: String {
if weather.current.weather.count > 0 {
return weather.current.weather[0].main
}
return ""
}
var windSpeed: String {
return String(format: "%0.1f", weather.current.wind_speed)
}
var humidity: String {
return String(format: "%d%%", weather.current.humidity)
}
var rainChances: String {
return String(format: "%0.0f%%", weather.current.dew_point)
}
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
var dayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "EEE"
return formatter
}()
var timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh a"
return formatter
}()
func getTimeFor(time: Int) -> String {
return timeFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(time)))
}
func getTempFor(temp: Double) -> String {
return String(format: "%0.1f", temp)
}
func getDayFor(day: Int) -> String {
return dayFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(day)))
}
}
Also i fetched that data for my previous view in my weather manager so im using the same function in my viewModel.
My weatherManager;
final class WeatherManager {
var longitude = LocationManager.shared.location?.coordinate.longitude
var latitude = LocationManager.shared.location?.coordinate.latitude
var units: String = "metric"
func getWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) async throws -> Response {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/onecall?lat=\(latitude)&lon=\(longitude)&units=\(units)&exclude=hourly,minutely&appid=\(API.API_KEY)") else { fatalError("Invalid Url.")}
let urlRequest = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { fatalError("Error while fetching data") }
let decodedData = try JSONDecoder().decode(Response.self, from: data)
return decodedData
}
}
But I stuck with compile errors about initializing my weather Also tried to make my weather model optional but in the end i get the fatal error which says Fatal error: Unexpectedly found nil while unwrapping an Optional value
What is the correct way of doing this if you are using fetched data in many views & viewModels
Your init() is trying to run asynchronously and it's updating a #Published property. Even if you manage to avoid compile errors, you cannot update a property that will change the state of your views (#Published) unless you are on the main thread.
What I propose:
#Published var weather = Response() // Initialise this property in some way, the dummy values will be used by the app until you complete fetching the data
init(weather: Response) { // Remove async
Task { // Here, you enter in an async environment
let data = await fetchData() // Read the data and pass it to a constant
DispatchQueue.main.async { // Get on the main thread
self.weather = data // Here, change the state of you app
}
}
}
I hope this works, but it would be better if after "But I stuck with compile errors..." you showed what kind of errors you find. I tried to use my best guess with the solution above.
We don't use view model objects in SwiftUI. Your object is doing unnecessary things that SwiftUI does for us automatically like formatting strings (so labels auto update automatically when region settings change) and managing asynchronous tasks (tasks are started when view appears and when ever data changes and also cancelled if data changes before previous request ends or the view disappears). Try re-architecting it to use SwiftUI data Views correctly, e.g.
struct WeatherView: View {
let location: Location
#State var weather: Weather?
var body: some View {
Form {
Text(weather.date, format: .dateTime) // new simpler formatting
Text(weather.date, formatter: dateFormatter) // label is auto updated when locale changes
Text(weather?.date == nil ? "No date" : "\(weather.date!, format: .dateTime)") // optional handling
}
.task(id: location) { newLocation // tasks auto cancelled and restarted when location changes
weather = await WeatherManager.shared.getWeather(location: newLocation)
}
}
So I am using a URL in the bolded text to parse JSON data retrieved remotely from that URL. My issue is that I want to parse data remotely AND asynchronously from TWO URLs not just one. The following code works great for 1 URL but I haven't the slightest idea how to do the same thing for 2. I am fairly new to Swift to any tips or pointers would be appreciated.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var customerNameLabel: UILabel!
#IBOutlet weak var cardNumberLabel: UILabel!
#IBOutlet weak var dateNTimeLabel: UILabel!
#IBOutlet weak var amountLabel: UILabel!
var customers = [Customer]()
var currentCustomerIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Retrieve JSON data from a remote server
let config = URLSessionConfiguration.default
// Create a session
let session = URLSession(configuration: config)
// Validate the URL to ensure that it is not a broken link
if let validURL = URL(string: "**THISISMYJSONURLHERE(removedforsecurity)**") {
//Create a task that will download whatever is found at validURL as a Data object
let task = session.dataTask(with: validURL, completionHandler: { (data, response, error) in
// If there is an error, we are going to bail out of this entire method (hence return)
if let error = error {
print("Data task failed with error: " + error.localizedDescription)
return
}
// If we get here that means we have received the info at the URL as a Data Object nd we can now ue it
print("Success")
//Check the response status
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let validData = data
else {print("JSON Object Creation Failed"); return}
do {
let jsonObj = try JSONSerialization.jsonObject(with: validData, options: .mutableContainers) as? [Any]
// Call our Parse method
self.ParseData(jsonObject: jsonObj)
self.displayData()
}
catch {
print(error.localizedDescription)
}
task.resume()
}
}
func ParseData(jsonObject: [Any]?) {
guard let json = jsonObject
else { print("Parse failed to unwrap the optional."); return }
for firstLevelItems in json {
guard let object = firstLevelItems as? [String: Any],
let fname = object["first_name"] as? String,
let lname = object["last_name"] as? String,
let fullName = fname + " " + lname as? String,
let customerNumber = object["customer_number"] as? Int,
let purchase = object["purchase"] as? [String: Any],
let time = purchase["time"] as? String,
let date = purchase["date"] as? String,
let amount = purchase["amount"] as? String
else { continue }
// See Note: Nested Functions
func addTransaction(_customer: Customer) {
if let cardNumber = purchase["card_number"] as? String? {
_customer.transactions.append(Transaction(firstName: fname, lastName: lname, time: time, date: date, amount: amount, cardNumber: cardNumber))
}
else {
_customer.transactions.append(Transaction(firstName: fname, lastName: lname, time: time, date: date, amount: amount))
}
}
let filteredCustomers = customers.filter({ (customer) -> Bool in
return customer.transactions[currentCustomerIndex].customerName == fullName
})
if filteredCustomers.count == 0 {
customers.append(Customer(customerNumber: customerNumber))
//Forced unwrapping here is ok because we know for a fact that customers wont be empty
addTransaction(_customer: customers.last!)
}
// If filtered array.count is 1 then that means we already have a customer object for this number
// In that case we just want to modify the existing customer object instead of creating a new one
else if filteredCustomers.count == 1 {
// filteredCustomer[0].customerNote = "This has been counted and Modified"
addTransaction(_customer: filteredCustomers[0])
}
else {
//See Note: Assertion
// Assertion Failure so that as we are building if this ever happens we know we have messed up
assertionFailure("No customers should exist twice in our customers array")
}
// print("Customer Number: \(customerNumber) has \(filteredCustomers.count) Orccurance in Customer's Array")
}
}
func displayData() {
DispatchQueue.main.async {
self.customerNameLabel.text = self.customers[self.currentCustomerIndex].customerName
self.cardNumberLabel.text = self.customers[self.currentCustomerIndex].cardNum
self.dateNTimeLabel.text = self.customers[self.currentCustomerIndex].dateNTime
self.amountLabel.text = "$" + self.customers[self.currentCustomerIndex].customerAmount.description
}
}
#IBAction func changeCustomer(_ sender: UIButton) {
currentCustomerIndex += sender.tag
if currentCustomerIndex < 0 {
currentCustomerIndex = customers.count - 1
}
else if currentCustomerIndex >= customers.count {
currentCustomerIndex = 0
}
displayData()
}
}
I am using some APIs to get data. These are initiated as session.dataTask and I am using classes to encapsulate the API calls, methods and returned properties for each different API. How should I configure my code so as to update the relevant screen labels and subViews when the API sessions have concluded and the data is available?
The relevant section of the AstronomicalTimes class init is:
init (date: Date, lat: Float, long: Float) {
let coreURL = "https://api.sunrise-sunset.org/json?"
let position = "lat=\(lat)&lng=\(long)"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = "&date=" + dateFormatter.string(from: date)
//let dateString = "&date=2020-06-21"
let urlString = coreURL + position + dateString + "&formatted=0"
let session = URLSession.shared
let url = URL(string: urlString)!
let request = URLRequest(url: url)
session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
if let error = error {
let nsError = error as NSError
print("Astronomical Times API call failed with error \(nsError.code)")
return
}
if let response = response as? HTTPURLResponse {
print("Astronomical Times API call response is \(response.statusCode)")
}
if let data = data {
do {
let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: data)
print("Astronomical times successfully parsed")
self.fillFields(astronomicalTimesResponse.results) //completes all class properties from parsed data
} catch {
print("Error while tide details parsing: \(error)")
}
}
}).resume()
A label is assigned the result of the API call in viewDidLoad() with:
currentAstronomicalTimes = AstronomicalTimes(date: savedDate, lat: currentSelection.station.lat, long: currentSelection.station.long)
lblAstDawn.text = currentAstronomicalTimes.strings.astronomicalTwilightBegin
Clearly this doesn't work as the screen is rendered with the labels and subViews blank before the API returns the data. I can't figure out how to signal the ViewController when the API has completed and then how to redraw the labels etc. I have tried updating the viewController fields in the API call closure expression but I can't update the UILabels from another class (and I think this approach is messy as the label update logic should really be in the ViewController)
Any help appreciated.
UPDATE after Rob's comments:
I have changed my class definition as advised and it successfully loads the data from the API. The class definition is as below, note I add a function which takes the loaded data and turns it into time strings and date() for ease of use in viewController (these all appear to be correctly populated after the API call)
import Foundation
enum AstronomicalTimesError: Error {
case invalidResponse(Data?, URLResponse?)
}
class AstronomicalTimes {
//structures for decoding daylight times
struct AstronomicalTimesResponse: Decodable {
public var results: AstronomicalTimes
public var status: String
}
struct AstronomicalTimes: Decodable {
var sunrise = String()
var sunset = String()
var solarNoon = String()
var dayLength = 0
var civilTwilightBegin = String()
var civilTwilightEnd = String()
var nauticalTwilightBegin = String()
var nauticalTwilightEnd = String()
var astronomicalTwilightBegin = String()
var astronomicalTwilightEnd = String()
private enum CodingKeys : String, CodingKey {
case sunrise = "sunrise"
case sunset = "sunset"
case solarNoon = "solar_noon"
case dayLength = "day_length"
case civilTwilightBegin = "civil_twilight_begin"
case civilTwilightEnd = "civil_twilight_end"
case nauticalTwilightBegin = "nautical_twilight_begin"
case nauticalTwilightEnd = "nautical_twilight_end"
case astronomicalTwilightBegin = "astronomical_twilight_begin"
case astronomicalTwilightEnd = "astronomical_twilight_end"
}
}
//used to hold string values to enter to label, i.e. time strings for labels
var strings = AstronomicalTimes()
//struct and variable used to hold specific date/times for gradient calculation
struct Times {
var sunrise = Date()
var sunset = Date()
var solarNoon = Date()
var dayLength = 0
var civilTwilightBegin = Date()
var civilTwilightEnd = Date()
var nauticalTwilightBegin = Date()
var nauticalTwilightEnd = Date()
var astronomicalTwilightBegin = Date()
var astronomicalTwilightEnd = Date()
}
var times = Times()
let date: Date
let latitude: Float
let longitude: Float
init (date: Date, latitude: Float, longitude: Float) {
self.date = date
self.latitude = latitude
self.longitude = longitude
}
func start(completion: #escaping (Result<AstronomicalTimesResponse, Error>) -> Void) {
var components = URLComponents(string: "https://api.sunrise-sunset.org/json")!
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // just in case your end user isn't using Gregorian calendar
dateFormatter.dateFormat = "yyyy-MM-dd"
components.queryItems = [
URLQueryItem(name: "lat", value: "\(latitude)"),
URLQueryItem(name: "lng", value: "\(longitude)"),
URLQueryItem(name: "date", value: dateFormatter.string(from: date)),
URLQueryItem(name: "formatted", value: "0")
]
let session = URLSession.shared
let url = components.url!
let request = URLRequest(url: url)
session.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(AstronomicalTimesError.invalidResponse(data, response)))
}
return
}
do {
print("Astronomical times api completed with status code ", httpResponse.statusCode)
let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: responseData)
DispatchQueue.main.async {
completion(.success(astronomicalTimesResponse))
self.fillFields(astronomicalTimesResponse.results)
}
} catch let jsonError {
DispatchQueue.main.async {
completion(.failure(jsonError))
}
}
}.resume()
}
func fillFields(_ input: AstronomicalTimes) -> Void {
//formats output fields into Date() or String (HH:mm) format
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" //Your date format
times.sunrise = dateFormatter.date(from: input.sunrise) ?? Date()
times.sunset = dateFormatter.date(from: input.sunset) ?? Date()
times.solarNoon = dateFormatter.date(from: input.solarNoon) ?? Date()
times.dayLength = input.dayLength
times.civilTwilightBegin = dateFormatter.date(from: input.civilTwilightBegin) ?? Date()
times.civilTwilightEnd = dateFormatter.date(from: input.civilTwilightEnd) ?? Date()
times.nauticalTwilightBegin = dateFormatter.date(from: input.nauticalTwilightBegin) ?? Date()
times.nauticalTwilightEnd = dateFormatter.date(from: input.nauticalTwilightEnd) ?? Date()
times.astronomicalTwilightBegin = dateFormatter.date(from: input.astronomicalTwilightBegin) ?? Date()
times.astronomicalTwilightEnd = dateFormatter.date(from: input.astronomicalTwilightEnd) ?? Date()
let timeFormatter = DateFormatter()
timeFormatter.dateFormat = "HH:mm"
strings.sunrise = timeFormatter.string(from: times.sunrise)
strings.sunset = timeFormatter.string(from: times.sunset)
strings.solarNoon = timeFormatter.string(from: times.solarNoon)
strings.dayLength = input.dayLength
strings.civilTwilightBegin = timeFormatter.string(from: times.civilTwilightBegin)
strings.civilTwilightEnd = timeFormatter.string(from: times.civilTwilightEnd)
strings.nauticalTwilightBegin = timeFormatter.string(from: times.nauticalTwilightBegin)
strings.nauticalTwilightEnd = timeFormatter.string(from: times.nauticalTwilightEnd)
strings.astronomicalTwilightBegin = timeFormatter.string(from: times.astronomicalTwilightBegin)
strings.astronomicalTwilightEnd = timeFormatter.string(from: times.astronomicalTwilightEnd)
}
}
I then call this from a function within viewController:
func getAstronomicalTimes(date: Date, latitude: Float, longitude: Float) -> Void {
let astronomicalTimes = AstronomicalTimes(date: date, latitude: latitude, longitude: longitude)
astronomicalTimes.start { result in
switch result {
case .success(let astronomicalTimesResponse):
print("astronomical times response ", astronomicalTimesResponse)
print("label", astronomicalTimes.strings.astronomicalTwilightBegin)
self.lblAstDawn.text = astronomicalTimes.strings.astronomicalTwilightBegin
case .failure(let error):
print(error)
}
}
}
This function is called within viewDidLoad():
getAstronomicalTimes(date: savedDate, latitude: currentSelection.station.lat, longitude: currentSelection.station.long)
However, the getAstronomicalTimes(date:latitude:longitude) does not update the lblAstDawn.text as I had hoped for.
Any clues as to where I am getting this wrong?
You need to supply a completion handler to your AstronomicalTimes request, so it can tell your view controller when the data has been retrieved, and the view controller can then update the various fields.
Thus:
enum AstronomicalTimesError: Error {
case invalidResponse(Data?, URLResponse?)
}
class AstronomicalTimes {
let date: Date
let latitude: Float // generally we use Double, but for your purposes, this might be adequate
let longitude: Float
init (date: Date, latitude: Float, longitude: Float) {
self.date = date
self.latitude = latitude
self.longitude = longitude
}
func start(completion: #escaping (Result<AstronomicalTimesResponse, Error>) -> Void) {
var components = URLComponents(string: "https://api.sunrise-sunset.org/json")!
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // just in case your end user isn't using Gregorian calendar
dateFormatter.dateFormat = "yyyy-MM-dd"
components.queryItems = [
URLQueryItem(name: "lat", value: "\(latitude)"),
URLQueryItem(name: "lng", value: "\(longitude)"),
URLQueryItem(name: "date", value: dateFormatter.string(from: date)),
URLQueryItem(name: "formatted", value: "0")
]
let session = URLSession.shared
let url = components.url!
let request = URLRequest(url: url)
session.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(AstronomicalTimesError.invalidResponse(data, response)))
}
return
}
do {
let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: responseData)
DispatchQueue.main.async {
completion(.success(astronomicalTimesResponse))
}
} catch let jsonError {
DispatchQueue.main.async {
completion(.failure(jsonError))
}
}
}.resume()
}
}
Then your viewDidLoad might do something like:
override viewDidLoad() {
super.viewDidLoad()
let astronomicalTimes = AstronomicalTimes(date: someDate, latitude: someLatitude, longitude: someLongitude)
astronomicalTimes.start { result in
switch result {
case .success(let astronomicalTimesResponse):
// populate your fields here
case .failure(let error):
print(error)
}
}
}
My problem is straighforward. I am fetching some data from site with TRON and I need to wait until data is successfully fetched than continue with execution. But my problem is that main thread is not waiting until completion block is completed.
I am doing this as Unit Test, so in setUp() function
Meteorites.Service.sharedInstance.fetchData(completion: { (meteorites, err) in
if err == nil {
self.meteorites = meteorites
}
})
than in test function I am using meteorites array, but I get EXC error (object is nil)
What I am doing wrong? Can someone explain me? Thanks
func fetchData(completion: #escaping ([Meteor]?, APIError<Service.JSONError>?) -> ()) {
print("fetching")
let request: APIRequest<Meteorites, JSONError> = tron.request("")
request.perform(withSuccess: { (meteorites) in
completion(meteorites.meteorites, nil)
}) { (err) in
completion(nil, err)
}
}
In case Meteorites class
class Meteorites: JSONDecodable {
var meteorites: [Meteor]
required init(json: JSON) throws {
print("fetched")
var meteorites = [Meteor]()
for meteorJson in json.array! {
let dateString = meteorJson["year"]
if dateString != JSON.null {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" //Your date format
dateFormatter.timeZone = TimeZone(abbreviation: "GMT+0:00")!
let date = dateFormatter.date(from: dateString.stringValue)
if let compareDate = dateFormatter.date(from: "2011-01-01T00:00:00.000") {
if date! >= compareDate {
let meteor = Meteor()
meteor.date = date!
meteor.name = meteorJson["name"].stringValue
meteor.fall = meteorJson["fall"].stringValue
meteor.id = meteorJson["id"].intValue
meteor.reclong = meteorJson["reclong"].doubleValue
meteor.reclat = meteorJson["reclat"].doubleValue
meteor.mass = meteorJson["mass"].intValue
meteor.nametype = meteorJson["nametype"].stringValue
meteor.recclass = meteorJson["recclass"].stringValue
meteor.lastUpdate = NSDate()
meteorites.append(meteor)
}
}
}
}
self.meteorites = meteorites
}
}