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!
Related
I perform the next code
let task = session.uploadTask(with: request, from: requestData.body) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
All is clear, after doing network request in background I return completion to .main . But how to handle case, if I want to call completion not in .main but in thread in which session.uploadTask was initiated, because in my application it could be not only .main .
There is not a GCD mechanism to retrieve the current dispatch queue so that you can later dispatch to it. (A long time ago, there used to be a way to fetch the current queue, but it was deprecated back in iOS 7, and even then it was “Recommended for debugging and logging purposes only.”)
If you want to call the completion handlers on specific dispatch queue, I would suggest supplying an explicit DispatchQueue parameter to the method. Below, I have it default to .main, but the caller can override that with whatever it wants:
func perform(_ request: URLRequest, with data: Data, on queue: DispatchQueue = .main, completion: #escaping (Result<T, Error>) -> Void) {
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.async {
completion(.failure(error))
}
}
...
}
...
}
I know that this is not precisely what you are looking for, but it is an easy way to let the caller specify on which queue your closure will be called.
If you are using operation queues, you can get the current to determine the current operation queue. And should one do this, one would use addOperation on that operation queue in order to call the completion handler.
func perform(_ request: URLRequest, with data: Data, completion: #escaping (Result<T, Error>) -> Void) {
guard let queue = OperationQueue.current else {
fatalError("Must be called from operation queue")
}
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.addOperation {
completion(.failure(error))
}
}
...
}
...
}
But this pattern only works if the caller was using operation queue, not when only using dispatch queues. For this reason, I would still be inclined to adopt the pattern of supplying the target operation queue as a parameter:
func perform(_ request: URLRequest, with data: Data, on queue: OperationQueue = .main, completion: #escaping (Result<T, Error>) -> Void) {
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.addOperation {
completion(.failure(error))
}
}
...
}
...
}
If you would like to call completion on the current thread simply call completion().
current thread
completion(.failure(error))
main thread
DispatchQueue.main.async {
completion(.failure(error))
}
background thread
Dispatch.global(qos: .background) {
completion(.failure(error))
}
I used this solution:
guard let currentDispatch = OperationQueue.current?.underlyingQueue else {
return
}
let task = session.uploadTask(with: request, from: requestData.body) { data, response, error in
if let error = error {
currentDispatch.async {
completion(.failure(error))
}
}
}
Have not tested it too much, but quick debugging shows what I wanted to achieve,
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)
}
}
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()
}
}
So i am doing a network calls to retrieve image and using that i am show a kind of a video.
After a while i can see the memory lose and energy impact:
after a while my app crushed and i got :"Terminated due to memory issue error"
Before that i got that error from the image caller method:"error from dataResponse:The operation couldn’t be completed. No space left on device"
This are the two method i use:
class InstallationViewController: BaseViewController {
func imageCaller(url: String , success: #escaping (UIImage) -> Void, failure: #escaping () -> Void) {
let handler = AuthenticateHandler()
self.urlSession = URLSession(configuration: URLSessionConfiguration.default, delegate: handler, delegateQueue: OperationQueue.main)
self.imageThumbnailTask = urlSession?.dataTask(with: URL(string:url)!) { data, res, err in
if err != nil {
print("error from dataResponse:\(err?.localizedDescription ?? "Response Error")")
failure()
return
}
DispatchQueue.main.async {
if let imageData = data, let image = UIImage(data: imageData) {
success(image)
URLCache.shared.removeAllCachedResponses()
}
}
}
self.imageThumbnailTask?.resume()
}
func imageThumbnailcall() {
self.indicaotrTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.HandleOverTime), userInfo: nil, repeats: false)
self.imageCaller( url: self.isShowingThermal ? self.thermalUrl : self.visualUrl, success: { (image) in
self.indicaotrTimer?.invalidate()
DispatchQueue.main.async{
self.imageLoaderIndicator.stopAnimating()
self.backGroundImageView.image = image
}
if self.isInVC {
self.imageThumbnailcall()
}
}) {
self.imageLoaderIndicator.stopAnimating()
}
}
Worth mentioning that this line:
let handler = AuthenticateHandler()
self.urlSession = URLSession(configuration: URLSessionConfiguration.default, delegate: handler, delegateQueue: OperationQueue.main)
for digest protocol
Looks like you have a retain cycle in your closure in the imageThumbnailcall function.
The closure creates a strong reference to self, and since it's a recursive function you will run out of memory pretty quick.
You need to capture self as [weak self] or [unowned self] in the closure.
Example using [unowned self]:
func imageThumbnailcall() {
self.indicaotrTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.HandleOverTime), userInfo: nil, repeats: false)
self.imageCaller( url: self.isShowingThermal ? self.thermalUrl : self.visualUrl, success: { [unowned self] (image) in
self.indicaotrTimer?.invalidate()
DispatchQueue.main.async{
self.imageLoaderIndicator.stopAnimating()
self.backGroundImageView.image = image
}
if self.isInVC {
self.imageThumbnailcall()
}
}) {
self.imageLoaderIndicator.stopAnimating()
}
}
If you want to learn more about
Looks like you're recursively calling imageThumbnailcall. If you don't end the recursion at some point, you could see exactly the symptoms that you are reporting.
if self.isInVC {
self.imageThumbnailcall()
}
Are you making sure to set isInVC properly so you break out of the recursive loop?
I have an Operation subclass and Operation queue with maxConcurrentOperationCount = 1.
This performs my operations in a sequential order that i add them which is good but now i need to wait until all operations have finished before running another process.
i was trying to use notification group but as this is run in a for loop as soon as the operations have been added to the queue the notification group fires.. How do i wait for all operations to leave the queue before running another process?
for (index, _) in self.packArray.enumerated() {
myGroup.enter()
let myArrayOperation = ArrayOperation(collection: self.outerCollectionView, id: self.packArray[index].id, count: index)
myArrayOperation.name = self.packArray[index].id
downloadQueue.addOperation(myArrayOperation)
myGroup.leave()
}
myGroup.notify(queue: .main) {
// do stuff here
}
You can use operation dependencies to initiate some operation upon the completion of a series of other operations:
let queue = OperationQueue()
let completionOperation = BlockOperation {
// all done
}
for object in objects {
let operation = ...
completionOperation.addDependency(operation)
queue.addOperation(operation)
}
OperationQueue.main.addOperation(completionOperation) // or, if you don't need it on main queue, just `queue.addOperation(completionOperation)`
Or, in iOS 13 and later, you can use barriers:
let queue = OperationQueue()
for object in objects {
queue.addOperation(...)
}
queue.addBarrierBlock {
DispatchQueue.main.async {
// all done
}
}
A suitable solution is KVO
First before the loop add the observer (assuming queue is the OperationQueue instance)
queue.addObserver(self, forKeyPath:"operations", options:.new, context:nil)
Then implement
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as? OperationQueue == queue && keyPath == "operations" {
if queue.operations.isEmpty {
// Do something here when your queue has completed
self.queue.removeObserver(self, forKeyPath:"operations")
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
Edit:
In Swift 4 it's much easier
Declare a property:
var observation : NSKeyValueObservation?
and create the observer
observation = queue.observe(\.operationCount, options: [.new]) { [unowned self] (queue, change) in
if change.newValue! == 0 {
// Do something here when your queue has completed
self.observation = nil
}
}
Since iOS13 and macOS15 operationCount is deprecated. The replacement is to observe progress.completedUnitCount.
Another modern way is to use the KVO publisher of Combine
var cancellable: AnyCancellable?
cancellable = queue.publisher(for: \.progress.completedUnitCount)
.filter{$0 == queue.progress.totalUnitCount}
.sink() { _ in
print("queue finished")
self.cancellable = nil
}
I use the next solution:
private let queue = OperationQueue()
private func addOperations(_ operations: [Operation], completionHandler: #escaping () -> ()) {
DispatchQueue.global().async { [unowned self] in
self.queue.addOperations(operations, waitUntilFinished: true)
DispatchQueue.main.async(execute: completionHandler)
}
}
Set the maximum number of concurrent operations to 1
operationQueue.maxConcurrentOperationCount = 1
then each operation will be executed in order (as if each was dependent on the previous one) and your completion operation will execute at the end.
Code at the end of the queue
refer to this link
NSOperation and NSOperationQueue are great and useful Foundation framework tools for asynchronous tasks. One thing puzzled me though: How can I run code after all my queue operations finish? The simple answer is: use dependencies between operations in the queue (unique feature of NSOperation). It's just 5 lines of code solution.
NSOperation dependency trick
with Swift it is just easy to implement as this:
extension Array where Element: NSOperation {
/// Execute block after all operations from the array.
func onFinish(block: () -> Void) {
let doneOperation = NSBlockOperation(block: block)
self.forEach { [unowned doneOperation] in doneOperation.addDependency($0) }
NSOperationQueue().addOperation(doneOperation)
}}
My solution is similar to that of https://stackoverflow.com/a/42496559/452115, but I don't add the completionOperation in the main OperationQueue but into the queue itself. This works for me:
var a = [Int](repeating: 0, count: 10)
let queue = OperationQueue()
let completionOperation = BlockOperation {
print(a)
}
queue.maxConcurrentOperationCount = 2
for i in 0...9 {
let operation = BlockOperation {
a[i] = 1
}
completionOperation.addDependency(operation)
queue.addOperation(operation)
}
queue.addOperation(completionOperation)
print("Done 🎉")