Why async / long running operations in BackgroundTasks don't work? - ios

Trying to use BackgroundTasks for iOS 13+. Long running operations don't seem to work:
// in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "foo.bar.name", using: nil) { task in
print("start!")
task.expirationHandler = {
// Not executed
print("expired!")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// Not executed
print("finish!")
task.setTaskCompleted(success: true)
}
}
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
BGTaskScheduler.shared.cancelAllTaskRequests()
let request = BGProcessingTaskRequest(identifier: "foo.bar.name")
request.earliestBeginDate = nil // or Date(timeIntervalSinceNow: 0) or Date(timeIntervalSinceNow: 5)...
do {
try BGTaskScheduler.shared.submit(request)
} catch let e {
print("Couldn't submit task: \(e)")
}
}
I also tried using a queue with Operation (for which I modeled my flow synchronously). This also didn't work. As soon as there's something that takes a while to complete, it gets stuck.
It doesn't log anything else to the console, no errors, no expired task message. It shows the last message before the long running operation and that's it. I confirmed that it doesn't move forward by storing a preference and examining it when restarting. It's not stored.
I added "foo.bar.name" to the info.plist (in "Permitted background task scheduler identifiers") and enabled capabilities both for background fetch and background processing. I'm testing on an iPhone with iOS 13.3.1 and using Xcode 11.4.1.
Additional notes:
I've been starting the tasks immediately as described here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
I also tested with Apple's demo project. It shows the same problem: The database cleaning operation doesn't complete (I added a log at the beginning of cleanDatabaseOperation.completionBlock and it never shows).

A couple of observations:
You should check the result code of register. And you should make sure you didn’t see your “Couldn't submit task” log statement.
Per that discussion in that link you shared, did you set your breakpoint immediately after the submit call? This accomplishes two things:
First, it makes sure you hit that line (as opposed, for example, to the SceneDelegate methods).
Second, if you just pause the app manually, some random amount of time after the app has gone into background, that’s too late. It has to be in that breakpoint immediately after you call submit. Then do e command. Then resume execution.
Anyway, when I do that, running your code, the BGProcessingTaskRequest ran fine. I’m running iOS 13.4.1 (and like you, Xcode 11.4.1).

Related

SwiftUI - background processing in a view

I have a music player that needs to fetch the current song from a function over and over again.
up til now I have put the function inside the view (SwiftUI) using the function .onAppear{} while technically it worked I found that when a user clicks on the view to make it bigger, or even when they close and reopen the app from background mode it is re-running the code as if it has not already started.
So I thought by adding a self.timer?.invalidate() function #State var timer: Timer?
I could potentially allow the script to run while it's open and then stop it when it disappears, that worked all the way up to when I needed to add
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification), perform: { output in
self.timer?.invalidate()
})
The issue now is when I reopen the app it won't restart - not sure why.
But I am starting to feel that there must be a better way to do this function, after all it's only checking a function that is in another file on loop.
So my question: where is the best place to create a code that does this
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
//CODE HERE
}
Clearly it needs to run once and in the background but not be called again unless the app is opened for the first time.
I was thinking I need to put it inside the AppDelegate file because I know that file is called only once or when certain functions need to run.
I was thinking this section
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//INSERT CODE HEER
}
I know that I would then need to just store the values to something like
UserDefaults.standard.set
and then just get the MediaPlayerView.swift to refresh these values.
Is this the best practice and what others would recommend?, And am I right by saying a view should only be reading information not trying to run functions.

How to resolve these CloudKit Background Task Warnings

I have a functioning iOS app that is producing some troubling warning messages and I'd like to resolve them.
From the console:
Background Task 37 ("CoreData: CloudKit Import"), was created over 30 seconds ago. In applications running in the background, this creates a risk of termination. Remember to call UIApplication.endBackgroundTask(_:) for your task in a timely manner to avoid this.
This warning is repeated periodically, however, each time with a different Task Id. I presume that if I can capture the Task Id, I could figure where to call
UIApplication.endBackgroundTask(_:)
I do not know if it's possible to obtain the Task Id. I do have a notification observer set to check for data changes. I'd prefer not to remove that.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
OperationQueue.main.addOperation({ () -> Void in
self.fetchProjects()
})
}

