How do I schedule a repeating (daily) background task? [closed] - ios

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 13 days ago.
The community is reviewing whether to reopen this question as of 12 days ago.
Improve this question
I want to send the user a notification at the same time every day.
I know that I can schedule a repeating notification, but that won't work in my case because the notification content is dynamic.
So, I think I need to use background tasks. But there is no repeat parameter on the background task scheduler.
My solution is to try to use recursion to schedule the next bg task from within the bg task. On app launch, I'll check to make sure the recursion chain is still alive and update it if it has been broken for whatever reason.
Here is my current code. Besides one error with async code in the launch handler, am I on the right track? I still can't wrap my head around the need to register, request AND submit, but the code looks right to me, based on the Swift docs and online examples.
Also, I'm not sure what to do about the unaccepted awaits. The error is Cannot pass function of type '(BGTask) async -> Void' to parameter expecting synchronous function type. It wants synchronous code in the register handler.
import BackgroundTasks
import CoreData
// This is an attempt to maintain a daily repeating background task. You can't tell the system to repeat the task, like you can with notifications, so we use recursion.
// There is a chance that the recursion chain will break if something goes wrong, so we check for that at app launch and handle it.
// The problem is that some users may not launch the app often. But let's try it out and see if it becomes a problem.
func scheduleDailyBackgroundTask (moc: NSManagedObjectContext, isRecursiveCall: Bool) async -> Void {
// Check to see if there is already a bg task scheduled
let pendingTasks = await BGTaskScheduler.shared.pendingTaskRequests()
// If so, return early
if pendingTasks.count > 0 {
return
}
// Schedule the task
BGTaskScheduler.shared.register(forTaskWithIdentifier: "SCHEDULE_DAILY_NOTIFICATION", using: nil) { task in
let request = BGAppRefreshTaskRequest(identifier: "SCHEDULE_DAILY_NOTIFICATION")
// If we're starting the recursion chain, we don't want a delay. But if this has been called by a background task, aim for 20 hours
if isRecursiveCall {
request.earliestBeginDate = .now.addingTimeInterval(20 * 3600)
}
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule daily background task: \(error)")
}
task.expirationHandler = {
print("Daily background task expired.")
}
await scheduleDailyNotification(moc: moc)
await scheduleDailyBackgroundTask(moc: moc, isRecursiveCall: true)
task.setTaskCompleted(success: true)
}
}

Related

