After reading several similar questions I don't find anything that fits into my problem.
I have an app that analyzes CSV files (some kind of big data analyzer app). So far CSV files were small and the analysis didn't take too long.
But now we need to support very large CSV files and the analysis takes 5-10 minutes.
If the user closes the app, this process is paused and when the users opens the app again the process is resumed. We need that the analysis continues while the app is in the background.
After reading about different possibilities of running in background I have this:
Use BGTaskScheduler
I'm not sure how to do it. I've followed this official tutorial https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app?language=objc and I've implemented this in my AppDelegate method:
didFinishLaunchingWithOptions {
...
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.mydomain.myapp.myproccess", using: nil) { task in
self.scheduleAppRefresh()
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
...
}
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.mydomain.myapp.myproccess")
request.earliestBeginDate = Date(timeIntervalSinceNow: 1)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh()
// Run my task, but how??
}
I don't know how to call the code inside one of my views (in fact one of my presenter cause I'm using VIPER) from appDelegate.
I'm not sure if this is how it works. If I'm not wrong, this BGTaskScheduler is to schedule a task when app goes to background.
Use beginBackgroundTask.
My code was this:
func csvAnalysis() {
DispatchQueue.global(qos: .userInitiated).async {
// Task to analyze CSV
}
}
and now is this:
func csvAnalysis() {
DispatchQueue.global(qos: .userInitiated).async {
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "mytask") {
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
// Task to analyze CSV
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
}
But this way, the process keeps pausing after entering background and resuming after opening the app again.
So, is it possible to execute this long task while the app is in background?
Related
I have been trying to schedule a BackgroundTask for my app that will refresh a database. So far I have been getting the same error when I try to launch the background task through Xcode
When I try scheduling the task with the following code:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:#"refreshEntries"]
I get the following error:
No task request with identifier <decode: missing data> has been
scheduled
Here's the code that I'm using
import BackgroundTasks
#main
struct ExpenseTrackerApp: SwiftUI.App {
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
ContentView()
}
// background task scheduler
.onChange(of: phase) { newPhase in
switch newPhase {
case .background: scheduleAppRefresh()
default: break
}
}
.backgroundTask(.appRefresh("refreshEntries")) {
await refreshRecurringEntries()
}
}
// Backgroud task
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "refreshEntries")
request.earliestBeginDate = .now.addingTimeInterval(24 * 3600)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule task \(error)")
}
print("Background task submitted") // breakpoint goes here
}
func refreshRecurringEntries() async {
// code that refreshes database
}
}
Some of the things I have already tried and did not work:
info.plist already has "Permitted background task scheduler identifiers" with key "refreshEntries"
I have already added Background modes capability to do background fetch and background processing
I have set the breakpoint right after the background request is made (see code above)
It seems like a lot of people have been posting this error already but so far no one has actually solved
With exactly the same setup as you describe, for me, it fails on the simulator
However, it works on a device.
When user launch the app or finish editing the data I need to update local notifications, basically it takes around 2-3 seconds in async way. I need to make sure that this code executes even if app leave foreground. What I have now:
func buildLocalNotifications()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
//recreate the notifications
}
}
And I can call this method from didFinishLaunchingWithOptions or when user save the form and everything works like a charm while app stays active for more then 3-4 seconds and its not blocking UI of course.. but if user lock the screen or terminate the app - this code won;t finished and notifications won't be created. How to safely execute sensitive code?
What is coming on my mind - show up a loader while performing this action - but it will block the user interaction
Ok I found the solution for the task which requires some time and should not be interrupted when app leaves foreground.
So we need beginBackgroundTask and endBackgroundTask
Small manager which you can use to execute code even when app is not in foreground
class BackgroundTaskManager {
let backgroundDQ = DispatchQueue.global(qos: .background)
var backgroundUpdateTask: UIBackgroundTaskIdentifier!
init(withName: String) {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(withName: withName) {}
}
/* Using completion handler to know when code is done*/
func runBackgroundTask(withCode: #escaping (_ cH: #escaping () -> Void) -> Void)
{
backgroundDQ.async {
withCode() {
self.endBackgroungTask()
}
}
}
func endBackgroungTask() {
if backgroundUpdateTask != nil && backgroundUpdateTask != UIBackgroundTaskInvalid {
UIApplication.shared.endBackgroundTask(backgroundUpdateTask)
backgroundUpdateTask = UIBackgroundTaskInvalid
}
}
}
And you can use it like
let taskManager = BackgroundTaskManager(withName: "LocalNotifications")
taskManager.doBackgroundTask() { (cH) in
//Your code goes here
//Send back completion handler so system knows when to finish background task
cH()
}
More information you can find on the Medium
If you want to make sure your code gets executed even if the user closes your app, you need to call your function in applicationWillTerminate. However, you only have ~5 seconds to execute code, before the system closes your app, so asynchronous execution is not encouraged here. It also doesn't matter if you execute code synchronously, since the user already quit your app, so you won't be blocking any UI updates.
Try to excute your code in background
DispatchQueue.global(qos: .background).async {
// your code here
}
I want to implement image sync in my ios app. for this what i have done is
take a photo
upload that photo to firebase
get the firebase stored url and send it to an api server so that server stores that info to a database
It works ok as long as the app is running, but when I exit out from the app pressing home button everything pauses.
So how can run a code that will not pause if app goes to home page?
As per this documentation,
For tasks that require more execution time to implement, you must
request specific permissions to run them in the background without
their being suspended. In iOS, only specific app types are allowed to
run in the background:
Apps that play audible content to the user while in the background,
such as a music player app
Apps that record audio content while in the
background Apps that keep users informed of their location at all
times, such as a navigation app
Apps that support Voice over Internet
Protocol (VoIP) Apps that need to download and process new content
regularly
Apps that receive regular updates from external accessories
Apps that implement these services must declare the services they
support and use system frameworks to implement the relevant aspects of
those services
Declaring the services lets the system know which
services you use, but in some cases it is the system frameworks that
actually prevent your application from being suspended.
Link: https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html
Here is how to implement (taken from this answer by Ashley Mills):
func doUpdate () {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = beginBackgroundUpdateTask()
var response: NSURLResponse?, error: NSError?, request: NSURLRequest?
let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
// Do something with the result
endBackgroundUpdateTask(taskID)
})
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
[Swift3] This is kind of worked for me
func doUpdate () {
DispatchQueue.global(qos: .background).async {
let taskID = self.beginBackgroundUpdateTask()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: {
print("printing after 10 min")
})
DispatchQueue.main.async {
self.endBackgroundUpdateTask(taskID: taskID)
}
}
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(expirationHandler: {})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}
Not sure what is the max time for this to complete since i see there is a expirationHandler
I am downloading an image from Firebase storage as follows:
let storage = FIRStorage.storage()
// Create a storage reference from our storage service
let storageRef = storage.reference(forURL: "MY_STORAGE_URL")
let imageRef = storageRef.child("Path_to_image")
// Download image in memory
let downloadTask = imageRef.data(withMaxSize: 1 * 1024 * 1024) {
(data, error) -> Void in
if (error != nil) {
//Handle the error
} else {
guard let imageData = data else {
print("Unable to unwrap image data.")
return
}
let downloadedImage = UIImage(data: imageData)
//Do some stuff with the image
}
}
I am also monitoring what happens with the download using the following observers:
// Observe changes in status
downloadTask.observe(.resume) { (snapshot) -> Void in
// Download resumed, also fires when the download starts
}
downloadTask.observe(.pause) { (snapshot) -> Void in
// Download paused
}
downloadTask.observe(.progress) { (snapshot) -> Void in
// Download reported progress
}
downloadTask.observe(.success) { (snapshot) -> Void in
// Download completed successfully
}
downloadTask.observe(.failure) { (snapshot) -> Void in
//Download failed
}
This all works just fine when the app is first started. However, I am getting problems if the app enters the background and I play around with some other applications (Facebook, Twitter, etc.), then bring the app back to the foreground. I also have problems if I leave the app open and running in the foreground for greater than or equal to 1 hour.
The problem is that the completion handler in let downloadTask = imageRef.data(withMaxSize: blah blah blah (in the first block of code above) is never called. If the completion handler is never called, I can never unwrap the data and attempt to use the image in my application.
Also, in the downloadTask observers, the only completion handlers that get fired are .resume and .progress. The .success or .failure events are never triggered. This seems to be a Firebase Storage bug to me, but I am not sure. Has anyone else encountered a similar issue? I don't understand why the code would work just fine from a fresh launch, but then after some time in the foreground or after some time in the background the image download stops working. Thanks in advance for any input you may have.
This is currently the expected behavior, unfortunately. Firebase Storage (at present) is foreground only: if the app is backgrounded, we haven't persisted the upload URL, and can't upload in the background nor restart it after it gets out of the background, so it probably is killed by the OS and the item isn't uploaded.
It's The Next Big Thing™ we'd like to tackle (our Android SDK makes it possible, though not easy), but unfortunately for now we haven't made more progress on this.
As a bit of a side note, your observers won't exist after the activity change--downloadTask is gone once the app is backgrounded, so when it comes back into the foreground, we basically need a method that retrieves all tasks that are currently backgrounded, and allows you to hook observers back up. Something like:
FIRStorage.storage().backgroundedTasks { (tasks) -> Void in
// tasks is an array of upload and download tasks
// not sure if it needs to be async
}
Is there really no way to run an UPLOAD task while an iOS app is in the background? This is ridiculous. Been looking at various stuff like NSURLSessionUploadTask, dispatch_after and even NSTimer, but nothing works for more than the meager 10 seconds the app lives after being put in the background.
How do other apps that have uploads work? Say, uploading an image to Facebook and putting the app in the background, will that cancel the upload?
Why cannot iOS have background services or agents like Android and Windows Phone has?
This is a critical feature of my app, and on the other platforms is works perfectly.
Any help is appreciated :(
You can continue uploads in the background with a “background session”. The basic process of creating a background URLSessionConfiguration with background(withIdentifier:) is outlined in Downloading Files in the Background. That document focuses on downloads, but the same basic process works for upload tasks, too.
Note:
you have to use the delegate-based URLSession;
you cannot use the completion handler renditions of the task factory methods with background sessions;
you also have to use uploadTask(with:fromFile:) method, not the Data rendition ... if you attempt to use uploadTask(with:from:), which uses Data for the payload, with background URLSession you will receive exception with a message that says, “Upload tasks from NSData are not supported in background sessions”; and
your app delegate must implement application(_:handleEventsForBackgroundURLSession:completionHandler:) and capture that completion handler which you can then call in your URLSessionDelegate method urlSessionDidFinishEvents(forBackgroundURLSession:) (or whenever you are done processing the response).
By the way, if you don't want to use background NSURLSession, but you want to continue running a finite-length task for more than a few seconds after the app leaves background, you can request more time with UIApplication method beginBackgroundTask. That will give you a little time (formerly 3 minutes, only 30 seconds in iOS 13 and later) complete any tasks you are working on even if the user leave the app.
See Extending Your App's Background Execution Time. Their code snippet is a bit out of date, but a contemporary rendition might look like:
func initiateBackgroundRequest(with data: Data) {
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
// Request the task assertion and save the ID.
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Network Tasks") {
// End the task if time expires.
if backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
// Send the data asynchronously.
performNetworkRequest(with: data) { result in
// End the task assertion.
if backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
}
Please don’t get lost in the details here. Focus on the basic pattern:
begin the background task;
supply a timeout clause that cleans up the background task if you happen to run out of time;
initiate whatever you need to continue even if the user leaves the app; and
in the completion handler of the network request, end the background task.
class ViewController: UIViewController, URLSessionTaskDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://0.0.0.0")!
let data = "Secret Message".data(using: .utf8)!
let tempDir = FileManager.default.temporaryDirectory
let localURL = tempDir.appendingPathComponent("throwaway")
try? data.write(to: localURL)
let request = URLRequest(url: url)
let config = URLSessionConfiguration.background(withIdentifier: "uniqueId")
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.uploadTask(with: request, fromFile: localURL)
task.resume()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("We're done here")
}