If my app has been in background for more than 5 minutes, I want to perform the navigation to lock screen view controller. Here is my code. But sometimes it works, as it should, and sometimes it does not work. How can it be fixed?
private var lockTimer: Timer?
func applicationDidEnterBackground(_ application: UIApplication) {
lockTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: false) { _ in
// Navigation code
}
}
func applicationWillEnterForeground(_ application: UIApplication) {
lockTimer?.invalidate()
lockTimer = nil
}
Background tasks are not guaranteed to run on iOS for very long. There are ways to improve your chances of completing your background task detailed here or here.
An alternative way you can implement this is by saving the time (say in the user defaults) the app enters the background and then when the app opens again, you check the time and move to the lock screen if it has been over 5 minutes.
Try below approach:
func applicationDidEnterBackground(_ application: UIApplication) {
let defaults = UserDefaults.standard
defaults.set(Date(), forKey: "LastInactiveDate")
defaults.synchronize()
}
func applicationWillEnterForeground(_ application: UIApplication) {
let defaults = UserDefaults.standard
if let lastInactiveDate = defaults.object(forKey: "LastInactiveDate") as? Date{
let seconds = Date().timeIntervalSince(lastInactiveDate)
print("Seconds ::" , seconds)
if seconds >= 300{
//Do any thing here to lock the app
}
}
defaults.set(nil, forKey: "LastInactiveDate")
defaults.synchronize()
}
Related
I'm trying to use applicationWillResignActive() in order to sync some data to my Firestore database before the application enters the background.
func applicationWillResignActive(_ application: UIApplication) {
self.uploadWantToPlay()
}
When I call my upload function from applicationWillResignActive() it runs but no data is added to Firestore before the next time the application becomes active.
When I for testing purposes instead run the same function from one of my ViewControllers the data is added instantly to Firestore.
I've also tried calling the function from applicationDidEnterBackground(), I've tried running it in it's own DispatchQueue. But it's had the same result.
How can I run this function as the user is about to leave the app and have it perform the database sync properly?
The functions handling the database sync;
func uploadWantToPlay() {
print ("Inside uploadWantToPlay")
if let wantToPlay = User.active.wantToPlayList {
if let listEntries = wantToPlay.list_entries {
let cleanedEntries = listEntries.compactMap({ (entry: ListEntry) -> ListEntry? in
if entry.game?.first_release_date != nil {
return entry
} else {
return nil
}
})
let gamesToUpload = cleanedEntries.filter {
$0.game!.first_release_date! > Int64(NSDate().timeIntervalSince1970 * 1000)
}
DatabaseConnection().writeWantToPlayToDatabase(user: User.active,wantToPlay: gamesToUpload)
}
}
}
func writeWantToPlayToDatabase(user: User, wantToPlay: [ListEntry]) {
firebaseSignIn()
let deviceId = ["\(user.deviceId)": "Device ID"]
for entry in wantToPlay {
let wantToPlayGameRef = fireStore.collection(WANTTOPLAY).document("\(entry.game!.id!)")
wantToPlayGameRef.updateData(deviceId) {(err) in
if err != nil {
wantToPlayGameRef.setData(deviceId) {(err) in
if let err = err {
Events().logException(withError: err, withMsg: "DatabaseConnection-writeWantToPlayToDatabase(user, [ListEntry]) Failed to write to database")
} else {
print("Document successfully written to WantToPlayGames")
}
}
} else {
print("Document successfully updated in WantToPlayGames")
}
}
}
}
According to the Apple documentation
Apps moving to the background are expected to put themselves into a
quiescent state as quickly as possible so that they can be suspended
by the system. If your app is in the middle of a task and needs a
little extra time to complete that task, it can call the
beginBackgroundTaskWithName:expirationHandler: or
beginBackgroundTaskWithExpirationHandler: method of the UIApplication
object to request some additional execution time. Calling either of
these methods delays the suspension of your app temporarily, giving it
a little extra time to finish its work. Upon completion of that work,
your app must call the endBackgroundTask: method to let the system
know that it is finished and can be suspended.
So, what you need to do here is to perform a finite length task while your app is being suspended. This will buy your app enough time to sync your records to the server.
An example snippet:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var backgroundTask: UIBackgroundTaskIdentifier!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
self.registerBackgroundTask()
// Do your background work here
print("Do your background work here")
// end the task when work is completed
self.endBackgroundTask()
}
func registerBackgroundTask() {
self.backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
assert(self.backgroundTask != UIBackgroundTaskInvalid)
}
func endBackgroundTask() {
print("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = UIBackgroundTaskInvalid
}
}
For further information refer to this article.
When apps goes background mode, I want to show a default image in capture screen. If user may try to kill the application with double press the home button which is show the list of application, there also need to show my static image.
I tried with this process, If any thing wrong please correct me.
func applicationDidEnterBackground(_ application: UIApplication) {
if((self.imgViewStaticImage) != nil)
{
self.imgViewStaticImage?.removeFromSuperview()
}
self.imgViewStaticImage = UIImageView(frame: (self.window?.bounds)!)
self.imgViewStaticImage?.image = UIImage(named: "banner")
self.window?.addSubview(self.imgViewStaticImage!)
self.window?.bringSubview(toFront: self.imgViewStaticImage!)
}
func applicationWillEnterForeground(_ application: UIApplication) {
if((self.imgViewStaticImage) != nil)
{
self.imgViewStaticImage?.removeFromSuperview()
}
}
In my app I need to download files with the following requirements:
download lots (say 3000) of small PNG files (say 5KB)
one by one
continue the download if the app in background
if an image download fails (typically because the internet connection has been lost), wait for X seconds and retry. If it fails Y times, then consider that the download failed.
be able to set a delay between each download to reduce the server load
Is iOS able to do that? I'm trying to use NSURLSession and NSURLSessionDownloadTask, without success (I'd like to avoid starting the 3000 download tasks at the same time).
EDIT: some code as requested by MwcsMac:
ViewController:
class ViewController: UIViewController, URLSessionDelegate, URLSessionDownloadDelegate {
// --------------------------------------------------------------------------------
// MARK: Attributes
lazy var downloadsSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier:"bgSessionConfigurationTest");
let session = URLSession(configuration: configuration, delegate: self, delegateQueue:self.queue);
return session;
}()
lazy var queue:OperationQueue = {
let queue = OperationQueue();
queue.name = "download";
queue.maxConcurrentOperationCount = 1;
return queue;
}()
var activeDownloads = [String: Download]();
var downloadedFilesCount:Int64 = 0;
var failedFilesCount:Int64 = 0;
var totalFilesCount:Int64 = 0;
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
startButton.addTarget(self, action:#selector(onStartButtonClick), for:UIControlEvents.touchUpInside);
_ = self.downloadsSession
_ = self.queue
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: User interaction
#objc
private func onStartButtonClick() {
startDownload();
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: Utils
func startDownload() {
downloadedFilesCount = 0;
totalFilesCount = 0;
for i in 0 ..< 3000 {
let urlString:String = "http://server.url/\(i).png";
let url:URL = URL(string: urlString)!;
let download = Download(url:urlString);
download.downloadTask = downloadsSession.downloadTask(with: url);
download.downloadTask!.resume();
download.isDownloading = true;
activeDownloads[download.url] = download;
totalFilesCount += 1;
}
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: URLSessionDownloadDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if(error != nil) { print("didCompleteWithError \(error)"); }
failedFilesCount += 1;
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let url = downloadTask.originalRequest?.url?.absoluteString {
activeDownloads[url] = nil
}
downloadedFilesCount += 1;
[eventually do something with the file]
DispatchQueue.main.async {
[update UI]
}
if(failedFilesCount + downloadedFilesCount == totalFilesCount) {
[all files have been downloaded]
}
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
DispatchQueue.main.async { completionHandler() }
}
}
}
// --------------------------------------------------------------------------------
}
AppDelegate:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var backgroundSessionCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// 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, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// 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 applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
}
Download:
class Download: NSObject {
var url: String
var isDownloading = false
var progress: Float = 0.0
var downloadTask: URLSessionDownloadTask?
var resumeData: Data?
init(url: String) {
self.url = url
}
}
What's wrong with this code:
I'm not sure that the background part is working. I followed this tutorial: https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started. It says that the app screenshot should be updated if I press home and then double tap home to show the app switcher. Does not seems to work reliably. It is updated when I re-open the app though. Having an iPhone since yesterday, I don't know if this is the normal behavior?
the 3000 downloads are started in the startDownload method. The maxConcurrentOperationCount of the queue does not seem to be respected: downloads are running concurrently
the downloadsSession.downloadTask(with: url); call takes 30ms. Multiplied by 3000, it takes 1mn30, that's a big problem :/ . Waiting a few seconds (2-3) would be ok.
I can't set a delay between two downloads (that's not a big problem. Would be nice though, but if I can't it will be OK)
Ideally, I would run the startDownload method asynchronously, and download the files synchronously in the for loop. But I guess I can't do that in background with iOS?
So here is what I finally did:
start the download in a thread, allowed to run for a few minutes in background (with UIApplication.shared.beginBackgroundTask)
download files one by one in a loop with a custom download method allowing to set a timeout
before downloading each file, check if UIApplication.shared.backgroundTimeRemaining is greater than 15
if yes, download the file with a timeout of min(60, UIApplication.shared.backgroundTimeRemaining - 5)
if no, stop downloading and save the download progress in the user defaults, in order to be able to resume it when the user navigates back to the app
when the user navigates back to the app, check if a state has been saved, and if so resume the download.
This way the download continues for a few minutes (3 on iOS 10) when the user leaves the app, and is pause just before these 3 minutes are elapsed. If the user leaves the app in background for more than 3 minutes, he must come back to finish the download.
I have a particular use case where I want to be able to start location updates when the app is already in the background. Specifically, I want to be able to press a button on my Pebble watchapp, and that causes my companion iOS app to begin location updates.
I am able to successfully start the location updates from my Pebble only if the iOS app is either in the foreground or has just entered the background within the past few seconds (like 5?). After a few seconds has passed with the app in the background, I can no longer start the updates. I know that the updates haven't started, since the backgroundTimeRemaining starts counting down to 0 (and if the updates did start, it stays at the max value). I also have the blue bar at the top that shows when location updates are on ("_____ is Using Your Location") and that also fails to show up after this weird ~5 second threshold has passed.
I've looked at almost all of the related questions on SO, and I've already tried basically everything that's been suggested, such as starting a background task right before starting the location updates. I've also tried delaying the start of the location updates by a couple seconds after starting the background task, and that doesn't work either.
The strange thing is, I also have an Apple Watch app that does the same things as the Pebble app, and the Apple Watch is able to start the location updates without fail all the time. Maybe the Apple Watch can do something special where it unsuspends the iOS app?
This is on iOS 9.
Any kind of help is appreciated, I'll try anything that's suggested.
Update: I put together a quick sample app that demonstrates the issue.
The code is posted below, or you can download the project from GitHub:
https://github.com/JessicaYeh/BackgroundLocationTest
If you try it out, you can see that setting the delay to 2 seconds works, but when the delay is longer (like 10 seconds), the updates don't start.
//
// AppDelegate.swift
// BackgroundLocationTest
//
// Created by Jessica Yeh on 2/18/16.
// Copyright © 2016 Yeh. All rights reserved.
//
import UIKit
import CoreLocation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let locationManager = CLLocationManager()
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
var timer: NSTimer?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
debugPrint(__FUNCTION__)
// Setup location manager
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
return true
}
#objc func checkBackgroundTimeRemaining() {
debugPrint(UIApplication.sharedApplication().backgroundTimeRemaining)
}
#objc func endBackgroundTask() {
if self.backgroundTask != UIBackgroundTaskInvalid {
UIApplication.sharedApplication().endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
}
}
func applicationWillResignActive(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationDidEnterBackground(application: UIApplication) {
debugPrint(__FUNCTION__)
// Cancel old timer and background task
timer?.invalidate()
endBackgroundTask()
// Start a new background task
backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {
self.endBackgroundTask()
}
// Start updating location
// let delaySec = 2.0 // successfully starts location update
let delaySec = 10.0 // doesn't start location update
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delaySec * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue(), {
debugPrint("Attempting to start updating location")
self.locationManager.startUpdatingLocation()
})
// Keep checking every 3 sec how much background time app has left
timer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: Selector("checkBackgroundTimeRemaining"), userInfo: nil, repeats: true)
}
func applicationWillEnterForeground(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationDidBecomeActive(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationWillTerminate(application: UIApplication) {
debugPrint(__FUNCTION__)
}
}
extension AppDelegate: CLLocationManagerDelegate {
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
debugPrint(__FUNCTION__)
}
}
Well, I don't exactly what you tried, but make sure you've turned on the Location updates on your target's Capabilities (Capabilities -> Background Modes) and also, for iOS 9.0, add this:
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"9.0")) {
self.locationManager.allowsBackgroundLocationUpdates = YES;
}
and change the line:
locationManager.requestWhenInUseAuthorization()
to:
locationManager.requestAlwaysAuthorization().
Good luck!
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("update"), userInfo: nil, repeats: true)
}
func update() {
println("Something cool")
}
}
It's ok for the Simulator ,I will get continuous "Something cool" through I tapped the home button. But it worked out when I debug the app with my iPhone. I did't get anything when I tapped the home button and make my app run background.
Someone told me I can play a long blank music to make my App run in the background.But I don't know how to play music in the background with swift #matt
You can use beginBackgroundTaskWithExpirationHandler to get some background execution time.
class ViewController: UIViewController {
var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?
override func viewDidLoad() {
super.viewDidLoad()
backgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
UIApplication.sharedApplication().endBackgroundTask(self.backgroundTaskIdentifier!)
})
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "update", userInfo: nil, repeats: true)
}
func update() {
println("Something cool")
}
}
Swift 3.0
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier!)
})
This code was inspired by this answer, but I ported to swift.
This apparently only runs for 3 minutes on iOS 7+.
I'm willing to bet that you don't need to run a timer in the background, what you need is to know the time that has elapsed while you were suspended. That is a much better use of your resources than trying to play an empty sound file.
Use applicationWillResignActive and applicationDidBecomeActive to determine how much time has elapsed. First save the fireDate of your timer and invalidate it in applicationWillResignActive
func applicationWillResignActive(application: UIApplication) {
guard let t = self.timer else { return }
nextFireDate = t.fireDate
t.invalidate()
}
Next, determine how much time is left for the timer in applicationDidBecomeActive by comparing the time now to the fireDate you saved in the applicationWillResignActive call:
func applicationDidBecomeActive(application: UIApplication) {
guard let n = nextFireDate else { return }
let howMuchLonger = n.timeIntervalSinceDate(NSDate())
if howMuchLonger < 0 {
print("Should have already fired \(howMuchLonger) seconds ago")
target!.performSelector(selector!)
} else {
print("should fire in \(howMuchLonger) seconds")
NSTimer.scheduledTimerWithTimeInterval(howMuchLonger, target: target!, selector: selector!, userInfo: nil, repeats: false)
}
}
If you need a repeating one, just do math to figure out how many times the timer should have fired while in the background
let howManyTimes = abs(howMuchLonger) / repeatInterval
when I tapped the home button and make my app run background
No, that's a false assumption. When you tap the home button, you do not make your app run in the background. You make your app suspend in the background. Your code does not run when you are in the background.
Apps can get special permission to run in the background just in order to perform certain limited activities, like continuing to play music or continuing to use Core Location. But your app does none of those things, so it goes into the background and stops.
add this code in appdelegate for run a task in background , its working finely,
var backgroundUpdateTask: UIBackgroundTaskIdentifier = 0
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
return true
}
func applicationWillResignActive(application: UIApplication) {
self.backgroundUpdateTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
self.endBackgroundUpdateTask()
})
}
func endBackgroundUpdateTask() {
UIApplication.sharedApplication().endBackgroundTask(self.backgroundUpdateTask)
self.backgroundUpdateTask = UIBackgroundTaskInvalid
}
func applicationWillEnterForeground(application: UIApplication) {
self.endBackgroundUpdateTask()
}