0

I am practice with DispatchQueue and struggle all day because the swift went too fast before API can finish write to array. What I notice two different codes below:

fetchingSunServer() // Start call the API to get sunrise and sunset time list
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
    print("\(sunrisesunsetString.count)\n\(sunrisesunset1970.count)\nFinished create data.")
})

// Will return the result:
// 4
// 4
// Finished create data.

This is fine, I was able to get information from that array if wait after 3 seconds.

But, I think this code above is unprofessional because sometime what if API is slow and will not be ready after 3 seconds so I am try make professional way. Here my codes:

In ViewController.swift

let fetchingSunriseSunset = DispatchGroup()
   fetchingSunriseSunset.enter()

   DispatchQueue.main.async {
      fetchingSunServer() // Start call the API to get sunrise and sunset time list
      fetchingSunriseSunset.leave()
   }
   fetchingSunriseSunset.notify(queue: .main) {
   print("\(sunrisesunsetString.count)\n\(sunrisesunset1970.count)\nFinished create data.")
}
// Will return the result:
// 0
// 0
// Finished create data.

Both array return zero because the API didn't finish, how can I make print wait until API finish insert sun time information into array?

UPDATED: Here code from fetchingSunServer()

        if let url = URL(string: "https://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400&formatted=0") {
            URLSession.shared.dataTask(with: url) { data, response, error in
                if let data = data {
                    do {
                        let res = try JSONDecoder().decode(Response.self, from: data)

                        let dateAPI = DateFormatter()
                        dateAPI.locale = Locale(identifier: "en_US_POSIX")
                        dateAPI.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
                        let ss_rise_ast = dateAPI.date(from: res.results.astronomical_twilight_begin)
                        let ss_rise_nau = dateAPI.date(from: res.results.nautical_twilight_begin)
                        let ss_rise_civ = dateAPI.date(from: res.results.civil_twilight_begin)
                        let ss_sunrise = dateAPI.date(from: res.results.sunrise)
                        let ss_solar = dateAPI.date(from: res.results.solar_noon)
                        let ss_sunset = dateAPI.date(from: res.results.sunset)
                        let ss_set_civ = dateAPI.date(from: res.results.civil_twilight_end)
                        let ss_sun_nau = dateAPI.date(from: res.results.nautical_twilight_end)
                        let ss_sun_ast = dateAPI.date(from: res.results.astronomical_twilight_end)

                        let dateToString = DateFormatter()
                        dateToString.dateFormat = "h:mm a"
                        let resultDate_ss_rise_ast = dateToString.string(from: ss_rise_ast!)
                        let resultDate_ss_rise_nau = dateToString.string(from: ss_rise_nau!)
                        let resultDate_ss_rise_civ = dateToString.string(from: ss_rise_civ!)
                        let resultDate_ss_sunrise = dateToString.string(from: ss_sunrise!)
                        let resultDate_ss_solar = dateToString.string(from: ss_solar!)
                        let resultDate_ss_sunset = dateToString.string(from: ss_sunset!)
                        let resultDate_ss_set_civ = dateToString.string(from: ss_set_civ!)
                        let resultDate_ss_sun_nau = dateToString.string(from: ss_sun_nau!)
                        let resultDate_ss_sun_ast = dateToString.string(from: ss_sun_ast!)

                        if res.status == "OK" {
                            self.sunrisesunsetString = [(
                            ss_rise_ast: resultDate_ss_rise_ast,
                            ss_rise_nau: resultDate_ss_rise_nau,
                            ss_rise_civ: resultDate_ss_rise_civ,
                            ss_sunrise: resultDate_ss_sunrise,
                            ss_solar: resultDate_ss_solar,
                            ss_sunset: resultDate_ss_sunset,
                            ss_set_civ: resultDate_ss_set_civ,
                            ss_sun_nau: resultDate_ss_sun_nau,
                            ss_sun_ast: resultDate_ss_sun_ast,
                            ss_timeday: res.results.day_length)]

                            self.sunrisesunset1970 = [(
                            ss_rise_ast: ss_rise_ast!.timeIntervalSince1970,
                            ss_rise_nau: ss_rise_nau!.timeIntervalSince1970,
                            ss_rise_civ: ss_rise_civ!.timeIntervalSince1970,
                            ss_sunrise: ss_sunrise!.timeIntervalSince1970,
                            ss_solar: ss_solar!.timeIntervalSince1970,
                            ss_sunset: ss_sunset!.timeIntervalSince1970,
                            ss_set_civ: ss_set_civ!.timeIntervalSince1970,
                            ss_sun_nau: ss_sun_nau!.timeIntervalSince1970,
                            ss_sun_ast: ss_sun_ast!.timeIntervalSince1970)]
                            self.fetchingSunriseSunset.leave()
                        } else {
                            print("Error received API from Sunrise and Sunset")
                            self.fetchingSunriseSunset.leave()
                        }
                    } catch let error {
                        print(error)
                        self.fetchingSunriseSunset.leave()
                    }
                }
            }.resume()
        }
    }

