バックグラウンド・スリープ時にもカウントダウンを動作させる

# やりたいこと
任意の時間からカウントダウンして、時間切れになったら任意の処理を行いたい。

## カウントダウン実装
今回はmm:ssの形でのカウントダウンを画面上に表示して、00:00になったら任意の処理を行うという実装です。

```swift:countDownContoroller.swift
import UIKit

class countDownContoroller: UIViewController {
    
    @IBOutlet weak var countDownLabel: UILabel!
    
    var timer = Timer()
    var time = 600 //10分間
    
    override func viewDidLoad() {
        super.viewDidLoad()
        countDownStart()
    }

    func countDownStart(){
        timerStart()
    }
    
    func timerStart(){
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
            self.time -= 1
            
            let formatter = DateFormatter()
            formatter.dateFormat = "mm:ss"
            
            let targetDate = Date(timeIntervalSinceReferenceDate: TimeInterval(self.time))
            let str = formatter.string(from: targetDate)
            self.countDownLabel.text = String(str)
            
            if self.time <= 0 {
                timer.invalidate()
                //カウントダウンが終了した時の処理
            }
        })
    }

}

```
これでmm:ssの形でカウントダウンすることは可能です。

ただ、このまま実装してしまうとアプリを表示している間にでないとタイマーが動かない問題があります。
バックグラウンド状態からアプリを復帰した場合にタイマーが再起動する感じですね。
バックグラウンド中の時間は考慮されません。

今回の現象の原因としてiPhoneはバックグランドにアプリの動作を停止させるのが原因です。
バッテリーなどのリソースのためだと思われます。

ただバックグラウンドでもアプリの処理を止めないようにするには
「TARGET」→「Signing & Capabilities」→「＋Capabilities」→「Background Modes」を選び、必要なチェックを入れればバックグラウンドでも処理が動きます。
![スクリーンショット 2022-06-24 10.36.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/683204/3d392ceb-8e3e-d5a8-fdaf-357598e05c2c.png)
しかし、この方法はAppleからのリジェクト対象になることがあり、今回のカウントダウンタイマーの実装には使用しない方がいいと思われます。

## 対処した内容
では、バックグランド処理をせずにカウントダウンを動かすにはどうすれば良いのでしょうか。

今回は「残り時間」 - 「バックグランドにいた時間」でバックグランド分の経過時間を考慮したカウントダウンを実装します。

### バックグランド・フォアグラウンドの検知
上記の計算をするにはバックグラウンドとフォアグラウンドの検知をしなければなりません。
[【Xcode/Swift】バックグランド・フォアグラウンド状態を検知・判定する方法](https://ios-docs.dev/backgournd-foreground/)
こちらのサイトを参考にしています。

iOS13以降を対象にしたプロジェクトならSceneDelegate.swiftを利用することができます。
SceneDelegate.swiftはプロジェクトを作成したときに自動生成されます。
その中のsceneWillEnterForegroundがフォアグラウンドの検知、sceneDidEnterBackgroundがバックグラウンドの検知です。これらを使っていけば良いですね。

```swift:SceneDelegate.swift
func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }
```

またiOS12以下を対象に作成されたプロジェクトだとSceneDelegate.swifは生成されません。
そのためNotificationCenterを利用します。

ControllerのviewDidLoad()内に以下を追記します。
```swift
NotificationCenter.default.addObserver(self,selector: #selector(foreground(notification:)),name: UIApplication.willEnterForegroundNotification,object: nil)
NotificationCenter.default.addObserver(self,selector: #selector(background(notification:)),name: UIApplication.didEnterBackgroundNotification,object: nil)
```
また、通知を受信した時に行う処理を記載します。
```swift
@objc func foreground(notification: Notification) {
    //処理
}
@objc func background(notification: Notification) {
    //処理
}
```

今回はNotificationCenterを利用して実装していきます。

### バックグラウンドの経過時間の算出
上記の方法でバックグランド・フォアグラウンドの検知ができてしまえばあとは簡単です。
まずはバックグランドにした時の日時を取得します。
それと今動いているタイマーは停止します。
```swift
@objc func background(notification: Notification) {
    //バックグランドにした瞬間の日時を取得
    backgroundDay = Date()
    //一旦止める
    timer.invalidate()
}
```
今度はフォアグラウンドにした瞬間の処理を書いていきます。
フォアグラウンドにした瞬間の日時を取得し、フォアグラウンドにした日時からバックラウンドにした日時を引きます。
その結果をタイマーで途中までカウントダウンされていたものから引きます。
```swift
@objc func foreground(notification: Notification) {
     //フォアグラウンドにした瞬間の日時を取得
     foregroundDay = Date()
     //フォアグラウンドにした日時とバックグラウンドにした日時の差を「秒」で算出
     let span = foregroundDay.timeIntervalSince(backgroundDay)
     //途中まで数えていたカウントダウンタイマーから上の差を引く
     time = Int(Double(time) - ceil(span))
     //カウントダウンタイマー起動
     timerStart()
}
```
フォアグラウンドとバックグラウンドの差は1.123456...のように小数点も取ってきてしまうため、切り捨て/切り上げなどやってあげた方がいいですね。今回は切り上げの「ceil」を用いています。

以上でバックグランド・スリープから復帰した時バックグラウンド中の時間をカウントダウンに反映させることが可能です。

### コード全体
```swift:countDownContoroller.swift
import UIKit

class countDownContoroller: UIViewController {
    
    @IBOutlet weak var countDownLabel: UILabel!
    
    var timer = Timer()
    var time = 600 //10分間
    var backgroundDay = Date()
    var foregroundDay = Date()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        countDownStart()
    }

    func countDownStart(){
        timerStart()
    }
    
    func timerStart(){
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
            self.time -= 1
            
            let formatter = DateFormatter()
            formatter.dateFormat = "mm:ss"
            
            let targetDate = Date(timeIntervalSinceReferenceDate: TimeInterval(self.time))
            let str = formatter.string(from: targetDate)
            self.countDownLabel.text = String(str)
            
            if self.time <= 0 {
                timer.invalidate()
                //カウントダウンが終了した時の処理
            }
        })
    }
    
    //バックグラウンドとフォアグラウンドを検知し、残り時間を再計算する
    func backgroundTimer(){
        NotificationCenter.default.addObserver(self,selector: #selector(foreground(notification:)),name: UIApplication.willEnterForegroundNotification,object: nil)
        NotificationCenter.default.addObserver(self,selector: #selector(background(notification:)),name: UIApplication.didEnterBackgroundNotification,object: nil)
    }
    
    @objc func background(notification: Notification) {
        //バックグランドにした瞬間の日時を取得
        backgroundDay = Date()
        //一旦止める
        timer.invalidate()
    }
    
    @objc func foreground(notification: Notification) {
         //フォアグラウンドにした瞬間の日時を取得
         foregroundDay = Date()
         //フォアグラウンドにした日時とバックグラウンドにした日時の差を「秒」で算出
         let span = foregroundDay.timeIntervalSince(backgroundDay)
         //途中まで数えていたカウントダウンタイマーから上の差を引く
         time = Int(Double(time) - ceil(span))
         //カウントダウンタイマー起動
         timerStart()
    }

}
```

## 参考記事
[iOS-Docs 【Xcode/Swift】バックグランド・フォアグラウンド状態を検知・判定する方法](https://ios-docs.dev/backgournd-foreground/)