Introduction
Understanding the iOS app lifecycle is fundamental to building reliable, responsive applications. Your app constantly transitions between states as users switch apps, receive calls, or lock their devices. How you handle these transitions determines whether your app feels polished or frustrating.
iOS apps exist in one of several states, and the system sends notifications when transitioning between them. Responding appropriately to these transitions ensures data is saved, resources are released, and users return to exactly where they left off.
This guide covers the complete iOS app lifecycle, from launch to termination, with practical patterns for handling each state correctly.
App States Overview
iOS apps
move through five primary states:
Not Running
The app is not launched or was terminated by the system. No code executes.
Inactive
The app is in the foreground but not receiving events. This brief transitional state occurs during:
- App launch
- Incoming phone calls
- Control Center/Notification Center display
- System alerts
Active
The app is in the foreground and receiving events. This is the normal running state where users interact with your app.
Background
The app is in the background but executing code. Apps enter this state when:
- User switches to another app
- User presses home button
- System needs to launch app for background task
Background time is limited. Most apps get approximately 5 seconds of execution time before being suspended.
Suspended
The app is in the background but not executing code. The system keeps the app in memory but may terminate it without notice to reclaim resources.
App Delegate Methods
For apps no
t using scenes (iOS 12 and earlier pattern), AppDelegate handles lifecycle events.
Application Launch
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// One-time setup
configureAnalytics()
configurePushNotifications()
setupCoreData()
// Check launch options for deep links, notifications, etc.
if let url = launchOptions?[.url] as? URL {
handleDeepLink(url)
}
return true
}
This method is called once per app launch. Perform essential setup here, but keep it fast. Slow launches lead to app termination and poor user experience.
State Transitions
func applicationWillResignActive(_ application: UIApplication) {
// App about to move from active to inactive
// Pause ongoing tasks, disable timers
pauseGame()
saveCurrentProgress()
}
func applicationDidEnterBackground(_ application: UIApplication) {
// App now in background
// Save data, release shared resources
saveApplicationState()
releaseSharedResources()
// Request additional background time if needed
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
backgroundTask = application.beginBackgroundTask {
application.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
// Perform critical work
completeCriticalSave {
application.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
func applicationWillEnterForeground(_ application: UIApplication) {
// App about to enter foreground
// Undo background changes, refresh data
refreshUserInterface()
checkForUpdates()
}
func applicationDidBecomeActive(_ application: UIApplication) {
// App now active
// Restart paused tasks, refresh UI
resumeGame()
updateBadgeCount()
}
func applicationWillTerminate(_ application: UIApplication) {
// App about to terminate
// Save data, perform final cleanup
// Note: Not called if app is suspended first
saveAllData()
}
Scene-Based Lifecycle (iOS 13+)
iOS 13 introduced scenes, enabling multiple instances of your app’s UI. This is particularly relevant for iPad multitasking.
Scene Delegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
// Scene connecting
// Create window and root view controller
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = MainViewController()
window?.makeKeyAndVisible()
// Handle any URLs or user activities
if let urlContext = connectionOptions.urlContexts.first {
handleDeepLink(urlContext.url)
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Scene disconnected
// Release resources specific to this scene
// Called when system discards scene (different from background)
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Scene now active
refreshData()
}
func sceneWillResignActive(_ scene: UIScene) {
// Scene about to become inactive
pauseActivities()
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Scene moving to foreground
prepareInterface()
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Scene now in background
saveState()
}
}
App Delegate with Scenes
When using scenes, AppDelegate still handles app-level events:
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// App-level setup only
// UI setup moves to SceneDelegate
configureFrameworks()
return true
}
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
// Return scene configuration
return UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
}
func application(
_ application: UIApplication,
didDiscardSceneSessions sceneSessions: Set<UISceneSession>
) {
// User closed scenes
// Clean up resources for discarded scenes
for session in sceneSessions {
cleanupResources(for: session)
}
}
}
Handling Common Scenarios
Saving User Data
Save data at appropriate lifecycle points:
class DataManager {
static let shared = DataManager()
private var unsavedChanges = false
func markDirty() {
unsavedChanges = true
scheduleAutoSave()
}
private func scheduleAutoSave() {
// Auto-save after period of inactivity
NSObject.cancelPreviousPerformRequests(
withTarget: self,
selector: #selector(autoSave),
object: nil
)
perform(#selector(autoSave), with: nil, afterDelay: 3.0)
}
@objc func autoSave() {
guard unsavedChanges else { return }
save()
}
func save() {
// Perform save
persistToDisk()
unsavedChanges = false
}
func saveIfNeeded() {
if unsavedChanges {
save()
}
}
}
// In SceneDelegate
func sceneWillResignActive(_ scene: UIScene) {
DataManager.shared.saveIfNeeded()
}
func sceneDidEnterBackground(_ scene: UIScene) {
DataManager.shared.save()
}
Pausing and Resuming Activities
Handle media playback, downloads, and other ongoing activities:
class MediaPlayer {
private var wasPlayingBeforeInterruption = false
init() {
setupNotifications()
}
private func setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleInterruption),
name: AVAudioSession.interruptionNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillResignActive),
name: UIApplication.willResignActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}
@objc private func appWillResignActive() {
wasPlayingBeforeInterruption = isPlaying
// Continue playing if background audio enabled
// Otherwise pause
if !hasBackgroundAudioCapability {
pause()
}
}
@objc private func appDidBecomeActive() {
if wasPlayingBeforeInterruption && !isPlaying {
play()
}
}
@objc private func handleInterruption(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
switch type {
case .began:
wasPlayingBeforeInterruption = isPlaying
pause()
case .ended:
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) && wasPlayingBeforeInterruption {
play()
}
}
@unknown default:
break
}
}
}
Handling Phone Calls and Interruptions
System interruptions affect your app’s state:
class GameViewController: UIViewController {
private var gameState: GameState = .playing
private var stateBeforeInterruption: GameState?
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillResignActive),
name: UIApplication.willResignActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}
@objc private func appWillResignActive() {
stateBeforeInterruption = gameState
if gameState == .playing {
pauseGame()
}
}
@objc private func appDidBecomeActive() {
// Don't auto-resume - show pause menu
// Let user choose when to resume
if stateBeforeInterruption == .playing {
showPauseMenu()
}
}
private func pauseGame() {
gameState = .paused
gameEngine.pause()
showPauseOverlay()
}
}
State Restoration
State restoration returns users to exactly where they left off.
Opt-In to State Restoration
// AppDelegate
func application(
_ application: UIApplication,
shouldSaveSecureApplicationState coder: NSCoder
) -> Bool {
return true
}
func application(
_ application: UIApplication,
shouldRestoreSecureApplicationState coder: NSCoder
) -> Bool {
return true
}
Assign Restoration Identifiers
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
restorationIdentifier = "MainViewController"
restorationClass = MainViewController.self
}
}
extension MainViewController: UIViewControllerRestoration {
static func viewController(
withRestorationIdentifierPath identifierComponents: [String],
coder: NSCoder
) -> UIViewController? {
return MainViewController()
}
}
Encode and Decode State
class DetailViewController: UIViewController {
var itemID: String?
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(itemID, forKey: "itemID")
}
override func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
itemID = coder.decodeObject(forKey: "itemID") as? String
if let id = itemID {
loadItem(with: id)
}
}
}
Scene-Based State Restoration
For scene-based apps, use NSUserActivity:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
// Create activity representing current state
let activity = NSUserActivity(activityType: "com.app.viewingItem")
activity.userInfo = [
"itemID": currentItemID,
"scrollPosition": scrollView.contentOffset.y
]
return activity
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Restore from activity
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
restoreState(from: activity)
}
}
private func restoreState(from activity: NSUserActivity) {
guard let userInfo = activity.userInfo else { return }
if let itemID = userInfo["itemID"] as? String {
navigateToItem(itemID)
}
if let scrollPosition = userInfo["scrollPosition"] as? CGFloat {
scrollView.contentOffset.y = scrollPosition
}
}
}
Background Execution
Apps can request limited background execution for specific tasks.
Background Task Completion
Request time to complete tasks when backgrounded:
func sceneDidEnterBackground(_ scene: UIScene) {
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "SaveData") {
// Expiration handler - clean up
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
// Perform work
DispatchQueue.global().async {
self.saveAllPendingData()
DispatchQueue.main.async {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
}
Background Modes
Enable specific background capabilities in your app’s Info.plist:
- Audio: Continue playing audio
- Location updates: Receive location changes
- VoIP: Handle incoming VoIP calls
- External accessory: Communicate with accessories
- Bluetooth: Act as Bluetooth accessory
- Background fetch: Periodically fetch content
- Remote notifications: Wake for silent push notifications
Background Fetch
Schedule periodic content updates:
// AppDelegate
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Set minimum fetch interval
application.setMinimumBackgroundFetchInterval(
UIApplication.backgroundFetchIntervalMinimum
)
return true
}
func application(
_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
// Fetch new data
DataService.shared.fetchLatestContent { result in
switch result {
case .success(let hasNewData):
completionHandler(hasNewData ? .newData : .noData)
case .failure:
completionHandler(.failed)
}
}
}
Memory Warnings
Respond to low memory conditions to prevent termination:
class ImageCache {
private var cache = NSCache<NSString, UIImage>()
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleMemoryWarning),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
@objc private func handleMemoryWarning() {
// Clear cache to free memory
cache.removeAllObjects()
}
}
// In view controller
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc.
imageCache.removeAll()
// Release anything not currently visible
if !isViewLoaded || view.window == nil {
view = nil
}
}
Best Practices
Launch Time Optimization
Keep launch fast:
- Defer non-essential setup
- Load data asynchronously
- Use launch storyboard for instant UI
- Profile with Instruments
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Essential only
setupCriticalServices()
// Defer everything else
DispatchQueue.main.async {
self.setupAnalytics()
self.setupRemoteConfig()
self.prefetchContent()
}
return true
}
Avoid Blocking Main Thread
Long operations during transitions cause poor UX:
func sceneDidEnterBackground(_ scene: UIScene) {
// Quick synchronous work only
markSaveNeeded()
// Heavy work in background
DispatchQueue.global(qos: .utility).async {
self.performHeavySave()
}
}
Test Lifecycle Thoroughly
Simulate various scenarios:
- Incoming calls during critical operations
- Low memory conditions
- App termination and restoration
- Rapid app switching
- Background fetch timing
Use Xcode’s debugging tools:
- Simulate Memory Warning (Debug menu)
- Background Fetch (Debug menu)
- Slow Animations to spot transition issues
Conclusion
Proper lifecycle management separates professional apps from amateur ones. Users expect apps to save their work, resume seamlessly, and handle interruptions gracefully.
Key takeaways:
- Save data at multiple points, not just termination
- Handle interruptions without losing user progress
- Implement state restoration for seamless return
- Respect background execution limits
- Respond appropriately to memory warnings
Test your lifecycle handling thoroughly. The scenarios users encounter are unpredictable, and getting this right builds trust in your app.
Building an iOS app that needs robust lifecycle management? The Awesome Apps team specializes in creating reliable iOS applications that handle every edge case. Contact us to discuss your project.