Updating screen fields when background task ends - swift

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)
}
}
}

Related

How to set a value as a condition in while loop using DispatchGroup?

I'm currently developing an application using SwiftUI.
I want to use a value created from a while loop using a DispatchGroup as a condition for while loop.
But my code doesn't work...
My goal is to get a date over a set total temperature from a start date.
How could I solve this problem?
UPDATED
For example:
If I call the method like below.
makeGetCallDateOverSetTemp(start_date:"2020-11-01", set_temp:100)
1'st loop -> totalTemp = 20 (a temperature of 2020-11-01 is 20℃)
2'nd loop -> totalTemp = 50 (a temperature of 2020-11-02 is 30℃)
3'rd loop -> totalTemp = 80 (a temperature of 2020-11-03 is 30℃)
4'th loop -> totalTemp = 105 (a temperature of 2020-11-04 is 25℃)
the while loop stops here and gets 2020-11-04 as the day over the set temperature.
AppState.swift
#Published var weatherInfos:[WeatherInfos]?
func makeGetCallDateOverSetTemp(start_date:String, set_temp:Int){
let start_date = self.dateFromString(string: start_date, format: "yyyy/MM/dd")
var addDays = 0
var totalTemp:Float = 0.0
let group = DispatchGroup()
// Set up the URL request
while Float(set_temp) < totalTemp {
let start_date = Calendar.current.date(byAdding: .day, value: addDays, to: start_date)
let url_start_date = self.stringFromDate(date: start_date!, format: "yyyy-MM-dd")
let endpoint: String = "https://sample.com/api/weather/?start_date=\(url_start_date)"
addDays += 1
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
continue
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxxx", forHTTPHeaderField: "authorization")
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
group.enter()
let task = session.dataTask(with: urlRequest) {(data, response, error) in
guard error == nil else {
print("error calling GET")
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// check for any errors
defer { group.leave()}
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{
self.weatherInfos = try JSONDecoder().decode([WeatherInfos].self, from: responseData)
for info in self.weatherInfos!{
totalTemp += info.temp
}
}catch{
print("Error: did not decode")
return
}
}
}
task.resume()
}
group.notify(queue: .main){
print(url_start_date)
}
}
func stringFromDate(date: Date, format: String) -> String {
let formatter: DateFormatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = format
return formatter.string(from: date)
}
func dateFromString(string: String, format: String) -> Date {
let formatter: DateFormatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = format
return formatter.date(from: string) ?? Date()
}
jsonModel.swift
struct WeatherInfos:Codable,Identifiable {
var id: Int
var temp: Float
}
A while loop whose condition relies on the result of an asynchronous task is impossible.
This is a stand-alone generic example with recursion to run in a Playground.
The static data is a struct, an array, a queue and a threshold value
struct Item {
let date : String
let values : [Int]
}
let items = [Item(date: "2020-02-01", values: [1, 3, 5]),
Item(date:"2020-02-02", values:[2, 4, 6]),
Item(date: "2020-02-03", values:[3, 5, 7])]
let queue = DispatchQueue(label: "Foo")
let threshold = 25
The variables are an index and the accumulated temperature
var temp = 0
var index = 0
The function getData calls itself passing the next item if the threshold is not reached yet. The asynchronous task is simulated with asyncAfter.
Finally the function notify is called.
func notify(date : String) {
DispatchQueue.main.async{ print(date, temp) }
}
func getData(date: String, values :[Int]) {
queue.asyncAfter(deadline: .now() + 1) {
for value in values {
temp += value
if temp >= threshold {
notify(date: date)
return
}
}
index += 1
if index < items.count {
let nextItem = items[index]
getData(date: nextItem.date, values: nextItem.values)
} else {
notify(date: "\(date) – temperature below threshold")
}
}
}
let firstItem = items[index]
getData(date: firstItem.date, values: firstItem.values)

Saving an object with date to user defaults - date won't be the same

I am saving and restoring a simple struct that contains a date. The reason for using this string format is that RESTful API is returning this date format. Meaning that it shouldn't be changed for this code example below.
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}
struct Person: Codable, Equatable {
let birthDate: Date
private enum CodingKeys: String, CodingKey {
case birthDate
}
}
func storePerson(_ person: Person) {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(DateFormatter.yyyyMMdd)
guard let encodedPerson = try? encoder.encode(person) else {
return
}
UserDefaults.standard.set(encodedPerson, forKey: "savedPerson")
}
func fetchPerson() -> Person {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd)
guard let savedPerson = UserDefaults.standard.data(forKey: "savedPerson"),
let person = try? decoder.decode(Person.self, from: savedPerson) else {
fatalError()
}
return person
}
let person = Person(birthDate: Date())
storePerson(person)
let storedPerson = fetchPerson()
print("\(person == storedPerson)") // this will be false
print("\(person.birthDate)")
print("\(storedPerson.birthDate)") // same date as above, but with no minutes
Why is the date not the same? The encoder and decoder are using the same dateEncodingStrategy

