How can I tell if my app has suspended?
Asked Answered
C

3

11

My understanding of iOS state management is that when the user hits the home button, the app becomes inactive, then enters the background, and then after a few seconds is suspended. A suspended app is then terminated if the system needs to free memory or if the user swipes the app away from the recents list.

My question is, is there any way for me to tell that my app has left the background state and entered the suspended state? I'm aware of the application delegate methods like applicationDidEnterBackground etc, but is there a way that I can tell the app was suspended? Am I correct in thinking that being suspended is not the same as being terminated?

My context for asking this question is that I'm creating an audio player app. I've enabled background audio in Info.plist and so when audio is playing and I press the home button I can see that the app remains in the background indefinitely (which is good). However, when audio is not playing there's no need to keep the app in the background and, as far as I understand it, the app ought to suspend. I want to be able to check whether or not this is happening!

Thanks very much - and do correct any misunderstandings I have.

Craiova answered 16/6, 2014 at 13:26 Comment(1)
has this problem been solved or are you still waiting for an answer?Hadj
D
11

iOS doesn't report when an app will be suspended (placed from background into a non-processing state) nor does it seem to fire a notification once the app has resumed. There is some confusion about this as there is a notification when the app becomes "active" or will resign the "active" state, however this is not always the right value needed. iOS Apps have a number of states:

  • Active: App is in the foreground (frontmost) and there are no notifications or menu's pulled over it. Pulling a menu down or getting an external notification or text message will cause the app to "resign" active, and resume active once the alert has been dealt with.
  • Background: App is not in the foreground but still processing. This happens briefly before suspend if there are no background tasks running, or can be a permanent state if there is a long running background mode (audio, location, etc) running.
  • Suspended: App is in memory, but run loops and processing is paused
  • Terminated: App is not running or in memory, has been quit either by user (force quit) or OS (to free up memory) or was never launched

In order to capture when app is suspended, you'll have to capture when app is being put into background and use values like backgroundTimeRemaining to estimate when a suspension will occur. Actual suspension can only be calculated as a "gap" in run loop processing, which can be done with a scheduled recurring timer and subtraction. I've created a helper class for this:

https://gist.github.com/BadPirate/0a480b947744c8c0e326daa4ab479b09

import UIKit
import Foundation

internal extension Notification.Name {
    static let applicationWillSuspend = Notification.Name("application-will-suspend")
    /// This notification gets called after the fact, but the `object` parameter is set to the `Date` of when the suspend occurred
    static let applicationDidSuspend = Notification.Name("application-did-suspend")
    static let applicationDidUnsuspend = Notification.Name("application-did-unsuspend")
    static let suspendStatusRecorderFailed = Notification.Name("suspend-status-recorder-failed")
}

internal class SuspendStatusRecorder {
    private var timer : Timer?
    private var task : UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid

    /// Start monitoring for suspend
    /// - parameter stallThreshold: Number of seconds of no processing before reporting a stall event
    internal func start() {
        stop() // If already going.
        startTask()
        let timer = Timer(timeInterval: 1, repeats: true) { [weak self] (_) in
            self?.checkStatus()
        }
        RunLoop.main.add(timer, forMode: .common)
    }

    internal func stop() {
        if let timer = timer {
            timer.invalidate()
            self.timer = nil
        }
        endTask()
    }

    private var lastPing : Int = 0
    private func willExpire() {
        endTask() // Allow app to suspend
        NotificationCenter.default.post(name: .applicationWillSuspend, object: nil)
        expectingSuspend = true
    }

    /// Set to an uptime value for when we expect our app to be suspended based on backgroundTimeRemaining
    private var expectingSuspend = false

    private func checkStatus() {
        let ping = uptime()
        if expectingSuspend {
            if ping - lastPing > 3 ||
            UIApplication.shared.applicationState == .active
            {
                // Timer stalled, either CPU failure or we were suspended.
                NotificationCenter.default.post(name: .applicationDidSuspend, object: Date(timeIntervalSinceNow: TimeInterval(lastPing - ping)))
                NotificationCenter.default.post(name: .applicationDidUnsuspend, object: nil)
                expectingSuspend = false
                startTask() // New background task so that we can make sure to catch next event
            }
        }
        lastPing = uptime()

        // In background, time is going to expire (resulting in suspend), report and end task
        if UIApplication.shared.applicationState == .background &&
           UIApplication.shared.backgroundTimeRemaining != Double.greatestFiniteMagnitude &&
           task != UIBackgroundTaskIdentifier.invalid
        {
            willExpire()
        }
    }

    private func endTask() {
        if task != UIBackgroundTaskIdentifier.invalid {
            UIApplication.shared.endBackgroundTask(task)
            self.task = UIBackgroundTaskIdentifier.invalid
        }
    }

    private func startTask() {
        task = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
            self?.willExpire()
        })
    }

    private func uptime() -> Int {
        var uptime = timespec()
        if 0 != clock_gettime(CLOCK_MONOTONIC_RAW, &uptime) {
            NotificationCenter.default.post(name: .suspendStatusRecorderFailed, object: "Could not execute clock_gettime, errno: \(errno)")
            stop()
        }
        return uptime.tv_sec
    }

    deinit {
        stop()
    }
}
Diplodocus answered 28/1, 2020 at 19:28 Comment(0)
H
4

You don't get a notification about being suspended:

https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html

"Suspended: The app is in the background but is not executing code. The system moves apps to this state automatically and does not notify them before doing so. While suspended, an app remains in memory but does not execute any code.

When a low-memory condition occurs, the system may purge suspended apps without notice to make more space for the foreground app."

Hadj answered 16/6, 2014 at 13:31 Comment(7)
Thank you - so there isn't any way for me to see whether my app is remaining in the background longer than it ought to? Is there any diagnostic tool that will allow me to find out whether the app has been suspended?Craiova
"Background" you can find out about, with an easy search here on SO. But according to Apple, there is no notification for "Suspended". Since no code is executed during suspension, you could run a counter in Background mode. If that counter isn't as high as you expect, then you might know you've been suspended.Hadj
Ah that's very helpful! So I can just set an indefinite loop or something going once the app enters background, and if it stops going up then I'll be able to tell that the app has suspended?Craiova
That might work! :D I say "might" just because I have not actually tried this. But it sounds reasonable.Hadj
If you decide this is the correct answer, you should click the checkmark. I realize you may still be testing.Hadj
Oh sorry! Yeah I'm not entirely sure - the loop thing didn't work so I will leave it unresolved in the hope that there's another way to answer the question. But thanks :)Craiova
Sure, no problem. I just check because your rep is low, and you might not have known. Good luck. :)Hadj
E
0

You can use a background task to check if the app was suspended.

  1. Create a helper class like SuspensionObserver.
  2. Inside the SuspensionObserver you should have a flag like wasAppSuspended.
  3. Create func observe() with the UIApplication.shared.beginBackgroundTask inside.
  4. Set the wasAppSuspended flag to false inside the observe()
  5. Set the wasAppSuspended flag to true inside the expirationHandler.
  6. Call observe() from didFinishLaunchingWithOptions or from applicationDidEnterBackground.
  7. Now in applicationWillEnterForeground, you can check if the app was suspended.

Also, ofc you can set delegates/handlers to perform required actions before the app is suspended.

Ed answered 19/6, 2020 at 7:10 Comment(1)
How can I know that my app went into suspended state after the expiration handler is called?Disraeli

© 2022 - 2024 — McMap. All rights reserved.