A Swift Package to detect whether an Apple device's screen is locked or unlocked.
- Detect current lock state (
.locked,.unlocked,.unknown) - Observe lock/unlock state changes in real-time
- Cross-platform support: macOS, iOS, Mac Catalyst
- App Extension support on iOS
- Swift Concurrency ready (
Sendableconformance)
- macOS 12.0+
- iOS 13.0+
- Mac Catalyst 13.0+
- Swift 5.10+
Add the following to your Package.swift:
dependencies: [
.package(url: "https://github.com/LZhenHong/LockDetector.git", branch: "main")
]Or in Xcode: File → Add Package Dependencies → Enter the repository URL.
import LockDetector
// macOS (synchronous)
let state = LockDetector.currentState
// iOS (requires @MainActor)
@MainActor
func checkLockState() {
let state = LockDetector.currentState
switch state {
case .locked:
print("Device is locked")
case .unlocked:
print("Device is unlocked")
case .unknown:
print("Unable to determine lock state")
}
}import LockDetector
// Store the token to keep observing
let token = LockDetector.observeStateChanges { state in
print("Screen is now \(state)")
}
// Stop observing when done
token.invalidate()
// Or let the token deinit to auto-invalidateFor App Extensions on iOS (Today Extension, Share Extension, etc.), call initialize() early in your extension's lifecycle:
import LockDetector
// In your extension's entry point
LockDetector.initialize()
// Then check state as needed
@MainActor
func checkState() {
let state = LockDetector.currentState
}
⚠️ Widget Extensions are not supported.currentStatereturns.unknownwhen called from a Widget Extension.
This limitation exists due to fundamental technical constraints:
| Constraint | Description |
|---|---|
| Sandbox isolation | Widget Extensions run in a separate container from the main app |
| No UIApplication | UIApplication.shared is unavailable in Widget Extensions |
| Timeline-based | Widgets update at system-determined intervals, not real-time |
| Lock Screen paradox | Lock Screen widgets only display when device is locked |
Use isWidgetExtension to detect if running in a Widget Extension:
if LockDetector.isWidgetExtension {
// Handle widget context - lock detection not available
}- Current state: Uses
CGSessionCopyCurrentDictionary()to read theCGSSessionScreenIsLockedkey - State changes: Observes
com.apple.screenIsLockedandcom.apple.screenIsUnlockedviaDistributedNotificationCenter
- Main app: Uses
UIApplication.shared.isProtectedDataAvailable - App Extension: Creates a file with
FileProtectionType.completethat becomes unreadable when locked - State changes: Observes
protectedDataDidBecomeAvailableNotificationandprotectedDataWillBecomeUnavailableNotification
public enum ScreenState: Sendable {
case unknown // Unable to determine state
case locked // Device is locked
case unlocked // Device is unlocked
}Returns the current lock state.
- macOS: Synchronous property
- iOS/Catalyst: Requires
@MainActor
@discardableResult
public static func observeStateChanges(
_ handler: @escaping @Sendable (ScreenState) -> Void
) -> ObservationTokenObserves lock state changes. Returns an ObservationToken to manage the observation lifecycle.
public final class ObservationToken: @unchecked Sendable {
public func invalidate() // Stop observing
}// Initialize protected file for App Extension support
public static func initialize()
// Check if running in an App Extension
public static var isAppExtension: Bool
// Check if running in a Widget Extension (returns .unknown for currentState)
public static var isWidgetExtension: Bool
// Path to the protected file
public static var protectedFilePath: StringMIT License