Need to call API's asynchronously and update the UI immediately instead of waiting for all the API to complete in Swift [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 7 months ago.
Improve this question
I have a PLP page, I need to hit stock API for all the products visible in screen, and Need to update the stock UI for each product once received the response for every product, I have implemented this using Dispatch Semaphore but faced crash when navigating between screens and I tried using Operation queue but in that I am getting the response at cumulative time of all the API hits. Even I tried with Dispatch group, it also responds same as Operation queue.
One API is taking around half second so each product stock should be updated in half to 1 second is what I need, Any help appreciated.
Tried code using Operation Queue
var queue: OperationQueue?
queue = OperationQueue()
queue?.qualityOfService = .background
productListView.sendCBRClosure = { [unowned self] cbrCodeArray in
queue?.maxConcurrentOperationCount = 5
for cbrCode in cbrCodeArray {
queue?.addOperation {
self.getStockDetail(cbrCode: cbrCode) // API call
}
}
}
Tried code using Dispatch Semaphore
var semaphore = DispatchSemaphore(value: 4)
productListView.sendCBRClosure = { [weak self] cbrCodeArray in
DispatchQueue.global().async { [weak self] in
for cbrCode in cbrCodeArray {
//API call
self?.getStockDetail(cbrCode: cbrCode) { qtyStr in
self?.semaphore.signal() // after API response received
}
self?.semaphore.wait()
}
Apple's networking calls using URLSession already run asynchronously on a background thread. There's no reason to need operation queues, DispatchQueue, etc.
Just submit a separate request for each item, and have the response handling code update the UI in a call to the main thread. (You can't do UIKit calls from a background thread.)
Each network operation will run separately and invoke it's completion handler once it's done.

beginBackgroundTask expirationHandler never called

I'm trying to complete a task in the background using UIApplication.shared.beginBackgroundTask but for some reason the expirationHandler is never called. The task I'm trying to complete is a video export from photo library but sometimes the export cannot be completed in time while the user is using the app in the foreground.
This is the code I'm using :
func applicationDidEnterBackground(_ application: UIApplication) {
if backgroundTask == .invalid && UploadQueue.instance.hasMoreWork() {
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "ExportQueue") {
NSLog("DriveLog - System is requesting end. Still more work to do ...")
self.endBackgroundTask()
}
print("Invalid? \(backgroundTask == .invalid)")
NSLog("DriveLog - Starting background task: %i", backgroundTask.rawValue)
}
}
func endBackgroundTask() {
NSLog("DriveLog - End called")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
I'm also calling :
(UIApplication.shared.delegate as! AppDelegate).endBackgroundTask()
during my task if I finish it earlier.
However I never see my expirationHandler being called in the log.
I have also tried putting beginBackgroundTask when starting the task in foreground but I get a warning message about task expiration while being in foreground.
You have not understood what the expiration handler is. It is called only if your time expires. Hence the name.
As soon as you call begin, start your task in the next line (not in the expiration handler). And when you are finished, call end.
You thus need to end the background task in two places: in the expiration handler, and outside it after actually performing your task.
It is very important to call end in both places, because if you fail to do so, the system will decide that you are a bad citizen and will never grant you any extra background time at all.
So, this is the diagram of the flow you need to construct:
Also note that this has nothing to do with UIBackgroundModes. That's a totally different mechanism.
matt's answer covers everything. I'm just going to try to give the same answer in different words because your edit suggests that matt's answer wasn't clear to you. (Read it again, though, it really does cover everything I'm going to say here, just in different words.)
You should not call beginBackgroundTask in applicationDidEnterBackground. You call it when you start whatever task you want time for. In your example that's going to be somewhere inside of UploadQueue. You don't call beginBackgroundTask when going into the background. You call it when you're starting a task that you would like to finish even if you go into the background.
Background tasks generally do not belong to the UIAppDelegate. They belong to the thing that creates the task (in your case: UploadQueue). You can create all the background tasks you want. They cost almost nothing. It's not just one "I want background" at the app level. Read matt's flow chart closely.
It's unclear from your question why you expect the expiration handler to be called. Do you expect your task to task to take so long that the OS forces you to stop it? That's what the expiration handler is for. If you've built your system correctly, it should rarely be called. Your task should end long before it's expired.
For full docs on how to do this, see Extending Your App's Background Execution Time. In particular note the caution:
Don’t wait until your app moves to the background to call the beginBackgroundTask(withName:expirationHandler:) method. Call the method before performing any long-running task.

GCD: URLSession download task

I have a requirement to download large number of files - previously only one file could be downloaded at a time. The current design is such that when the user downloads a single file, a URLSession task is created and the progress/completion/fail is recorded using the delegate methods for urlsession. My question is, how can I leave a dispatch group in this delegate method? I need to download 10 files at a time, start the next 10 when the previous ten finishes. Right now, if I leave the dispatch group in the delegate method, the dispatch group wait waits forever. Here's what I've implemented so far:
self.downloadAllDispatchQueue.async(execute: {
self.downloadAllDispatchGroup = DispatchGroup()
let maximumConcurrentDownloads: Int = 10
var concurrentDownloads = 0
for i in 0..<files.count
{
if self.cancelDownloadAll {
return
}
if concurrentDownloads >= maximumConcurrentDownloads{
self.downloadAllDispatchGroup.wait()
concurrentDownloads = 0
}
if let workVariantPart = libraryWorkVariantParts[i].workVariantPart {
concurrentDownloads += 1
self.downloadAllDispatchGroup.enter()
//call method for download
}
}
self.downloadAllDispatchGroup!.notify(queue: self.downloadAllDispatchQueue, execute: {
DispatchQueue.main.async {
}
})
})
In the delegates:
func downloadDidFinish(_ notification: Notification){
if let dispatchGroup = self.downloadAllDispatchGroup {
self.downloadAllDispatchQueue.async(execute: {
dispatchGroup.leave()
})
}
}
Is this even possible? If not, how can I achieve this?
If downloadAllDispatchQueue is a serial queue, the code in your question will deadlock. When you call wait, it blocks that current thread until it receives the leave call(s) from another thread. If you try to dispatch the leave to a serial queue that is already blocked with a wait call, it will deadlock.
The solution is to not dispatch the leave to the queue at all. There is no need for that. Just call it directly from the current thread:
func downloadDidFinish(_ notification: Notification) {
downloadAllDispatchGroup?.leave()
}
When downloading a large number of files, we often use a background session. See Downloading Files in the Background. We do this so downloads continue even after the user leaves the app.
When you start using background session, there is no need to introduce this “batches of ten” logic. The background session manages all of these requests for you. Layering on a “batches of ten” logic only introduces unnecessary complexities and inefficiencies.
Instead, we just instantiate a single background session and submit all of the requests, and let the background session manage the requests from there. It is simple, efficient, and offers the ability to continue downloads even after the user leaves the app. If you are downloading so many files that you feel like you need to manage them like this, it is just as likely that the end user will get tired of this process and may want to leave the app to do other things while the requests finish.

Swift iOS -DispatchWorkItem is still running even though it's getting Cancelled and Set to Nil

I use GCD's DispatchWorkItem to keep track of my data that's being sent to firebase.
The first thing I do is declare 2 class properties of type DispatchWorkItem and then when I'm ready to send the data to firebase I initialize them with values.
The first property is named errorTask. When initialized it cancels the firebaseTask and sets it to nil then prints "errorTask fired". It has a DispatchAsync Timer that will call it in 0.0000000001 seconds if the errorTask isn't cancelled before then.
The second property is named firebaseTask. When initialized it contains a function that sends the data to firebase. If the firebase callback is successful then errorTask is cancelled and set to nil and then a print statement "firebase callback was reached" prints. I also check to see if the firebaseTask was cancelled.
The problem is the code inside the errorTask always runs before the firebaseTask callback is reached. The errorTask code cancels the firebaseTask and sets it to nil but for some reason the firebaseTask still runs. I can't figure out why?
The print statements support the fact that the errorTask runs first because
"errorTask fired" always gets printed before "firebase callback was reached".
How come the firebaseTask isn't getting cancelled and set to nil even though the errorTask makes those things happen?
Inside my actual app what happens is if a user is sending some data to Firebase an activity indicator appears. Once the firebase callback is reached then the activity indicator is dismissed and an alert is shown to the user saying it was successful. However if the activity indicator doesn't have a timer on it and the callback is never reached then it will spin forever. The DispatchAsyc after has a timer set for 15 secs and if the callback isn't reached an error label would show. 9 out of 10 times it always works .
send data to FB
show activity indicator
callback reached so cancel errorTask, set it to nil, and dismiss activity indicator
show success alert.
But every once in while
it would take longer then 15 secs
firebaseTask is cancelled and set to nil, and the activity indicator would get dismissed
the error label would show
the success alert would still appear
The errorTask code block dismisses the actiInd, shows the errorLabel, and cancels the firebaseTask and sets it to nil. Once the firebaseTask is cancelled and set to nil I assumed everything inside of it would stop also because the callback was never reached. This may be the cause of my confusion. It seems as if even though the firebaseTask is cancelled and set to nil, someRef?.updateChildValues(... is somehow still running and I need to cancel that also.
My code:
var errorTask:DispatchWorkItem?
var firebaseTask:DispatchWorkItem?
#IBAction func buttonPush(_ sender: UIButton) {
// 1. initialize the errorTask to cancel the firebaseTask and set it to nil
errorTask = DispatchWorkItem{ [weak self] in
self?.firebaseTask?.cancel()
self?.firebaseTask = nil
print("errorTask fired")
// present alert that there is a problem
}
// 2. if the errorTask isn't cancelled in 0.0000000001 seconds then run the code inside of it
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0000000001, execute: self.errorTask!)
// 3. initialize the firebaseTask with the function to send the data to firebase
firebaseTask = DispatchWorkItem{ [weak self] in
// 4. Check to see the if firebaseTask was cancelled and if it wasn't then run the code
if self?.firebaseTask?.isCancelled != true{
self?.sendDataToFirebase()
}
// I also tried it WITHOUT using "if firebaseTask?.isCancelled... but the same thing happens
}
// 5. immediately perform the firebaseTask
firebaseTask?.perform()
}
func sendDataToFirebase(){
let someRef = Database.database().reference().child("someRef")
someRef?.updateChildValues(myDict(), withCompletionBlock: {
(error, ref) in
// 6. if the callback to firebase is successful then cancel the errorTask and set it to nil
self.errorTask?.cancel()
self.errorTask? = nil
print("firebase callback was reached")
})
}
This cancel routine is not doing what I suspect you think it is. When you cancel a DispatchWorkItem, it performs no preemptive cancellation. It certainly has no bearing on the updateChildValues call. All it does is perform a thread-safe setting of the isCancelled property, which if you were manually iterating through a loop, you could periodically check and exit prematurely if you see that the task was canceled.
As a result, the checking of isCancelled at the start of the task isn't terribly useful pattern, because if the task has not yet been created, there is nothing to cancel. Or if the task has been created and added to a queue, and canceled before the queue had a chance to start, it will obviously just be canceled but never started, you'll never get to your isCancelled test. And if the task has started, it's likely gotten past the isCancelled test before cancel was called.
Bottom line, attempts to time the cancel request so that they are received precisely after the task has started but before it has gotten to the isCancelled test is going to be an exercise in futility. You have a race that will be almost impossible to time perfectly. Besides, even if you did happen to time this perfectly, this merely demonstrates how ineffective this whole process is (only 1 in a million cancel requests will do what you intended).
Generally, if you had asynchronous task that you wanted to cancel, you'd wrap it in an asynchronous custom Operation subclass, and implement a cancel method that stops the underlying task. Operation queues simply offer more graceful patterns for canceling asynchronous tasks than dispatch queues do. But all of this presumes that the underlying asynchronous task offers a mechanism for canceling it and I don't know if Firebase even offers a meaningful mechanism to do that. I certainly haven't seen it contemplated in any of their examples. So all of this may be moot.
I'd suggest you step away from the specific code pattern in your question and describe what you are trying to accomplish. Let's not dwell on your particular attempted solution to your broader problem, but rather let's understand what the broader goal is, and then we can talk about how to tackle that.
As an aside, there are other technical issues in your example.
Specifically, I'm assuming you're running this on the main queue. So task.perform() runs it on the current queue immediately. But your DispatchQueue.main.asyncAfter(...) can only be run when whatever is running on the main queue is done. So, even though you specified a delay of 0.0000000001 seconds, it actually won't run until the main queue is available (namely, after your perform is done running on the main queue and you're well past the isCancelled test).
If you want to test this race between running the task and canceling the task, you need to perform the cancel on a different thread. For example, you could try:
weak var task: DispatchWorkItem?
let item = DispatchWorkItem {
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.00001) {
task?.cancel()
}
task = item
DispatchQueue.main.async {
item.perform()
}
Now you can play with various delays and see the different behavior between a delay of 0.1 seconds and one of 0.0000000001 seconds. And you'll want to make sure the app has reached quiescence before you try this test (e.g. do it on a button press event, not in viewDidLoad).
But again, this will merely illustrate the futility of the whole exercise. You're going to have a really hard time catching the task between the time it started and before it checked the isCancelled property. If you really want to manifest the cancel logic in some repeatable manner, we're going to have to artificially make this happen:
weak var task: DispatchWorkItem?
let queue = DispatchQueue(label: "com.domain.app.queue") // create a queue for our test, as we never want to block the main thread
let semaphore = DispatchSemaphore(value: 0)
let item = DispatchWorkItem {
// You'd never do this in a real app, but let's introduce a delay
// long enough to catch the `cancel` between the time the task started.
//
// You could sleep for some interval, or we can introduce a semphore
// to have it not proceed until we send a signal.
print("starting")
semaphore.wait() // wait for a signal before proceeding
// now let's test if it is cancelled or not
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
task?.cancel()
semaphore.signal()
}
task = item
queue.async {
item.perform()
}
Now, you'd never do this, but it just illustrates that isCancelled does work.
Frankly, you'd never use isCancelled like this. You would generally use the isCancelled process if doing some long process where you can periodically check the isCancelled status and exit if it is true. But that's not the case in your situation.
The conclusion of all of this is that checking isCancelled at the start of a task is unlikely to ever achieve what you had hoped for.

ios - How to implement background fetch correctly in ios swift

I have a situation where i am using background fetch to call my data sync process, As the sync function is a heavy task, it is executed in a background thread.
here is my code,
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
print("Background Fetch")
Utilities.syncCompleted = false // declared as :> static var syncCompleted:Bool = false
BackgroundSync().startSync() // heavy background task, and iam updating [Utilities.syncCompleted = true) on thread completion
while Utilities.syncCompleted == false {
NSThread.sleepForTimeInterval(1) // sleep for sometime
}
if Utilities.syncCompleted{
completionHandler(UIBackgroundFetchResult.NewData)
}else {
completionHandler(UIBackgroundFetchResult.NoData)
}
}
Now i have some questions :
As background fetch is of 30 sec, if my task is not completed in 30 sec then what happens, because i wont be able to set completionHandler to .NoData or .Failure
Is there is any default completionHandler value which is set (like .NoData) if developer does not specify in 30 sec.
Is there any other better way to do this.
Thanks in advance
Actually, if you are starting a backgroundTask, you are not limited to 30s, but to 10 minutes or whatever is the current limit for those.
Background fetch is a full-on starting of your app, including access to the main thread, UI changes etc. Essentially equivalent to starting the app minus the actual on-screen display. Background task are much more limited in what you can do but are allowed to take longer.
Thus in your case I would not care much about returning proper value of NoData or NewData. Start your sync as background task and call completionHandler(UIBackgroundFetchResult.NewData).
If you want to get as fair to the system as possible, you can check application.backgroundTimeRemaining and then schedule dispatch_after just before that expiring. So if you task finished before it, it will send NewData/NoData as received, but if it takes longer, then your dispatch_after block will send NewData and be done with it.

Resources