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.
Related
i have no problem when using debug function in my ios device not simulator.
(ex, e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:#"TASK_IDENTIFIER"] )
but when do not using debug function, follow my code, it will be play the music after 60 seconds going to background. however nothing to happen in the device.
how do i test the device not using debug function?
import UIKit
import BackgroundTasks
import os.log
import AVFoundation
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
let bgTaskIdentifier = "com.hakjun.bgTest.playMusic"
var alarmTime : Int = 0
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
print("test bg")
}
return true
}
func scheduleAppRefresh(time : Double) {
let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: time)
do {
try BGTaskScheduler.shared.submit(request)
print("schedule app refresh")
} catch {
print("Could not schedule app refresh task \(error.localizedDescription)")
}
}
func handleAppRefresh(task : BGAppRefreshTask){
scheduleAppRefresh(time: 60)
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
let appRefreshOperation = BlockOperation {
Singleton.sharedInstance.play()
}
// queue.addOperation(appRefreshOperation)
task.expirationHandler = {
print("expire background")
queue.cancelAllOperations()
}
let lastOperation = queue.operations.last
lastOperation?.completionBlock = {
task.setTaskCompleted(success: !(lastOperation?.isCancelled ?? false))
}
print("background handle")
queue.addOperation(appRefreshOperation)
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("test bg os log2")
logger.log("App did enter background")
scheduleAppRefresh(time: 60)
}
}
class Singleton {
static let sharedInstance = Singleton()
private var player: AVAudioPlayer?
func play() {
let audioSession = AVAudioSession.sharedInstance()
guard let url = Bundle.main.url(forResource: "alarm2", withExtension: "mp3") else { return }
do {
try audioSession.setCategory(.playback, mode: .default, options: [])
} catch let error as NSError {
print("audioSession 설정 오류 : \(error.localizedDescription)")
}
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue)
guard let player = player else { return }
player.play()
} catch let error {
print(error.localizedDescription)
}
}
func stop() {
player?.stop()
}
}
FYI, BGAppResfreshTask is for “updating your app with small bits of information”, i.e., for performing a small network request to refresh your app so that, when the user next launches the app, you have more current information ready and waiting for them. But this app refresh is performed at a time chosen at the discretion of the OS, based upon many factors, but not earlier than earliestBeginDate.
Thus is not appropriate for an alarm clock because (a) you are not doing a network request to refresh your app; and (b) it is not guaranteed to run at the designated “earliest” date, only some time thereafter.
You might consider scheduling a user notification, instead.
You asked:
how do i test the device not using debug function?
You add logging statements. But rather than using print or NSLog, one would add Logger statements as discussed in WWDC 2020 Explore logging in Swift. (Or, if supporting iOS versions prior to iOS 14, use os_log; this was described in WWDC 2016 video Unified Logging and Activity Tracing, but that video is no longer available.) These Logger/os_log logging statements issued from an iOS app can be monitored from the macOS Console app.
So, once you have added your logging messages in your code in the relevant spots, using Logger (or os_log), you can then
install app on your device,
connect device to your computer,
launch the app directly from your device and
you can watch the log statements issued by your app in your macOS Console.
See points 3 and 4 in Swift: print() vs println() vs NSLog().
But note, you do not want to run the app from Xcode. You can install it by running it from Xcode, but then stop execution and re-launch the app directly on the device, not using Xcode. Unfortunately, being attached to the Xcode debugger keeps the app artificially running in the background when it would really be otherwise suspended when running independently on the device. So, when testing background execution on a physical device, do not debug it from Xcode directly, but rather add logging statements, launch the app directly from the device, and watch the logging statements in the macOS console.
Alternatively, sometimes background processes happen hours later, so I also will occasionally write log statements to a text file in the Application Support directory, and revisit that file later (by downloading the container back to my Mac later). In the case of background fetch and background tasks (which can happen hours or days later), this can be useful. In the case of an alarm app, though, the macOS Console approach outlined above is easiest.
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.
I'm trying to get my watchOS complication to automatically update every few minutes. In the ComplicationCOntroller.swift, I setup the complication in the getCurrentTimelineEntry() function, and it works the first time, but only updates the value displayed in the complication if I launch the WatchOS app. Can I set my complication to automatically update in the background? Even if it's a set time interval, e.g. "update in 3 minutes". Here's my ExtensionDelegate:
import WatchKit
import WatchConnectivity
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
let session = WCSession.default
let defaults = UserDefaults.standard
let server = CLKComplicationServer.sharedInstance()
override init() {
super.init()
WKInterfaceDevice.current().isBatteryMonitoringEnabled = true
session.delegate = self
session.activate()
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session started on \(WKInterfaceDevice.current().name)")
defaults.set("32", forKey: "PhoneBatteryLevel")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
print("Received application context: \(applicationContext["PhoneBatteryLevel"]!)")
defaults.set(applicationContext["PhoneBatteryLevel"], forKey: "PhoneBatteryLevel")
for complication in server.activeComplications! {
server.reloadTimeline(for: complication)
print("Reloading complications...")
}
}
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
}
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.
}
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.
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.
backgroundTask.setTaskCompletedWithSnapshot(false)
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(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompletedWithSnapshot(false)
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
// Be sure to complete the relevant-shortcut task once you're done.
relevantShortcutTask.setTaskCompletedWithSnapshot(false)
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
// Be sure to complete the intent-did-run task once you're done.
intentDidRunTask.setTaskCompletedWithSnapshot(false)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
}
And the relevant ComplicationController code:
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTimelineEntry?) -> Void) {
switch complication.family {
case .modularSmall:
let template = CLKComplicationTemplateModularSmallSimpleText()
template.textProvider = CLKSimpleTextProvider(text: "\(defaults.integer(forKey: "PhoneBatteryLevel"))")
template.tintColor = UIColor.yellow
let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
handler(entry)
case .modularLarge:
let template = CLKComplicationTemplateModularLargeStandardBody()
template.headerTextProvider = CLKSimpleTextProvider(text: "Battery Levels")
template.headerTextProvider.tintColor = UIColor.green
template.body1TextProvider = CLKSimpleTextProvider(text: "iPhone: \(defaults.integer(forKey: "PhoneBatteryLevel"))%")
template.body2TextProvider = CLKSimpleTextProvider(text: "Watch: \(getWatchBatteryLevel())%")
let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
handler(entry)
default:
return
}
}
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
}
}
}
We're using a UIAlertController as a loading indicator while a network request occurs. There are no actions associated with this UIAlertController as it is closed automatically when the network activity is completed. We display this alert after the user taps the login button for our app.
When we run our tests, they fail at the point right after this with:
UI Testing Failure - Did not receive view did disappear notification within 2.0s
Per other answers on SO, I've tried to use addUIInterruptionMonitor to handle the alert, but without any success. I think this is because there are no actionable buttons on the UIAlertController. Since there's no action that can be taken on the alert, the interruption monitor just looks like this:
addUIInterruptionMonitor(withDescription: "Loading") { handler in
return true
}
Even with this though, I get the same error. How can I work around this?
EDIT: Relevant UI testing code below:
class UI_Tests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = true
XCUIApplication().launch()
}
func testLogin() {
let app = XCUIApplication()
let tablesQuery = app.tables
let secureTextField = tablesQuery.cells.containing(.staticText, identifier:"PIN").children(matching: .secureTextField).element
secureTextField.tap()
secureTextField.typeText("1234")
app.buttons["Login"].tap()
addUIInterruptionMonitor(withDescription: "Loading") { handler in
return true
}
// Test failure occurs here.
let searchField = tablesQuery.searchFields.element(boundBy: 0)
searchField.tap()
searchField.typeText("hey")
}
}
After reaching out to Apple DTS, it turns out that when displaying a UI interruption / UIAlertController that dismisses based on a timeout, that you need to combine the UI interruption monitor with a timeout-based expectation (otherwise, the interruption monitor will return before the alert has dismissed!).
Using the UI testing example in the question, this approach looks like this:
class UI_Tests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = true
XCUIApplication().launch()
}
func testLogin() {
let app = XCUIApplication()
let tablesQuery = app.tables
let secureTextField = tablesQuery.cells.containing(.staticText, identifier:"PIN").children(matching: .secureTextField).element
secureTextField.tap()
secureTextField.typeText("1234")
app.buttons["Login"].tap()
addUIInterruptionMonitor(withDescription: "Loading") { alert in
self.expectation(for: NSPredicate(format: "exists == false"), evaluatedWith: alert, handler: nil);
self.waitForExpectations(timeout: 10, handler: nil)
return true
}
// Test failure occurs here.
let searchField = tablesQuery.searchFields.element(boundBy: 0)
searchField.tap()
searchField.typeText("hey")
}
}
This expectation will wait for 10 seconds to be filled. If the alert doesn't dismiss after 10 seconds, the expectation won't be met and the test will fail, but if it does, the test will succeed.