79666563

Date: 2025-06-15 13:02:46
Score: 1
Natty:
Report link

How to reliably detect a true device reboot in Swift, ignoring manual clock changes?

The Question

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:

  1. 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.

  2. 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:

The Answer

After extensive testing, the most robust solution is to correlate three pieces of information on every app launch:

  1. System Uptime: A monotonic clock that only resets on reboot.

  2. Wall-Clock Time: The user-visible time (Date()).

  3. Calculated Boot Time: The value from KERN_BOOTTIME.

The Core Principle

The OS maintains a fundamental mathematical relationship between these three values:

elapsedBootTime ≈ elapsedWallTime - elapsedUptime

The Final Swift Solution

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))
    }
}

How to Use

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
    }
}
Reasons:
  • Blacklisted phrase (1): How can we
  • Blacklisted phrase (0.5): I need
  • Whitelisted phrase (-1): solution is
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • Starts with a question (0.5): How to
  • Low reputation (1):
Posted by: Amar Sharma