How to trigger a multi-step api transaction in the background? - ios

In case it matters, this app was a 4.2 app but is upgrading to 5.0 with new functionality, including this.
In response to a content-available APN, I need to combine local device data with remote data before triggering a message to a third party. In the foreground, this process works, but in the background, it appears to freeze until the app is in the foreground.
I thought to resolve this with a DispatchQueue -- and that is getting me a bit further , but it is still not going all the way through.
When I receive my APN, I ensure it looks right (its a content-avaialbe notification and has a category), then fire off loadPrediction:
// Tells the app that a remote notification arrived that indicates there is data to be fetched.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler:
#escaping (UIBackgroundFetchResult) -> Void) {
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
completionHandler(.failed)
return
}
if aps["content-available"] as? Int == 1 {
guard let category = aps["category"] as? String else {
print("on didReceiveRemoteNotification - did not receive payload with category")
print(String(describing: userInfo))
return
}
switch category {
case APNCATEGORIES.PREDICTION.rawValue:
DataModel.shared.loadPredictions() {
completionHandler(.newData)
}
break
default:
print("on didReceiveRemoteNotification - received unknown category '\(category)'")
completionHandler(.failed)
}
} else {
print("on didReceiveRemoteNotification - did not receive content-available in APN")
print(String(describing: aps))
completionHandler(.noData)
}
}
In loadPredictions, I request two pieces of data from the backend. edit: I've read that you might want to start a different queue for each POST request, so I've revised this next code block to its current form instead of just one queue:
/** load prediction data for notification scheduling */
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let queue = DispatchQueue(label: "loadingPredictions", qos: .utility, attributes: .concurrent)
queue.sync { [weak self] in
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
self?.getPredictionsDataFromFirestore() { [weak self] in
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
let queue = DispatchQueue(label: "getNotificationSpecificationFromFireStore_\(app.name)", qos: .utility, attributes: .concurrent)
queue.async { [weak self] in
print("getting Notification Specification from FireStore")
self?.getNotificationSpecificationFromFireStore(app: app) { [weak self] spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
return
}
guard let spec = spec else {
return
}
self?.postNotification(app: app, spec: spec)
}
}
}
// loadMergedForecasts($1)
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
}
callback()
}
}
}
They don't really need to be dependently related like that, but there's no point in doing the second one if the first fails.. If they both succeed, I should post a notification to my recipient in postNotification:
/** notify third party app of available notificatiions to schedule */
func postNotification (app: App, spec: NotificationSpecification) {
print("posting notification")
do {
let notify = Data(app.notify.utf8)
let localNotificationDetails = try JSONDecoder().decode(NotificationDetails.self, from: notify)
if spec.p8 != "custom" {
let token = localNotificationDetails.token
} else {
guard let bodyJSON = localNotificationDetails.body else {
return
}
guard let url = spec.custom_endpoint else { return }
guard let methodString = spec.custom_method?.uppercased() else { return }
guard let method = HTTPMethod(rawValue:methodString) else { return }
if ![.post, .put, .patch].contains(method) {
print("app has unsupported method '\(method)' -- \(String(describing: app))")
return
}
guard var headers = spec.custom_headers else { return }
if !headers.keys.map({ entry_key in entry_key.uppercased() }).contains("CONTENT-TYPE") {
headers["Content-Type"] = "application/json"
}
print("manually posting the notification with \(String(describing: bodyJSON))")
let queue = DispatchQueue(label: "manuallyPostNotifications", qos: .utility, attributes: .concurrent)
AF.request(url, method:method, parameters: bodyJSON).responseJSON(queue: queue) { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
}
} catch let e {
print("error posting notification to app \(app.id)\n\(e)")
}
}
NONE of these methods are on a View.
At first, there were zero cues and I dont know that I made it past the first loadPrediction. In its current state, the log looks like this when the app was in the background:
loading predictions
loading predictions - in async task, about to getPredictionsDataFromFirestore
getting Predictions Data from Firestore
for each app - about to getNotificationSpecificationFromFireStore
getting Notification Specification from FireStore
edit: that's one additional line, but it doesnt represent any improvement for the additional queues.
It will complete and succeed if I foreground it (and he whole thing takes 1-2 seconds when fully in the foreground). But I'd like to do all my work now.
Questions:
I'm doing queues wrong. How do I not exhaust the queue that I am in?
Can anyone confirm or deny that this will work when the app is closed? I can see that work is done when the app is closed, but I haven't since gone back to test if the api calls work because I cant get it to work just in the background.
addendum
revised code for current answer
/** load prediction data for notification scheduling */
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let queue = DispatchQueue(label: "loadingPredictions", qos: .default)
queue.sync { [weak self] in
let group = DispatchGroup()
group.enter()
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
self?.getPredictionsDataFromFirestore() { [weak self] in
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
group.enter()
print("getting Notification Specification from FireStore")
self?.getNotificationSpecificationFromFireStore(app: app) { [weak self] spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
group.leave()
return
}
guard let spec = spec else {
group.leave()
return
}
self?.postNotification(app: app, spec: spec) {
group.leave()
}
}
group.leave()
}
// loadMergedForecasts($1)
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
group.leave()
}
group.notify(queue: .main) {
callback()
print("I am being called too early?")
}
}
}
}
and (added a callback to the final method call):
/** notify third party app of available notificatiions to schedule */
func postNotification (app: App, spec: NotificationSpecification, _ callback: #escaping () -> Void ) {
print("posting notification")
do {
let notify = Data(app.notify.utf8)
let localNotificationDetails = try JSONDecoder().decode(NotificationDetails.self, from: notify)
if spec.p8 != "custom" {
let token = localNotificationDetails.token
callback()
} else {
guard let bodyJSON = localNotificationDetails.body else {
callback()
return
}
guard let url = spec.custom_endpoint else {
callback()
return
}
guard let methodString = spec.custom_method?.uppercased() else {
callback()
return
}
guard let method = HTTPMethod(rawValue:methodString) else {
callback()
return
}
if ![.post, .put, .patch].contains(method) {
print("app has unsupported method '\(method)' -- \(String(describing: app))")
callback()
return
}
guard var headers = spec.custom_headers else { return }
if !headers.keys.map({ entry_key in entry_key.uppercased() }).contains("CONTENT-TYPE") {
headers["Content-Type"] = "application/json"
}
print("manually posting the notification with \(String(describing: bodyJSON))")
let queue = DispatchQueue(label: "manuallyPostNotifications", qos: .utility, attributes: .concurrent)
AF.request(url, method:method, parameters: bodyJSON).responseJSON(queue: queue) { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
callback()
}
}
} catch let e {
print("error posting notification to app \(app.id)\n\(e)")
callback()
}
}
Realizing that my print statement wasn't inside the notify callback, I've revised it -- still not getting inside of the second firebase call.
loading predictions
loading predictions - in async task, about to getPredictionsDataFromFirestore
getting Predictions Data from Firestore
for each app - about to getNotificationSpecificationFromFireStore
getting Notification Specification from FireStore
I am being called too early?

