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

# はじめに

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

```swift
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を用いた手法

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

```swift
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](https://forums.developer.apple.com/thread/70079)

# Data Protectionを使う

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

- `.UIApplicationProtectedDataDidBecomeAvailable`
- `.UIApplicationProtectedDataWillBecomeUnavailable`

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

```swift
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については、現在調査中ですが、おそらくロックを検知する正規の方法はありません。