GCD `asyncAfter` does not start when app in background/inactive - swift

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
}
}
}

Related

DispatchQueue.main.asyncAfter hanging on repeat, but does not hang when using sleep

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.

Background Queue processes Terminates app when minimised

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.

Dispatch Source timer schedule timout

With the following Swift code, I'm trying to create a task that runs every hour:
let queue: DispatchQueue = .main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(3600), leeway: .milliseconds(100)
timer.setEventHandler { [weak self] in
// run code
}
Now, when I have the repeating set at a lower number, say 10 or event 150 seconds, it triggers as expected both in the foreground and background (or, rather, once the foreground hits it triggers, if the timer went off while in the background).
However, when I let the app timeout to the lock screen, and wait for an hour, it doesn't display.
Is there some timeout that Apple has for DispatchSource schedules? If so, what is it? And is there any way to change or get around it?
Edit
I don't want special functionality when it backgrounds, I want the code to keep running as normal and to trigger the event handler when the timeout happens, even if it's in the background
I ended up taking matt's suggestion and saving the time every time the code gets called, as seen below. Worked well!
let timeOfLastCheck = Date()
let queue: DispatchQueue = .main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(3600), leeway: .milliseconds(100)
timer.setEventHandler { [weak self] in
timeOfLastCheck = Date()
// run code
}
And elsewhere, where the timer is actually getting created:
let notificationCenter: NotificationCenter = .default
let activeNotificationToken = notificationCenter.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: nil
) { [weak self] _ in
let now = Date()
if let `self` = self,
let timeInterval = TimeInterval(dispatchTimeInterval: self.interval), // TimeInterval is extended elsewhere to be able to take in a DispatchTimeInterval in the init
now > timeOfLastCheck.addingTimeInterval(timeInterval) {
self.timeOfLastCheck = Date()
// run code
}
}

Calling stop() on AVAudioPlayerNode after finished playing causes crash

I have an AVAudioPlayerNode object which sends a callback once it is finished playing. That callback triggers many other functions in the app, one of which sends out a courtesy stop() message. For some reason, calling stop() at the moment the AVAudioPlayerNode finishes causes a crash. In the code here, I have abbreviated it so that the AVAudioPlayerNode just calls stop immediately to demonstrate the effect (rather than including my whole application). You can see clearly that it crashes. I don't understand why. Either a) the node is still playing and stop() stops it or b) it is done playing and stop can be ignored.
My guess is that this is some edge case where it is at the very end of the file buffer, and it is in an ambiguous state where has no more buffers remaining, yet is technically still playing. Perhaps calling stop() tries to flush the remaining buffers and they are empty?
func testAVAudioPlayerNode(){
let testBundle = Bundle(for: type(of: self))
let url = URL(fileURLWithPath: testBundle.path(forResource: "19_lyrics_1", ofType: "m4a")!)
let player = AVAudioPlayerNode()
let engine = AVAudioEngine()
let format = engine.mainMixerNode.outputFormat(forBus: 0)
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: format)
let delegate = FakePlayMonitorDelegate()
do {
let audioFile = try AVAudioFile(forReading: url)
let length = audioFile.length
player.scheduleFile(audioFile, at: nil, completionCallbackType: AVAudioPlayerNodeCompletionCallbackType.dataPlayedBack, completionHandler: {(completionType) in
print("playing = \(player.isPlaying)")
player.stop()
})
try engine.start()
let expectation = self.expectation(description: "playback")
delegate.expectation = expectation
player.play()
self.waitForExpectations(timeout: 6.0, handler: nil)
} catch {
}
}
In my case calling the .stop() method from the Main thread (you can use another) has resolved the issue.
DispatchQueue.main.async {
player.stop()
}
It can be the deadlock. I have noticed that the completionHandler is invoked from the background queue which is a serial queue, let's call it a "render queue".
Seems that the .stop() method is trying to do some work on a "render queue" but at the moment a "render queue" is busy with the completionHandler.

Background refresh task not triggered in watchos 6 independent app

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.