You are firing off asynchronous tasks and your callback() will be executed before these tasks are complete. Since callback() eventually calls the completionHandler, your app will be suspended before all of its work is done.
You can use a dispatch group to delay the callBack() until everything is complete. The additional dispatch queues aren't necessary.
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let dispatchGroup = DispatchGroup()
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
dispatchGroup.enter()
self.getPredictionsDataFromFirestore() {
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
dispatchGroup.enter()
self.getNotificationSpecificationFromFireStore(app: app) { spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
dispatchGroup.leave()
return
}
guard let spec = spec else {
dispatchGroup.leave()
return
}
self.postNotification(app: app, spec: spec)
dispatchGroup.leave()
}
}
}
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
callback()
}
}

Related

Background thread - two network calls [duplicate]

This question already has answers here:
DispatchGroup logical workflow
(2 answers)
Closed 1 year ago.
I have two methods in the completeOnboarding method and both of them have network operation which should be done in the background thread as follows. However, I am wondering if I am doing why completion(true) gets called first, how could I able to handle that issue?
DispatchQueue.global(qos: .background).async {
self?.completeOnboarding( completion: { (success) in
DispatchQueue.main.async {
if success {
print("success")
} else {
print("failed")
}
}
})
func completeOnboarding(completion: #escaping(Bool) -> Void){
// has network post operation
classRegistration() {(success) in
if !success {
completion(false)
return
}
}
// has network post operation
classLocation() { (success) in
if !success {
completion(false)
return
}
}
completion(true)
}
The final completion(true) is not waiting for classLocation() and classRegistration() calls. If you have multiple network calls and you want to wait for all of them to finish you could (one approach) add them to a DispatchGroup and wait for that one to finish:
func dispatchAndWait(completion: #escaping () -> Void) {
func networkOne(completion: #escaping (_ success: Bool) -> Void) {
print("[DEBUG] Enter \(#function)")
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0...3)) {
completion(Bool.random())
}
print("[DEBUG] Return \(#function)")
}
func networkTwo(completion: #escaping (_ success: Bool) -> Void) {
print("[DEBUG] Enter \(#function)")
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0...3)) {
completion(Bool.random())
}
print("[DEBUG] Return \(#function)")
}
// Create a DispatchGroup and add both calls
let dispatchGroup = DispatchGroup()
// Enter first network call
dispatchGroup.enter()
networkOne { success in
print("[DEBUG] Complete networkOne with success: \(success)")
// Exit first network call
dispatchGroup.leave()
}
// Enter second network call
dispatchGroup.enter()
networkTwo { success in
print("[DEBUG] Complete networkTwo with success: \(success)")
// Exit second network call
dispatchGroup.leave()
}
// notify gets called when all tasks have exited
dispatchGroup.notify(queue: DispatchQueue.main) {
completion()
}
}
One more advise:
classRegistration() {(success) in
if !success {
completion(false)
return
}
}
will never complete in case of success==true, you should make sure that the completion is called on every path
classRegistration() {(success) in
// ... do whatever needs to be done here
completion(success)
}
Assuming classRegistration needs to succeed for classLocation to begin --
quick & dirty --
func completeOnboarding(completion: #escaping(Bool) -> Void){
// has network post operation
classRegistration() {(success) in
if success {
// has network post operation
classLocation() { (success) in
completion(success)
}
} else {
completion(false)
}
}
}
a proper way (others include - NSOperations with dependency, Dispatch group)
func completeOnboarding(completion: #escaping(Bool) -> Void){
let serialQueue = DispatchQueue(label: "classname.serial")
var proceedWithSuccess = true
serialQueue.async {
serialQueue.suspend() //run 1 operation at a time
classRegistration() {(success) in
proceedWithSuccess = success
serialQueue.resume() //let next operation run
}
}
serialQueue.async {
guard proceedWithSuccess else { return }
serialQueue.suspend()
classLocation() { (success) in
proceedWithSuccess = success
serialQueue.resume()
}
}
serialQueue.async {
completion(proceedWithSuccess)
}
}
If you want classLocation() to fire even if registration fails - just get rid of guard statement above.
If it were up to me I'd use a custom NSOperation subclass for Async operation & explicitly mention dependency between operations but it needs a ton of boilerplate code (perhaps something to look into later); serial queue (or dispatch group from the other answer) oughta be enough for you in this case though.