3 Answers3

3

You can change the implementation of "fetchingSunServer" method a bit like this:

func fetchingSunServer(completion: @escaping (() -> Void)) {
    //// Your code for fetching the data and after getting the data from server
    completion()
}

your implementation will be like :

self.fetchingSunServer {
    DispatchQueue.main.async {
         print("\(sunrisesunsetString.count)\n\(sunrisesunset1970.count)\nFinished create data.")
    }
}

you don't have to use DispatchGroup in this scenario then.

Amit
  • 4,837
  • 5
  • 31
  • 46
0

You're definitely on the right path, that waiting for a set number of seconds is not a good strategy. Your code should wait until the fetch is done, and then proceed, not until 3 seconds have passed, and then proceed.

In your second example, it seems like you might be a little confused around the indentation, and the ordering of what you're doing.

The way your code is written now, it essentially says:

  • Go off on a different thread, and fetch the data.
  • Whilst you are doing that, notify the main thread of the data you got.

Clearly this doesn't work, as one needs to happen before the other.

I imagine your problem is here:

DispatchQueue.main.async {
    fetchingSunServer() // Start call the API to get sunrise and sunset time list
    etchingSunriseSunset.leave()
} <----
fetchingSunriseSunset.notify(queue: .main) {
    print("\(sunrisesunsetString.count)\n\(sunrisesunset1970.count)\nFinished create data.")
}

Unlike usually, where the statement on the next line is processed after the statement on the first line. What you are really doing here is first:

  • Queue up the action to be done (Everything inside your async call)
  • Then print

The code is still happening in "order", but what is happening in order is not the stuff inside the async block, rather the creation of that block. And that can be a little confusing to wrap your head around.

I imagine what you actually want, is something closer to this:

DispatchQueue.main.async {
    fetchingSunServer() // Start call the API to get sunrise and sunset time list
    fetchingSunriseSunset.leave()
    fetchingSunriseSunset.notify(queue: .main) {
        print("\(sunrisesunsetString.count)\n\(sunrisesunset1970.count)\nFinished create data.")
    }
}

The difference here is small but important.

Rather than setting up the async call, then going to print, you now set up the async call, to go print when the fetchingSunServer() call is over.

Hanse00
  • 54
  • 4
-1

Better approach would be to use Result type in the closure. And as suspected you don't have a need to use DispatchGroup, because you're only making a single API call here. Modify your method like this:

Funciton fetchingSunServer Declaration:

func fetchingSunServer(completion: @escaping (Result<Response, Error>) -> Void) {
    if let url = URL(string: "https://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400&formatted=0") {
        URLSession.shared.dataTask(with: url) { data, response, error in
            DispatchQueue.main.async {
                if let data = data {
                    do {
                        completion(.success(try JSONDecoder().decode(Response.self, from: data)))
                    } catch {
                        completion(.failure(error))
                    }
                }
            }
        }.resume()
    }
}

Calling:

fetchingSunServer { (result) in
    switch result {
    case .success(let res):

        let dateAPI = DateFormatter()
        dateAPI.locale = Locale(identifier: "en_US_POSIX")
        dateAPI.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
        let ss_rise_ast = dateAPI.date(from: res.results.astronomical_twilight_begin)
        let ss_rise_nau = dateAPI.date(from: res.results.nautical_twilight_begin)
        let ss_rise_civ = dateAPI.date(from: res.results.civil_twilight_begin)
        let ss_sunrise = dateAPI.date(from: res.results.sunrise)
        let ss_solar = dateAPI.date(from: res.results.solar_noon)
        let ss_sunset = dateAPI.date(from: res.results.sunset)
        let ss_set_civ = dateAPI.date(from: res.results.civil_twilight_end)
        let ss_sun_nau = dateAPI.date(from: res.results.nautical_twilight_end)
        let ss_sun_ast = dateAPI.date(from: res.results.astronomical_twilight_end)

        let dateToString = DateFormatter()
        dateToString.dateFormat = "h:mm a"
        let resultDate_ss_rise_ast = dateToString.string(from: ss_rise_ast!)
        let resultDate_ss_rise_nau = dateToString.string(from: ss_rise_nau!)
        let resultDate_ss_rise_civ = dateToString.string(from: ss_rise_civ!)
        let resultDate_ss_sunrise = dateToString.string(from: ss_sunrise!)
        let resultDate_ss_solar = dateToString.string(from: ss_solar!)
        let resultDate_ss_sunset = dateToString.string(from: ss_sunset!)
        let resultDate_ss_set_civ = dateToString.string(from: ss_set_civ!)
        let resultDate_ss_sun_nau = dateToString.string(from: ss_sun_nau!)
        let resultDate_ss_sun_ast = dateToString.string(from: ss_sun_ast!)

        if res.status == "OK" {
            self.sunrisesunsetString = [(
                ss_rise_ast: resultDate_ss_rise_ast,
                ss_rise_nau: resultDate_ss_rise_nau,
                ss_rise_civ: resultDate_ss_rise_civ,
                ss_sunrise: resultDate_ss_sunrise,
                ss_solar: resultDate_ss_solar,
                ss_sunset: resultDate_ss_sunset,
                ss_set_civ: resultDate_ss_set_civ,
                ss_sun_nau: resultDate_ss_sun_nau,
                ss_sun_ast: resultDate_ss_sun_ast,
                ss_timeday: res.results.day_length)]

            self.sunrisesunset1970 = [(
                ss_rise_ast: ss_rise_ast!.timeIntervalSince1970,
                ss_rise_nau: ss_rise_nau!.timeIntervalSince1970,
                ss_rise_civ: ss_rise_civ!.timeIntervalSince1970,
                ss_sunrise: ss_sunrise!.timeIntervalSince1970,
                ss_solar: ss_solar!.timeIntervalSince1970,
                ss_sunset: ss_sunset!.timeIntervalSince1970,
                ss_set_civ: ss_set_civ!.timeIntervalSince1970,
                ss_sun_nau: ss_sun_nau!.timeIntervalSince1970,
                ss_sun_ast: ss_sun_ast!.timeIntervalSince1970)]
            print("\(sunrisesunsetString.count)\n\(sunrisesunset1970.count)\nFinished create data.")
        } else {
            print("Error received API from Sunrise and Sunset")
        }

    case .failure(let error):
        print(error)
    }
}
Frankenstein
  • 15,732
  • 4
  • 22
  • 47
  • Look good but only one missed...notice error said "Use of unresolved identifier 'res' and found out I have let res = try JSONDecoder().decode(Response.self, from: data) but your show completion(.success(try JSONDecoder().decode(Response.self, from: data))), where should I put "let res = ..."? –  Jun 19 '20 at 05:48
  • Ohhhhh never mind. Thanks!! –  Jun 19 '20 at 05:53
  • Missed that bit, try using the complete names instead of shortforms like "res" which make a good coding practice. – Frankenstein Jun 19 '20 at 05:53
  • I print result and both arrays still zero...maybe I put wrong place? Where should I put print or dispatch?? –  Jun 19 '20 at 06:01
  • 1
    Add the print statement where I've added it. Not outside the closure block. If you want you could make another closure function or better way is to make a model initializer. But posting it here would make it out of scope. – Frankenstein Jun 19 '20 at 06:05
  • THUMB UP, work perfectly. Thank you so much. –  Jun 19 '20 at 06:09