Group Items By Property in Swift 4

My data source is
{"events": [{"name":"event
foo","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test
"}, {"name":"event
bar","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test2"},
{"name":"event
foobar","date":"2018-07-21","time":"11:00","am_or_pm":"PM","day":"Saturday","description":"test3"}]}
I have tried dictionary/arrays, but not really getting close to my wanted result.
Pulling out data into an array:
var times = ["9:00","9:00","11:00"]
var names = ["event foo","event bar","event foobar"]
Desired output:
["9:00", "11:00"]
[["event foo", "event bar"], ["event foobar"]]
Any pointers to do this in Swift is appreciated. My end result is to hope to section a uitableview grouped by time.
If using Swift 4, you can use reduce(into:) and the default value for subscript operator:
guard
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: [Any]],
let events = json["events"] as? [[String: String]] else {
return
}
let results = events.reduce(into: [String: [String]]()) { result, value in
guard let time = value["time"], let name = value["name"] else { return }
result[time, default: []].append(name)
}
That results in a dictionary:
["11:00": ["event foobar"], "7:00": ["event foo", "event bar"]]
Or, as Vadian suggested, you can use Dictionary(grouping:,by:), but then you have to map it if you only want your name values, resulting in an array of tuples:
let results = Dictionary(grouping: events, by: { $0["time"]! })
.map { ($0.key, $0.value.map { $0["name"]! })}
[("11:00", ["event foobar"]), ("7:00", ["event foo", "event bar"])]
Personally, like Vadian suggested, I'd be inclined to combine date, time, and am_or_pm to build a full Date object and use one of the above patterns. E.g.:
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd h:mm a"
formatter.locale = Locale(identifier: "en_US_POSIX")
// if date/time values are in GMT, uncomment the following line:
//
// formatter.timeZone = TimeZone(secondsFromGMT: 0)
let results = events.reduce(into: [Date: [String]]()) { result, value in
guard
let timeString = value["time"],
let dateString = value["date"],
let amPm = value["am_or_pm"],
let date = formatter.date(from: dateString + " " + timeString + " " + amPm),
let name = value["name"] else { return }
result[date, default: []].append(name)
}
or
let results = Dictionary(grouping: events, by: { dictionary -> Date in
let string = dictionary["date"]! + " " + dictionary["time"]! + " " + dictionary["am_or_pm"]!
return formatter.date(from: string)!
})
.map { ($0.key, $0.value.map { $0["name"]! })}
Or, if the web service returned a single ISO 8601/RFC 3339 string representation of the date, time, and am/pm in the JSON, this could be simplified further.
I recommend to decode the JSON with Decodable and create a full date from the components. Then use Dictionary(grouping:by:) to group the array.
First create a DateFormatter (uncomment the time zone line if you need absolute UTC dates)
let dateFormatter : DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
// formatter.timeZone = TimeZone(secondsFromGMT: 0)!
formatter.dateFormat = "yyyy-MM-dd hh:mm a"
return formatter
}()
Create two structs for the root element and the events array. A custom initializer creates the Date instance
struct Root : Decodable {
let events : [Event]
}
struct Event : Decodable {
let date : Date
let name, description : String
private enum CodingKeys: String, CodingKey { case name, date, time, am_or_pm, description}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
description = try container.decode(String.self, forKey: .description)
let datePortion = try container.decode(String.self, forKey: .date)
let timePortion = try container.decode(String.self, forKey: .time)
let ampm = try container.decode(String.self, forKey: .am_or_pm)
let dateString = "\(datePortion) \(timePortion) \(ampm)"
guard let fullDate = dateFormatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(forKey: .date,
in: container,
debugDescription: "Date cannot be created")
}
date = fullDate
}
}
Decode the JSON and group the array
let jsonString = """
{"events": [{"name":"event foo","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test "}, {"name":"event bar","date":"2018-07-21","time":"7:00","am_or_pm":"PM","day":"Saturday","description":"test2"}, {"name":"event foobar","date":"2018-07-21","time":"11:00","am_or_pm":"PM","day":"Saturday","description":"test3"}]}
"""
do {
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Root.self, from: data)
let grouped = Dictionary(grouping: result.events, by: { $0.date})
print(grouped)
} catch {
print("error: ", error)
}

Swift - completion handler not waiting

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
}
}

Definition of Global Variable Resetting

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.