I have 3 Background Queue tasks running on app launch. They take approximately 25sec to complete. There is no memory issues, nor an issue with the tasks themselves, the tasks simply go through a database for filtering/reading data purposes (SQLite).
If I minimise the app whilst the tasks are running, the app is killed within 3sec, as any time I go back to the app after 3 sec the app starts again. I get the "Message from debugger: Terminated due to signal 9" as soon as I minimise the app. I use Swift and iOS14, is there a way to complete 30sec or less background SQLite DB tasks with the app not released from memory? Or just pause/kill the tasks to prevent killing the app?
I run my tasks using the DispatchQueue Extension:
extension DispatchQueue {
static func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
DispatchQueue.global(qos: .background).async {
background?()
if let completion = completion {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
completion()
})
}
}
}
static func backgroundFast(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
DispatchQueue.global(qos: .default).async {
background?()
if let completion = completion {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
completion()
})
}
}
}
}
If you only need 25 seconds for your backgound tasks to complete you should be able to call the beginBackgroundTask(withName:expirationHandler:) method in response to your app delegate's applicationDidEnterBackground(_:) method being called.
That will give you up to 3 minutes of background time.
See this article for Apple for more information.
https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/extending_your_app_s_background_execution_time
One thing to be aware of (Quoting Apple's docs)
Warning
If you are using scenes (see Scenes), UIKit will not call this
method. Use sceneDidEnterBackground(_:) instead to perform any final
tasks. UIKit posts a didEnterBackgroundNotification regardless of
whether your app uses scenes.
Related
I am trying to create a Robotic Process Automation tool for Macos using Swift. Users create an Automation that is an array of Step objects and then play it. One of the subclasses of Step is Pause which is supposed to pause the execution for a given number of seconds.
For some reason, execution hangs when I use the DispatchQueue.main.asyncAfter() method in the Pause class. Usually the first run through the automation is fine, but when it goes to repeat, it eventually hangs for much longer. The error goes away when I use sleep() instead.
The other weird thing about this bug is when I open Xcode to try and see what is happening, the hang resolves and execution continues. I am wondering if the process enters background somehow and then the DispatchQueue.main.asyncAfter() doesn't work. I have tried to change the Info.plist "Application does not run in background" to YES, but this doesn't have any effect.
The problem with using sleep() is it blocks the UI thread so users can't stop the automation if they need to. I have tried lots of different variations of threading with DispatchQueue, but it always seems to hang somewhere on repeat execution. I have also tried using a Timer.scheduledTimer() instead of DispatchQueue but that hangs as well. I'm sure I'm missing something simple, but I can't figure it out.
Creating the Step Array and Starting Automation
class AutomationPlayer {
static let shared = AutomationPlayer()
var automation: Automation?
var stepArray: [Step] = []
func play() {
// Create array of steps
guard let steps = automation?.steps, let array = Array(steps) as? [Step] else {
return
}
// If the automation repeats, add more steps to array.
for _ in 0..<(automation?.numberOfRepeats ?? 1) {
for (index, step) in array.enumerated() {
stepArray.append(step)
}
}
// Add small delay to allow window to close before execution.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
self?.execute(index: 0)
}
}
private func execute(index: Int) {
let step = stepArray[index]
executeStep(step: step) { [weak self] success, error in
guard error == nil else { return }
let newIndex = index + 1
if newIndex < self?.stepArray.count ?? 0 {
//Need a small delay between steps otherwise execution is getting messed up.
usleep(400000)
self?.execute(index: newIndex)
} else {
self?.stepArray = []
}
}
}
private func executeStep(step: Step?, completionHandler: #escaping (Bool, Error?) -> Void) -> Void {
step?.execute(completionHandler: { [weak self] success, error in
guard error == nil else {
completionHandler(false, error)
return
}
completionHandler(true, nil)
})
}
Pause Class
#objc(Pause)
public class Pause: Step {
override func execute(completionHandler: #escaping (Bool, Error?) -> Void) {
print("Pause for: \(self.time) seconds")
// This will eventually hang when the automation repeats itself
DispatchQueue.main.asyncAfter(deadline: .now() + Double(self.time)) {
completionHandler(true, nil)
})
// This will also hang
Timer.scheduledTimer(withTimeInterval: self.time, repeats: false) { timer in
completionHandler(true, nil)
}
// If I use this instead, the automation repeats just fine
sleep(UInt32(self.time))
completionHandler(true, nil)
}
}
So I think I figured it out. MacOS was putting my app into AppNap after a certain period of time which would cause the DispatchQueue.main.async() to stop working. For some reason, AppNap does not affect delays when you use sleep()
I found an answer here
This answer was a little older. I am using SwiftUI to build my mac app so I added this my #main struct
#main
struct Main_App: App {
#State var activity: NSObjectProtocol?
var body: some Scene {
WindowGroup("") {
MainWindow()
.onAppear {
activity = ProcessInfo().beginActivity(options: .userInitiated, reason: "Good Reason")
}
}
}
This seems to prevent the app from going into AppNap and the automation continues. It's pretty ugly, but it works.
I'm running a background upload task but I found it's blocking the main thread. After looking I suspect this happens because 3rd party library (Firebase in this case) must be scheduling its async callback on the main thread.
Is there a way to explicitly make the callback run on the global thread?
Here's how I start the task from the main thread:
DispatchQueue.global(qos: .background).async {
PhotoUploadOperation().start()
}
Here's an oversimplified version of the upload task:
class PhotoUploadOperation {
func uploadCameraRoll() {
for element in photos {
self.uploadPhoto(element.image, uid) { url in
// Some work
if let url = url {
let photo = Photo(uid: uid, url: url, creationDate: element.date)
self.sendPhoto(photo: photo) { success in
// Some work
}
}
}
}
}
}
Could you try this solution?
Firebase or other 3party framework use "method swizling" to accomplish some network log or other things. But your scenario is a little bit different.
extension DispatchQueue {
static func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
DispatchQueue.global(qos: .background).async {
background?()
if let completion = completion {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
completion()
})
}
}
}
}
Usage:
DispatchQueue.background(delay: 3.0, background: {
// do something in background
}, completion: {
// when background job finishes, wait 3 seconds and do something in main thread
})
DispatchQueue.background(background: {
// do something in background
}, completion:{
// when background job finished, do something in main thread
})
DispatchQueue.background(delay: 3.0, completion:{
// do something in main thread after 3 seconds
})
And you dont forget allow background processing from Signing and Capabilities
I'm developing an independent watch app on XCode 11.0 beta 5. Everything works alright except background refresh. I'm using the following code to schedule the background refresh task when I open the app:
let fireDate = Date(timeIntervalSinceNow: 60.0 * 30.0)
// optional, any SecureCoding compliant data can be passed here
let userInfo = ["reason" : "update UI"] as NSDictionary
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: fireDate, userInfo: userInfo) { (error) in
if (error == nil) {
print("successfully scheduled background task, use the crown to send the app to the background and wait for handle:BackgroundTasks to fire.")
}
}
The func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) is never called.
If I use the Debug->Simulate background fetch option from XCode the method gets called.
I have exactly the same problem, even if I don't run the app as a standalone. The problem only occurred with watchOS 6.
Does anyone have an idea what the solution is?
Here is my source code:
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
print("applicationDidFinishLaunching")
self.reloadActiveComplications()
scheduleNextReload()
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
self.reloadActiveComplications()
scheduleNextReload()
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
print("background")
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once you’re done.
scheduleNextReload()
self.reloadActiveComplications()
backgroundTask.setTaskCompletedWithSnapshot(true)
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once you’re done.
connectivityTask.setTaskCompletedWithSnapshot(true)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompletedWithSnapshot(true)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(true)
}
}
}
func reloadActiveComplications() {
let server = CLKComplicationServer.sharedInstance()
print("ExtensionDelegate: requesting reload of complications")
for complication in server.activeComplications ?? [] {
server.reloadTimeline(for: complication)
}
}
func scheduleNextReload() {
var targetDate:Date
let currentDate = Date()
let timezoneOffset = TimeZone.current.secondsFromGMT()
let epochDate = currentDate.timeIntervalSince1970
let timezoneEpochOffset = (epochDate + Double(timezoneOffset))
let timeZoneOffsetDate = Date(timeIntervalSince1970: timezoneEpochOffset)
targetDate = timeZoneOffsetDate.addingTimeInterval(120)
print("ExtensionDelegate: scheduling next update at %#", "\(timeZoneOffsetDate)")
print("ExtensionDelegate: scheduling next update at %#", "\(targetDate)")
WKExtension.shared().scheduleBackgroundRefresh(
withPreferredDate: targetDate,
userInfo: nil,
scheduledCompletion: { error in
// contrary to what the docs say, this is called when the task is scheduled, i.e. immediately
NSLog("ExtensionDelegate: background task %#",
error == nil ? "scheduled successfully" : "NOT scheduled: \(error!)")
}
)
}
}
Try scheduling it in applicationDidResignActive, rather than from a controller.
It only does it IF the application is in the background. It doesn't seem to think it needs to do it if the application isn't in the background.
You can make applicationDidResignActive fire by pressing the crown button.
During a computationally intensive task, I wish to show the user an activity indicator. What is the best way to do this?
My task (contrived of course), lasts a couple of seconds:
func startThinking(howMany: Int) {
for i in 0...howMany {
let p:Double = Double(i)
let _ = p / Double.pi
}
delegate?.finishedThinking()
}
This is called on a button tap:
#IBAction func startTap(_ sender: Any) {
Thinker.sharedInstance.startThinking(howMany: 500000000)
myActivity.startAnimating()
}
And stopped when the thinking task is finished:
func finishedThinking() {
print ("finished thinking")
myActivity.stopAnimating()
}
But the activity indicator is not showing up; the UI is blocked by the difficult thinking task.
I've tried putting the startAnimating on the main thread:
DispatchQueue.main.async {
self.myActivity.startAnimating()
}
or the difficult task onto its own thread:
DispatchQueue.global().async {
Thinker.sharedInstance.startThinking(howMany: 500000000)
}
and various other combinations that I've run across in Stack. What am I doing wrong?
Firstly, I would move the call to start animating to before the thinker call, and verify that it works if you don't start thinking. You also need to stop the animation from the main thread.
#IBAction func startTap(_ sender: Any) {
myActivity.startAnimating()
DispatchQueue.global(qos: .userInitiated).async {
Thinker.sharedInstance.startThinking(howMany: 500000000)
}
}
func finishedThinking() {
DispatchQueue.main.async {
myActivity.stopAnimating()
}
}
I adjusted a few things:
moved the .startAnimating() call to be first. It is already on the main thread since it was called from the interface
specify the qos as .userInitiated
run the .stopAnimating() on the main thread
I'm using AVAudioPlayer for playing records. Between each playback session I have interval from 0 to 10 sec. To make this interval I'm using AVAudioPlayerDelegate and when playing is finished I'm starting new playback after delay:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
guard let session = playbackSessionId,
let audioTrack = audioTrack,
let failureHandler = playingFailure,
let successHandler = playingSuccess else {
playingFinished(flag, error: nil)
return
}
print("audioPlayerDidFinishPlaying fired")
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + LoopInterval.currentInterval) { [weak self] in
print("asyncAfter fired")
guard let strongSelf = self,
let currentSession = strongSelf.playbackSessionId, session == currentSession else { return }
strongSelf.startPlayingRecordInLoop(audioTrack, success: successHandler, failure: failureHandler)
}
}
After app goes to the background (home button), audioPlayerDidFinishPlaying fires, but DispatchQueue.global(qos: .utility).asyncAfter not. So in console I see:
audioPlayerDidFinishPlaying fired
As soon as app become active, asyncAfter fires and I see next log message:
asyncAfter fired
When app is active, all works as expected.
Hope it'll help someone. I found problem: when app goes in background it stops background tasks, and fires them only after becomes active. To avoid need you should keep your app running in background and awaiting for your long-running background task.
backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
})
This method lets your app continue to run for a period of time after it transitions to the background. You should call this method at times where leaving a task unfinished might be detrimental to your app’s user experience. For example, your app could call this method to ensure that had enough time to transfer an important file to a remote server or at least attempt to make the transfer and note any errors. You should not use this method simply to keep your app running after it moves to the background.
After task finished you should call endBackgroundTask. If you won't end background task until backgroundTimeRemaining becomes 0, app will be terminated:
UIApplication.shared.endBackgroundTask(backgroundTaskID)
Each call to this method must be balanced by a matching call to the endBackgroundTask(:) method. Apps running background tasks have a finite amount of time in which to run them. (You can find out how much time is available using the backgroundTimeRemaining property.) If you do not call endBackgroundTask(:) for each task before time expires, the system kills the app. If you provide a block object in the handler parameter, the system calls your handler before time expires to give you a chance to end the task.
That's what I did in my case:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
guard let session = playbackSessionId,
let audioTrack = audioTrack,
let failureHandler = playingFailure,
let successHandler = playingSuccess else {
playingFinished(flag, error: nil)
return
}
backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
guard let taskId = self?.backgroundTaskID else { return }
UIApplication.shared.endBackgroundTask(taskId)
})
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + LoopInterval.currentInterval) { [weak self] in
guard let strongSelf = self,
let currentSession = strongSelf.playbackSessionId, session == currentSession else { return }
strongSelf.startPlayingRecordInLoop(audioTrack, success: successHandler, failure: failureHandler)
if let backgroundTaskID = strongSelf.backgroundTaskID {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
strongSelf.backgroundTaskID = nil
}
}
}