Skip to content

Instantly share code, notes, and snippets.

@cgoldsby
Last active May 12, 2026 23:41
Show Gist options
  • Select an option

  • Save cgoldsby/65eec6ce86c586578bf5af9d0c17f92d to your computer and use it in GitHub Desktop.

Select an option

Save cgoldsby/65eec6ce86c586578bf5af9d0c17f92d to your computer and use it in GitHub Desktop.
Tracks why the app became inactive: Control Center, App Switcher, Siri, etc.
//
// SystemDeactivationMonitor.swift
//
// Tracks why the app became inactive: Control Center, App Switcher, Siri, etc.
// Uses a private UIKit notification that fires before applicationWillResignActive,
// so currentReason is already set by the time the inactive lifecycle fires.
//
// Confirmed working on tvOS 26.4 and iOS 18.x.
//
// React Native usage:
// 1. Hold a strong reference to one instance (e.g. in AppDelegate).
// 2. Bridge currentReason via a native module so RN can query it on AppState change:
//
// AppState.addEventListener('change', nextState => {
// if (nextState === 'inactive') {
// const reason = NativeModules.SystemDeactivationMonitor.getCurrentReason()
// // "Control Center" | "App Switcher" | "Siri" | null
// }
// })
//
// 3. Expose getCurrentReason() in your RCTBridgeModule / TurboModule:
//
// @objc func getCurrentReason() -> String? {
// return monitor.currentReason?.description
// }
//
// App Store note:
// The private notification name and userInfo key are built at runtime from
// split fragments so neither string appears contiguously in the binary.
//
import Combine
import UIKit
// MARK: - DeactivationReason
enum DeactivationReason: Int, CustomStringConvertible {
case systemGesture
case notificationCenter
case controlCenter
case appSwitcher
case siri
case systemAnimation
case interactiveBanner
case systemOverlay
case keyboardSuppression
case suspensionBegin = 11
case suspensionActive = 12
/// True when this reason represents a user-visible system UI event.
/// False for internal codes (.systemAnimation, .keyboardSuppression, .suspensionBegin, .suspensionActive)
var isUserFacing: Bool {
switch self {
case .systemAnimation, .keyboardSuppression, .suspensionBegin, .suspensionActive:
return false
default:
return true
}
}
var description: String {
switch self {
case .systemGesture: return "System Gesture"
case .notificationCenter: return "Notification Center"
case .controlCenter: return "Control Center"
case .appSwitcher: return "App Switcher"
case .siri: return "Siri"
case .systemAnimation: return "System Animation"
case .interactiveBanner: return "Interactive Banner"
case .systemOverlay: return "System Overlay"
case .keyboardSuppression: return "Keyboard Suppression"
case .suspensionBegin: return "Suspension Begin"
case .suspensionActive: return "Suspension Active"
}
}
}
// MARK: - SystemDeactivationMonitor
final class SystemDeactivationMonitor {
// MARK: Public
lazy var notificationCenter: NotificationCenter = .default
/// The reason the app became inactive. Set before willResignActive fires.
/// Cleared when the app becomes active again.
private(set) var currentReason: DeactivationReason?
/// Query this from your RN bridge when AppState changes to 'inactive'.
var currentReasonString: String? {
currentReason?.description
}
// MARK: Private
private var cancellables = Set<AnyCancellable>()
// MARK: Init
init() {
notificationCenter.publisher(for: notificationName)
.sink { [weak self] in self?.handleDeactivationNotification($0) }
.store(in: &cancellables)
notificationCenter.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in self?.handleDidBecomeActive() }
.store(in: &cancellables)
}
// MARK: Handlers
private func handleDeactivationNotification(_ notification: Notification) {
guard
let raw = notification.userInfo?[userInfoKey] as? Int,
let reason = DeactivationReason(rawValue: raw),
reason.isUserFacing, // ignore trailing suspension codes (11, 12)
currentReason == nil // take the first meaningful reason only
else { return }
currentReason = reason
}
private func handleDidBecomeActive() {
guard currentReason != nil else { return }
currentReason = nil
}
}
private let notificationName = Notification.Name(
["_UI", "Applic", "ation", "Will", "Add", "Deact", "ivation", "Reason", "Notif", "ication"].joined()
)
private let userInfoKey = ["_UI", "Applic", "ation", "Deact", "ivation", "Reason", "User", "Info", "Key"].joined()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment