I need to reliably detect if an iOS device has been rebooted since the app was last launched. The key challenge is to differentiate a genuine reboot from a situation where the user has simply changed the system time manually.
Initial Flawed Approaches:
Using KERN_BOOTTIME
: A common approach is to use sysctl
to get the kernel boot time.
// Fetches the calculated boot time
func currentBootTime() -> Date {
var mib = [CTL_KERN, KERN_BOOTTIME]
var bootTime = timeval()
var size = MemoryLayout<timeval>.size
sysctl(&mib, UInt32(mib.count), &bootTime, &size, nil, 0)
return Date(timeIntervalSince1970: TimeInterval(bootTime.tv_sec))
}
The problem is that this value is not a fixed timestamp. It's calculated by the OS as wallClockTime - systemUptime
. If a user manually changes the clock, the returned Date
will also shift, leading to a false positive.
Using systemUptime
: Another approach is to check the system's monotonic uptime via ProcessInfo.processInfo.systemUptime
or clock_gettime
. If the new uptime is less than the last saved uptime, it must have been a reboot.
The problem here is the "reboot and wait" scenario. A user could reboot the device and wait long enough for the new uptime to surpass the previously saved value, leading to a false negative.
The Core Challenge:
How can we create a solution that correctly identifies a true reboot and is immune to all edge cases, including:
Manual time changes (both forward and backward).
The "reboot and wait" scenario.
After extensive testing, the most robust solution is to correlate three pieces of information on every app launch:
System Uptime: A monotonic clock that only resets on reboot.
Wall-Clock Time: The user-visible time (Date()
).
Calculated Boot Time: The value from KERN_BOOTTIME
.
The OS maintains a fundamental mathematical relationship between these three values:
elapsedBootTime ≈ elapsedWallTime - elapsedUptime
If this equation holds true between two app launches, it means we are in the same boot session. Any change in the reported boot time is simply a result of a manual clock adjustment.
If this equation is broken, it can only mean that a new boot session has started, and the underlying uptime and boot time values have been reset independently of the wall clock. This is a genuine reboot.
Here is a complete, self-contained class that implements this logic. It correctly handles all known edge cases.
import Foundation
import Darwin
/// A robust utility to definitively detect if a device has been rebooted,
/// differentiating a genuine reboot from a manual clock change.
public final class RebootDetector {
// MARK: - UserDefaults Keys
private static let savedUptimeKey = "reboot_detector_saved_uptime"
private static let savedBootTimeKey = "reboot_detector_saved_boot_time"
private static let savedWallTimeKey = "reboot_detector_saved_wall_time"
/// Contains information about the boot state analysis.
public struct BootAnalysisResult {
/// True if a genuine reboot was detected.
let didReboot: Bool
/// The boot time calculated during this session.
let bootTime: Date
/// A human-readable string explaining the reason for the result.
let reason: String
}
/// Checks if the device has genuinely rebooted since the last time this function was called.
///
/// This method is immune to manual time changes and the "reboot and wait" edge case.
///
/// - Returns: A `BootAnalysisResult` object with the result and diagnostics.
public static func checkForGenuineReboot() -> BootAnalysisResult {
// 1. Get current system state
let newUptime = self.getSystemUptime()
let newBootTime = self.getKernelBootTime()
let newWallTime = Date()
// 2. Retrieve previous state from UserDefaults
let savedUptime = UserDefaults.standard.double(forKey: savedUptimeKey)
let savedBootTimeInterval = UserDefaults.standard.double(forKey: savedBootTimeKey)
let savedWallTimeInterval = UserDefaults.standard.double(forKey: savedWallTimeKey)
// 3. Persist the new state for the next launch
UserDefaults.standard.set(newUptime, forKey: savedUptimeKey)
UserDefaults.standard.set(newBootTime.timeIntervalSince1970, forKey: savedBootTimeKey)
UserDefaults.standard.set(newWallTime.timeIntervalSince1970, forKey: savedWallTimeKey)
// --- Analysis Logic ---
// On first launch, there's no previous state to compare with.
if savedUptime == 0 {
return BootAnalysisResult(didReboot: true, bootTime: newBootTime, reason: "First launch detected.")
}
// Primary Check: If uptime has reset, it's always a genuine reboot. This is the simplest case.
if newUptime < savedUptime {
return BootAnalysisResult(didReboot: true, bootTime: newBootTime, reason: "Genuine Reboot: System uptime was reset.")
}
// At this point, newUptime >= savedUptime. This could be a normal launch,
// a manual time change, or the "reboot and wait" edge case.
let savedWallTime = Date(timeIntervalSince1970: savedWallTimeInterval)
let savedBootTime = Date(timeIntervalSince1970: savedBootTimeInterval)
let elapsedUptime = newUptime - savedUptime
let elapsedWallTime = newWallTime.timeIntervalSince(savedWallTime)
let elapsedBootTime = newBootTime.timeIntervalSince(savedBootTime)
// The Core Formula Check: Does the math add up?
// We expect: elapsedBootTime ≈ elapsedWallTime - elapsedUptime
let expectedElapsedBootTime = elapsedWallTime - elapsedUptime
// Allow a small tolerance (e.g., 5 seconds) for minor system call inaccuracies.
if abs(elapsedBootTime - expectedElapsedBootTime) < 5.0 {
// The mathematical relationship holds. This means we are in the SAME boot session.
// It's either a normal launch or a manual time change. Both are "not a reboot".
// We can even differentiate them for more detailed logging.
if abs(elapsedWallTime - elapsedUptime) < 5.0 {
return BootAnalysisResult(didReboot: false, bootTime: newBootTime, reason: "No Reboot: Time continuity maintained.")
} else {
return BootAnalysisResult(didReboot: false, bootTime: newBootTime, reason: "No Reboot: Manual time change detected.")
}
} else {
// The mathematical relationship is broken.
// This can only happen if a new boot session has started, invalidating our saved values.
// This correctly catches the "reboot and wait" scenario.
return BootAnalysisResult(didReboot: true, bootTime: newBootTime, reason: "Genuine Reboot: Time continuity broken.")
}
}
// MARK: - Helper Functions
/// Fetches monotonic system uptime, which is not affected by clock changes.
private static func getSystemUptime() -> TimeInterval {
var ts = timespec()
// CLOCK_MONOTONIC is the correct choice for iOS as it includes sleep time.
guard clock_gettime(CLOCK_MONOTONIC, &ts) == 0 else {
// Provide a fallback for safety, though clock_gettime should not fail.
return ProcessInfo.processInfo.systemUptime
}
return TimeInterval(ts.tv_sec) + TimeInterval(ts.tv_nsec) / 1_000_000_000
}
/// Fetches the calculated boot time from the kernel.
private static func getKernelBootTime() -> Date {
var mib = [CTL_KERN, KERN_BOOTTIME]
var bootTime = timeval()
var size = MemoryLayout<timeval>.size
guard sysctl(&mib, UInt32(mib.count), &bootTime, &size, nil, 0) == 0 else {
// In a real app, you might want to handle this error more gracefully.
fatalError("sysctl KERN_BOOTTIME failed; errno: \(errno)")
}
return Date(timeIntervalSince1970: TimeInterval(bootTime.tv_sec))
}
}
Call the function early in your app's lifecycle, for example in your AppDelegate
:
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let result = RebootDetector.checkForGenuineReboot()
if result.didReboot {
print("✅ A genuine reboot was detected!")
} else {
print("❌ No reboot occurred since the last launch.")
}
print(" Reason: \(result.reason)")
print(" Current session boot time: \(result.bootTime)")
// Your other setup code...
return true
}
}