Cancel a URLSession based on user input in Swift

I've centralized API calls for my App in a class called APIService.
Calls look like the one below:
// GET: Attempts getconversations API call. Returns Array of Conversation objects or Error
func getConversations(searchString: String = "", completion: #escaping(Result<[Conversation], APIError>) -> Void) {
{...} //setting up URLRequest
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let _ = data
else {
print("ERROR: ", error ?? "unknown error")
completion(.failure(.responseError))
return
}
do {
{...} //define custom decoding strategy
}
let result = try decoder.decode(ResponseMultipleElements<[Conversation]>.self, from: data!)
completion(.success(result.detailresponse.element))
}catch {
completion(.failure(.decodingError))
}
}
dataTask.resume()
}
I'm executing API calls from anywhere in the Application like so:
func searchConversations(searchString: String) {
self.apiService.getConversations(searchString: searchString, completion: {result in
switch result {
case .success(let conversations):
DispatchQueue.main.async {
{...} // do stuff
}
case .failure(let error):
print("An error occured \(error.localizedDescription)")
}
})
}
What I would like to achieve now is to execute func searchConversations for each character tapped by the user when entering searchString.
This would be easy enough by just calling func searchConversations based on a UIPressesEvent being fired. Like so:
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
{...} // handle special cases
default:
super.pressesEnded(presses, with: event)
searchConversations(searchString: SearchText.text)
}
}
My problem is this now:
Whenever a new character is entered, I'd like to cancel the previous URLSession and kick-off a new one. How can I do that from inside the UIPressesEvent handler?
The basic idea is to make sure the API returns an object that can later be canceled, if needed, and then modifying the search routine to make sure to cancel any pending request, if any:
First, make your API call return the URLSessionTask object:
#discardableResult
func getConversations(searchString: String = "", completion: #escaping(Result<[Conversation], APIError>) -> Void) -> URLSessionTask {
...
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
...
}
dataTask.resume()
return dataTask
}
Have your search routine keep track of the last task, canceling it if needed:
private weak var previousTask: URLSessionTask?
func searchConversations(searchString: String) {
previousTask?.cancel()
previousTask = apiService.getConversations(searchString: searchString) { result in
...
}
}
We frequently add a tiny delay so that if the user is typing quickly we avoid lots of unnecessary network requests:
private weak var previousTask: URLSessionTask?
private weak var delayTimer: Timer?
func searchConversations(searchString: String) {
previousTask?.cancel()
delayTimer?.invalidate()
delayTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { [weak self] _ in
guard let self = self else { return }
self.previousTask = self.apiService.getConversations(searchString: searchString) {result in
...
}
}
}
The only other thing is that you probably want to change your network request error handler so that the “cancel” of a request isn’t handled like an error. From the URLSession perspective, cancelation is an error, but from our app’s perspective, cancelation is not an error condition, but rather an expected flow.
You can achieve this by using a timer,
1) Define a timer variable
var requestTimer: Timer?
2) Update searchConversations function
#objc func searchConversations() {
self.apiService.getConversations(searchString: SearchText.text, completion: {result in
switch result {
case .success(let conversations):
DispatchQueue.main.async {
{...} // do stuff
}
case .failure(let error):
print("An error occured \(error.localizedDescription)")
}
})
}
3) Update pressesEnded
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
{...} // handle special cases
default:
super.pressesEnded(presses, with: event)
self.requestTimer?.invalidate()
self.requestTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(searchConversations), userInfo: nil, repeats: false)
}
}

How do I tell when the table view can be reloaded so that it shows JSON data I've downloaded and parsed?

I have an app that downloads some information from a URL (ie. author name, story title, and the cover image). I'm able to download and parse the JSON from the server properly, but I'm stuck at one point.
The app consists of one View Controller (called ViewController.swift) and one class file (called GetStories.swift). Once the app has finished download and parsing the JSON from the server, I want the table view in the View Controller to reload itself (self.tableView.reloadData()).
I've set up a chain of completion blocks in GetStories.swift that accomplish the following steps in order:
1) Download the JSON
2) Parse the JSON
3) Save it to disk
func updateUI(){
saveDownloadedAndParsedJSONToDisk {
}
}
func saveDownloadedAndParsedJSONToDisk(completionHandler: #escaping RefreshTableView){
parseJSON {
self.saveDataToJSON()
completionHandler()
}
}
func parseJSON(completionHandler: #escaping ReadyToSave){
downloadJSON { jsonPayload, error in
do {
if let data = jsonPayload {
self.stories = try JSONDecoder().decode(Stories.self, from: data)
if let stories = self.stories {
self.stories = stories
completionHandler()
} else {
print("An error occurred while decoding JSON.")
}
} else if let error = error {
print("Error retrieving data: \(error)")
}
} catch {
print(error.localizedDescription)
}
}
}
func downloadJSON(completionHandler: #escaping NetworkResponse){
let storiesAPIURL = URL(string: "\(wattpadAPIURL)")
var wattpadAPIRequest = URLRequest(url: storiesAPIURL!)
wattpadAPIRequest.httpMethod = "GET"
let session = URLSession.shared
let dataTask = session.dataTask(with: wattpadAPIRequest) { (data : Data?, response : URLResponse?, error : Error?) in
if let data = data {
completionHandler(data, nil)
} else if let error = error {
completionHandler(nil, error)
print(error.localizedDescription)
}
}
dataTask.resume()
}
In ViewController.swift, I am calling updateUI(). Then, I'm calling the delegate method in the protocol:
self.storyResults?.delegate?.didFinishFetchingAndParsingData(finished: true)
The delegate method is doing this:
func didFinishFetchingAndParsingData(finished: Bool) {
guard finished else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
So, as you see, I'm using a 0.5 second delay to reload the table view because I'm not sure how to tell when exactly the downloading, parsing and saving has all fully finished.
If I don't use the delay of 0.5 seconds in the delegate method, the table view gets reloaded at an inappropriate time and there are no results displayed as a result. So, executing the reload this way doesn't work:
DispatchQueue.main.async {
self.tableView.reloadData()
}
What is the proper way to do this?
Thanks in advance!
Move the delegate call to the completionHandler inside updateUI, then it will be called at the right moment. Right now you have an empty completionHandler there.
func updateUI() {
saveDownloadedAndParsedJSONToDisk {
DispatchQueue.main.async {
self.storyResults?.delegate?.didFinishFetchingAndParsingData(finished: true)
}
}
}
...
func didFinishFetchingAndParsingData(finished: Bool) {
guard finished else {
return
}
self.tableView.reloadData()
SVProgressHUD.dismiss()
}

EXC_BAD_ACCESS (code=2) when using JSONEncoder.encode()

I have a (custom, linked-list based) queue that I want to deserialize when the app starts and serialize when the app stops, like so (AppDelegate.swift):
func applicationWillResignActive(_ application: UIApplication) {
RequestManager.shared.serializeAndPersistQueue()
}
func applicationDidBecomeActive(_ application: UIApplication) {
RequestManager.shared.deserializeStoredQueue()
}
The issue is during serialization when I exit the app. Here's the code that's running:
public func serializeAndPersistQueue() {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(queue) // Bad access here
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(at: url)
}
FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
}
catch {
print(error)
}
}
As you can see, fairly straightforward. It uses the JSONEncoder to convert my queue to a data object, then writes that data to the file at url.
However, during encoder.encode() I get EXC_BAD_ACCESS every time, without fail.
Additionally, I should note that peak and dequeue operations are conducted on the queue from a background thread. I'm not sure if that makes a difference due to my lack of understanding surrounding GCD. Here's what that method looks like:
private func processRequests() {
DispatchQueue.global(qos: .background).async { [unowned self] in
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 0)
while !self.queue.isEmpty {
group.enter()
let request = self.queue.peek()!
self.sendRequest(request: request, completion: { [weak self] in
_ = self?.queue.dequeue()
semaphore.signal()
group.leave()
})
semaphore.wait()
}
group.notify(queue: .global(), execute: { [weak self] in
print("Ending the group")
})
}
}
Lastly, I'll note that:
My queue conforms to the Codable protocol just fine––well, there are no compiler errors, at least. If its implementation beyond that matters, let me know and I'll show it.
The crash occurs a few seconds after I exit the app, while the execution of the processRequests function stops immediately after

