Xcode
iOS
Swift

【iOS】非公開APIを使わずにiPhoneのロック状態を検知する

はじめに

iPhoneアプリを作っていると、ロック状態を検知したいということが往々にしてあります。まず最初に思いつくのはこんなコードですが、

override func viewDidLoad() {
    NotificationCenter.default
        .addObserver(self,
                     selector: #selector(changedAppStatus(_:)),
                     name: .UIApplicationProtectedDataDidBecomeAvailable,
                     object: nil)

    NotificationCenter.default
        .addObserver(self,
                     selector: #selector(changedAppStatus(_:)),
                     name: .UIApplicationProtectedDataWillBecomeUnavailable,
                     object: nil)
}

@objc func changedAppStatus(_ notification: Notification) {
    if notification.name == .UIApplicationWillEnterForeground {
        // Foreground

    } else if notification.name == .UIApplicationDidEnterBackground {
        // Background
    }
}

これはアプリが開かれる/閉じられた時に通知が飛んでくるので、ロックされた時以外にも、ホームボタンを押してアプリを閉じた時なども通知されてしまいます。実のところ、Appleはロックを検知できるズバリなAPIを提供していません。

【非推奨】非公開APIを用いた手法

調べると、以下の様な実装方法が出てきます。

lockObserver = UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque())


CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
                                lockObserver,
                                { (_, _, _, _, _) in
                                    // Locked/Unlocked
                                },
                                "com.apple.springboard.lockcomplete" as CFString,
                                nil,
                                .deliverImmediately)

ですが、こちらは非公開APIを用いる方法なので推奨しません

非公開APIとは

非公開APIとは、実装されてはいるものの、Appleが公開していないAPIのことです。これを実装したアプリを審査に出すと、以下のようなメッセージとともにリジェクトされます。

Your app uses or references the following non-public APIs:

com.apple.springboard.lockcomplete

The use of non-public APIs is not permitted on the App Store because it can lead to a poor user experience should these APIs change.

Continuing to use or conceal non-public APIs in future submissions of this app may result in the termination of your Apple Developer account, as well as removal of all associated apps from the App Store.

the termination of your Apple Developer account :fearful:
隠蔽する方法もあるようですが、Appleに目をつけられるのでやめておきましょう。調べてみると、こちらのスレッドが見つかりました。
how to detect the locked/unlocked screen status with swift

Data Protectionを使う

上記のスレッドでは、こちらの通知を利用するようにと書かれています。

  • .UIApplicationProtectedDataDidBecomeAvailable
  • .UIApplicationProtectedDataWillBecomeUnavailable

これらは本来、「保護されたデータにコードからアクセス不可能/可能になった時」に送られる通知ですが、≒ ロック解除です。以下のコードで、iPhoneのロック/アンロックを検知することができます。

override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default
        .addObserver(self,
                     selector: #selector(changedAppStatus(_:)),
                     name: .UIApplicationProtectedDataDidBecomeAvailable,
                     object: nil)

    NotificationCenter.default
        .addObserver(self,
                     selector: #selector(changedAppStatus(_:)),
                     name: .UIApplicationProtectedDataWillBecomeUnavailable,
                     object: nil)
}

@objc func changedAppStatus(_ notification: Notification) {
    if notification.name == .UIApplicationProtectedDataDidBecomeAvailable {
        // unlocked device
    }
    if notification.name == .UIApplicationProtectedDataWillBecomeUnavailable {
        // locked device
    }
}

ただ、iPhoneにパスコードがセットされていない場合この通知は飛んできません。LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)falseが返ってくる時はデバイスにパスコードが設定されていないので、その時はよしなに処理しましょう。
また、シミュレータでは上記のメソッドでtrueが返ってきますが、パスコードは設定できないのでこの通知を受け取ることはできません。

おわりに

iPhoneのロック検知には.UIApplicationProtectedDataDidBecomeAvailable.UIApplicationProtectedDataWillBecomeUnavailableの通知を使いましょう。パスコードの設定されていないiPhoneについては、現在調査中ですが、おそらくロックを検知する正規の方法はありません。