Swift4環境でTimerの使い方と挙動を確認してみた

# はじめに
こんにちは:leaves:
アプリ内で利用する機会はそれほどでもないのですが、定期実行する[Timer](https://developer.apple.com/documentation/foundation/timer)について調べてみたのでメモとして書いてみたいと思います。(Xcode: 9.1, Swift:4.0.2)
至らぬ点などコメント頂けたら幸いです。

# Timerの基本的な利用
Timerはインスタンス生成即座に起動されるようです。
:warning:ここで注意点なのですが「[fire()](https://developer.apple.com/documentation/foundation/timer/1414035-fire) = 起動」を意味するものではなく、[fire()](https://developer.apple.com/documentation/foundation/timer/1414035-fire)は、Timerインスタンスが破棄されていない状態にて、すぐにスケジューリングしていたメソッドを呼びたい時に使うためのメソッドのようです。

```swift
import UIKit

class MainViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(MainViewController.timerUpdate), userInfo: nil, repeats: true)
    }
    
    @objc func timerUpdate() {
        print("update")
    }
}
//5秒間隔で呼ばれる
//5秒後...
//update
//5秒後...
//update
```

|名前|説明|
|:--|:--|
|timeInterval|ループなら間隔,1度きりなら発動までの秒数|
|target|メソッドを持つオブジェクト|
|selector|実行するメソッド|
|userInfo|オブジェクトに付ける値|
|repeats|繰り返し実行するかどうか|

Timerを**target**の変数(ここではMainViewController)に保持した場合などの色々なパターンを書いてみたいと思います。

# [scheduledTimer](https://developer.apple.com/documentation/foundation/timer/1412416-scheduledtimer)を利用するパターン
scheduledTimerでTimerを生成した場合は、内部的に現在の実行ループ(通常はメインスレッド)にてスケジュール登録されるようです。

## オブジェクトを保持しない

```swift
import UIKit

class MainViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        Timer.scheduledTimer(timeInterval: 5,                                         //ループなら間隔 1度きりなら発動までの秒数
                                target: self,                                         //メソッドを持つオブジェクト
                                selector: #selector(MainViewController.timerUpdate),  //実行するメソッド
                                userInfo: nil,                                        //オブジェクトに付けて送信する値
                                repeats: true)                                        //繰り返し実行するかどうか
    }
    
    @objc func timerUpdate() {
        print("update")
    }
}
//5秒間隔で呼ばれる
//5秒後...
//update
//5秒後...
//update
```

## オブジェクトを保持する

```swift
import UIKit

class MainViewController: UIViewController {
    
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(MainViewController.timerUpdate), userInfo: nil, repeats: true)
    }
    
    @objc func timerUpdate() {
        print("update")
    }
    
    @IBAction func buttonAction(_ sender: Any) {
        print("invalidate!!")
        self.timer?.invalidate()
    }
}
//5秒間隔で呼ばれる
//5秒後...
//update
//5秒後...
//update
//invalidate!!
//.......
```

変数を保持しておくことで[invalidate()](https://developer.apple.com/documentation/foundation/timer/1415405-invalidate)でタイマーを破棄することができ、実質タイマーが停止します。
:warning:Timerインスタンスは破棄されるので、同じインスタンスでの再開は不可能です。再開する場合は破棄した時点の時間を計算するなど、Timerインスタンスを生成し直す必要があります。

---
**変数に対して、重ねてscheduledTimerをした場合**

```swift
import UIKit

class MainViewController: UIViewController {
    
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(MainViewController.timerUpdate), userInfo: nil, repeats: true)
        print(self.timer)
        self.timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(MainViewController.timerUpdate2), userInfo: nil, repeats: true)
        print(self.timer)
    }
    
    @objc func timerUpdate() {
        print("update")
    }
    @objc func timerUpdate2() {
        print("update2")
    }
    
    @IBAction func buttonAction(_ sender: Any) {
        print("invalidate!!")
        self.timer?.invalidate()
    }
}
//Optional(<__NSCFTimer: 0x1c017c500>)
//Optional(<__NSCFTimer: 0x1c017e600>)
//5秒後...
//update
//update2
//5秒後...
//update
//update2
//invalidate!!
//5秒後...
//update
//5秒後...
//update
//invalidate!!
//5秒後...
//update
```

内部的に実行ループに登録されるようで、2回目の上書きでも、`invalidate`後に1回目で生成したTimerが呼び出されてしまいます。

# [Timer.init](https://developer.apple.com/documentation/foundation/timer/1408356-init)を利用するパターン
イニシャライザでTimerを生成した場合は、自ら実行ループ(通常はメインスレッド)にスケジュール登録をする必要があります。

```swift
import UIKit

class MainViewController: UIViewController {
    
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.timer = Timer(timeInterval: 5, target: self, selector: #selector(MainViewController.timerUpdate), userInfo: nil, repeats: true)
        //登録
        RunLoop.main.add(self.timer!, forMode: .defaultRunLoopMode)
    }
    
    @objc func timerUpdate() {
        print("update")
    }
    
    @IBAction func buttonAction(_ sender: Any) {
        print("invalidate!!")
        self.timer?.invalidate()
    }
}
//5秒間隔で呼ばれる
//5秒後...
//update
//5秒後...
//update
//invalidate!!
//.......
```

---

**変数に対して、重ねてinitをした場合**

```swift
import UIKit

class MainViewController: UIViewController {
    
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.timer = Timer(timeInterval: 5, target: self, selector: #selector(MainViewController.timerUpdate), userInfo: nil, repeats: true)
        self.timer = Timer(timeInterval: 5, target: self, selector: #selector(MainViewController.timerUpdate2), userInfo: nil, repeats: true)
        RunLoop.main.add(self.timer!, forMode: .defaultRunLoopMode)
    }
    
    @objc func timerUpdate() {
        print("update")
    }
    @objc func timerUpdate2() {
        print("update2")
    }
    
    @IBAction func buttonAction(_ sender: Any) {
        print("invalidate!!")
        self.timer?.invalidate()
    }
}
//5秒間隔で呼ばれる
//5秒後...
//update2
//5秒後...
//update2
//invalidate!!
//.......
```

こちらは上書きされた後に自らケジュール登録をしたので、2回目のTimerのみが呼ばれます。

# タイマーが呼ばれているタイミング
**iPhone**
> ホームボタンを押して、アプリがバックグランドになると、タイマーのメソッド呼び出しが止まり、再びアクティブになると呼び出しが戻るようです。

**AppleWatch**
> digital crownを押してHomeに戻っても、Timerは呼ばれ続けるようです。任意で止めたい場合は[willActivate()](https://developer.apple.com/documentation/watchkit/wkinterfacecontroller/1619547-willactivate)や[didDeactivate()](https://developer.apple.com/documentation/watchkit/wkinterfacecontroller/1619541-diddeactivate)でコントロールする必要があるみたいです。

# 参考にさせていただいた記事
- [NSTimer の fireって使えないとばっかり思ってたが、使用方法が違ってた。 Qiita](https://qiita.com/YearCentury/items/29f27720caccd6b2a9ce)
- [Apple Reference Timer](https://developer.apple.com/documentation/foundation/timer)

見て頂いてありがとうございます。