How to manage download queue?

I am taking user input to download files from the server. The downloading task may include requesting web services.
I am expecting something like this:
1) Whenever the user selects a file to download or requesting for web
service, then it should be treated as one block of operation or task
and should go in the queue which will be managed globally at the app
level.
2) At the same time if the queue is empty then it should
automatically start executing the current task.
3) If queue contains
any operation then it should execute all old operation in
synchronously then execute the last one.
Can any one suggest how this can be done by the optimized way?
Take a look what I tried:
class func downloadChaptersFromDownloadQueue() {
let gbm = GlobalMethods()
for chapterDetail in gbm.downloadOpertationQueue.array.enumerated() {
if chapterDetail.element.chapterdata.state == .non || chapterDetail.element.chapterdata.state == .paused || chapterDetail.element.chapterdata.state == .downloading {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloading
let s = DispatchSemaphore(value: 0)
self.downloadActivty(courseId: chapterDetail.element.courseId, mod: chapterDetail.element.chapterdata, selectedIndexpath: chapterDetail.element.cellForIndexpath, success: { (result) in
if (result) {
if (WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .downloaded)) {
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloaded
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": 1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
else {
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
}
else {
_ = WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .non)
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
})
s.wait()
}
}
}
Create an async queue. First use a dispatch group to track how many requests are completed and get notified when all are finished (completely async).
Next, enqueue ALL your requests. Each request should have a unique identifier so you know which request completed or failed (the chapterId & pageNumber in your case should be enough).
Execute all the requests at once (again it is async) and you will be notified when each one completes (on the main queue through your completion block). The completion block should be called with all the requests responses and their unique identifiers.
Example:
class NetworkResponse {
let data: Data?
let response: URLResponse?
let error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
}
class NetworkQueue {
static let instance = NetworkQueue()
private let group = DispatchGroup()
private let lock = DispatchSemaphore(value: 0)
private var tasks = Array<URLSessionDataTask>()
private var responses = Dictionary<String, NetworkResponse>()
private init() {
}
public func enqueue(request: URLRequest, requestID: String) {
//Create a task for each request and add it to the queue (we do not execute it yet). Every request that is created, we enter our group.
self.group.enter();
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
//Only one thread can modify the array at any given time.
objc_sync_enter(self)
self.responses.updateValue(NetworkResponse(data: data, response: response, error: error), forKey: requestID)
objc_sync_exit(self)
//Once the request is complete, it needs to leave the group.
self.group.leave()
}
//Add each task to the queue.
self.tasks.append(task)
}
public func execute(completion: #escaping (_ responses: Dictionary<String, NetworkResponse>) -> Void) {
//Get notified on the main queue when every single request is completed (they all happen asynchronously, but we get one notification)
self.group.notify(queue: DispatchQueue.main) {
//Call our completion block with all the responses. Might be better to use a sorted dictionary or something here so that the responses are in order.. but for now, a Dictionary with unique identifiers will be fine.
completion(self.responses)
}
//Execute every task in the queue.
for task in self.tasks {
task.resume()
}
//Clear all executed tasks from the queue.
self.tasks.removeAll()
}
}
EDIT (Using your own code):
class func downloadChaptersFromDownloadQueue() {
let gbm = GlobalMethods()
let group = DispatchGroup()
let lock = NSLock()
//Get notified when ALL tasks have completed.
group.notify(queue: DispatchQueue.main) {
print("FINISHED ALL TASKS -- DO SOMETHING HERE")
}
//Initially enter to stall the completion
group.enter()
defer {
group.leave() //Exit the group to complete the enqueueing.
}
for chapterDetail in gbm.downloadOpertationQueue.array.enumerated() {
if chapterDetail.element.chapterdata.state == .non || chapterDetail.element.chapterdata.state == .paused || chapterDetail.element.chapterdata.state == .downloading {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloading
//Enter the group for each downloadOperation
group.enter()
self.downloadActivty(courseId: chapterDetail.element.courseId, mod: chapterDetail.element.chapterdata, selectedIndexpath: chapterDetail.element.cellForIndexpath, success: { (result) in
lock.lock()
defer {
group.leave() //Leave the group when each downloadOperation is completed.
}
if (result) {
if (WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .downloaded)) {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloaded
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": 1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
else {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
}
else {
_ = WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .non)
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
})
}
}
}
Again, this is asynchronous because you don't want the user waiting forever to download 100 pages..
For tasks like these the first thing you need to do is to do them asynchronously using dispatch_async, so that they are on a different thread and don't impact the performance of the application (or freeze it.)
Whenever your downloads succeeds/fails, you can always control what happens next in it's completion block. (I suggest you use recursion for what you're trying to achieve as it suits your needs).
Hope this helps!

Resources