How to track the time a user listens an specific podcast

I have an iOS app that is about podcasts and I want to track how long a user listens every podcast. I have tried the basic - when a user plays I save the timestamp and when stops it sends an event with the timestamp difference but it obviously doens't work because there's many edge cases.
I have issues to know when a user has the app in background and stops listening at some point through the the system controls. Also when the user or the system kills the app without tapping on "pause" or "stop". I think these 2 cases are my main non-tracked cases so far.
Any idea how can I build a working solution? I don't want/can't pay an external service - I am merely relying on Firebase.
Thanks!
You can override applicationWillTerminate method in your app, and save a current user progress to UserDefaults.
As docs say, you have few seconds to do it:
This method lets your app know that it is about to be terminated and
purged from memory entirely. You should use this method to perform any
final clean-up tasks for your app, such as freeing shared resources,
saving user data, and invalidating timers. Your implementation of this
method has approximately five seconds to perform any tasks and return.
Your code can look like this:
var player: AVPlayer!
func applicationWillTerminate(_ application: UIApplication) {
UserDefaults.standard.setValue(player.currentTime().seconds, forKey: "curPlayerTime")
}
Then, on application launch, you can restore it:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let lastPlayerTime = UserDefaults.standard.value(forKey: "curPlayerTime") as? Double {
// update your player
}
return true
}

can't reference firebase database in applicationWillTerminate

I am trying to update my firebase database before the app being forced to close to no avail.
Settings:
Xcode Version: 8.2.1
ios simulator
Info.plist > Application does not run in background: Bool YES
Simulate:
Launch application
Close application via Hardware > Home on simulator
Code: AppDelegate.swift
this updates
func applicationDidEnterBackground(_ application: UIApplication) {
let user = FIRAuth.auth()!.currentUser!
FIRDatabase.database().reference().child("status").child(user.uid).setValue("foobar")
}
this does not update (print is functioning)
func applicationWillTerminate(_ application: UIApplication) {
print("applicationWillTerminate")
let user = FIRAuth.auth()!.currentUser!
FIRDatabase.database().reference().child("status").child(user.uid).setValue("foobar")
}
Here is screenshot of my db structure
sample
Since I have 5 seconds to execute an asynchronous task, I feel like it should work
Your implementation of this method has approximately five seconds to
perform any tasks and return. If the method does not return before
time expires, the system may kill the process altogether.
https://developer.apple.com/reference/uikit/uiapplicationdelegate/1623111-applicationwillterminate
any clues?

iOS: Why is fetch background function never fully executed?

My code in appdelegate for background fetch is never fully run. I have the background fetch option turned on and the plist updated.
I trigger the code by pressing Debug > Simulate Background Fetch
This is the code
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
User.getNotifications(User.getUserDetails()["id"].string!, callback: {(notifications) in
//update notification badge count
notificationBadgeCount = X
})
}
'User.getNotifications' looks like this
getNotifications(id: String, callback...){
alamofire.request(.GET....){ jsonResponse in
//GETS HERE
callback(jsonResponse)
}
}
When triggering the simulated background fetch, the alamofire GET request is sent and data is returned (I've checked the server and the call is sent), however, the app seems to suspend at (//GETS HERE) in the getNotifications call, so the rest the code in the background fetch (//update notification badge count) is never run.
The code seems to time out. I'm supposed to get 30s however it seems to time out in 5s or something.
Any idea why that section of code isn't executed?
NOTE: If I re-open the app manually, then the rest of the code executes.
performFetch has an incoming function called completionHandler. You must call that function to complete the fetch and stop the countdown clock. You are not doing that and you thus are timing out and the app is suspended.

Resources