【Swift】加算型Timer（ストップウォッチみたいな）の作り方 〜Timerの基本的な使い方〜

下記ブログの転載です
https://rc-code.info/ios/post-167/


Swiftにてストップウォッチの様な **加算型のTimer** を作りたかったので、備忘録。
今回は **`Timer`** の **`scheduledTime関数`** を利用して、加算型Timerを作成します。

## Timer.scheduledTimer のサンプル

まずは先に全体のサンプルを。。

```swift:
final class ViewController {

    private var timer: Timer?                           // Timerを保持する変数
    private(set) var timerTotalDuration: TimeInterval = 0    // 総秒数を保持する変数

    func startTimer() {
        // ⑤ Timer のスケジューリング重複を回避
        guard timer == nil else { return }

        // ① Timerのスケジューリングと保持
        timer = Timer.scheduledTimer(
            timeInterval: 5,
            target: self,
            selector: #selector(handleTimer(_:)),
            userInfo: nil,
            repeats: true
        )
    }

    func stopTimer() {
        // ③ Timer のスケジューリングを破棄
        timer?.invalidate()
    }

    func resetTimer() {
        // ④ Timer のスケジューリングを破棄し、総秒数を0に戻す 
        stopTimer()
        timerTotalDuration = 0
    }

    @objc private func handleTimer(_ timer: Timer) {
        // ② Timer の間隔秒を総秒数に加算する
        timerTotalDuration += timer.timeInterval
        print("timer fired. total: \(timerTotalDuration)")
    }
}
```
&nbsp;

## 各箇所を詳しく説明

### ① Timer のスケジューリングと保持

```swift:
timer = Timer.scheduledTimer(
    timeInterval: 5,
    target: self,
    selector: #selector(handleTimer(_:)),
    userInfo: nil,
    repeats: true
)
```

**`scheduledTi関数`** は下記の引数を指定して、実行します。

引数名 | 指定する要素
------------ | -------------
timeInterval | タイマーが発火する秒数を指定
target | selector に支持する関数を持っているインスタンス指定
selector | 実行する関数を指定
userInfo | 関数に引き渡したいDictionaryを指定
repeats | 繰り返しをするか否かを指定

実行すると **`Timer`** が生成されて、**自動的にスケジューリングされ動き始めます**。
**`timeInterval`** で指定した時間がすぎると、**`selector`** で指定した関数が実行されます。
**`scheduledTimer関数`** は実行されると**生成した `Timer`**を返却するので、上記の例だと **`var timer`** に **`Timer`** が格納されます。
&nbsp;

### ② Timer の間隔秒を総秒数に加算する

```swift:
@objc private func handleTimer(_ timer: Timer) {
    timerTotalDuration += timer.timeInterval
    print("timer fired. total: \(timerTotalDuration)")
}
```

今回は加算型のタイマーを作るのが目的なので、クラスが持つ **`var timerTotalDuration`** に経過した時間を加算していきます。
**`timeInterval`** に **`5`** を設定していたら5秒毎に **`handleTimer関数`** が実行され、**`timer.timeInterval`** に格納されている **`5`** が **`timerTotalDuration`** に加算されていきます。
&nbsp;

### ③ Timer のスケジューリングを破棄

```swift:
timer?.invalidate()
```

今回はストップウォッチのように **タイマーを一時停止** できるようにします。
ですが、 **`Timer`** のスケジュール自体をストップすることは出来ず、**invalidate関数 で一度 `Timer` 自体を破棄**しなければいけません。
再度タイマーを動かしたい時には **`scheduledTimer関数`** をもう一度実行して、**同じ設定の `Timer` を作り直す**必要があります。

上記サンプルでは、**`startTimer()`** で **`Timer`** が生成され、**`stopTimer()`** で破棄されます。
ですが経過した秒数は **`timerTotalDuration`** に保持してあるので、再開したい場合は **`startTimer()`** を実行し、**新しい `Timer`** で計測を再開します。
&nbsp;

### ④ Timer のスケジューリングを破棄し、総秒数を0に戻す

```swift:
stopTimer()
timerTotalDuration = 0
```

この箇所はいわゆる**初期化**になります。
**`stopTimer関数`** で **`Timer`** を破棄（ストップ）し、加算していた総秒数を **`0`** に戻します。
これで、再度 **`startTimer関数`** が実行された際には **`0`** からのカウントになります。
&nbsp;

### ⑤ Timer のスケジューリング重複を回避

```swift:
guard timer == nil else { return }
```

最後に⑤の箇所になりますが、この箇所は **`Timer` を利用する際に気をつけたい部分** となります。

**`scheduledTimer関数`** 実行前に、**`var timer`** に **`Timer`** が格納されていないかチェックしていますが、これは**必ず行った方が良い判定です**。
というのも、**`scheduledTimer関数`** を実行すれば **`Timer`** をいくらでも生成できてしまい、**その都度新しい `Timer` が返却されてしまう**からです。

例として下記のように実行した場合、**参照を無くしたタイマーが発火し続ける**事になります。

```swift:
// 一つ目のタイマーを生成
var timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)

// 二つ目のタイマーを生成（timerに格納されていた一つ目のタイマー参照は、二つめのタイマー参照に上書きされる）
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)

// 二つのタイマーが同時に走り続ける

// timer をRunLoopのスケジュールから破棄する（二つ目のタイマーが破棄される）
timer?.invalidate()

// timerが保持していたのは二つ目のタイマーだったので、一つ目は破棄されずに発火し続ける
```

なので、必ず破棄していない **`Timer`** への参照を失わないようにすべきです。

更に注意すべきなのは。**`scheduledTimer関数`**に指定する **`target`** によっては**循環参照になる**ので、**`Timer`** を破棄しない限り、**`target`** に指定したインスタンスも**解放されなくなります**。

```swift:
// ViewControllerに var timer を持たせる
var timer: Timer?

// scheduledTimer関数の target に self を指定する（Timerがselfの参照をもつ）
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)

// この時点で self は Timer の参照を持ち、Timer は self の参照を持つ（ = 循環参照 ）

// timer を削除する（二つ目のタイマーが破棄される）
timer?.invalidate()

// この時点で循環参照が解除される
```

つまり、**`Timer`** の参照はしっかり管理し、**`target`** に指定しているインスタンスが破棄されるべきタイミングで、必ず **`invalidate関数`** を呼ばなければなりません。
そういったリスクを踏まえ、⑤の処理は必ず入れておく事をおすすめします。
&nbsp;

## 合わせて注意したい事

<a href="https://rc-code.info/ios/post-172/" rel="noopener" target="_blank">【Swift】Timerがズレる (遅延する) 時の原因と対処法</a>
<a href="https://rc-code.info/ios/post-184/" rel="noopener" target="_blank">【Swift】Timerの強参照が破棄できない時の対処法（deinit での破棄方法）</a>
&nbsp;

## Info. ドキュメント

<a href="https://developer.apple.com/documentation/foundation/timer" target="_blank" rel="noopener" shape="rect">公式ドキュメント</a>
&nbsp;

## Info. サンプルテスト環境

Mac OS: 10.13.6
Xcode: Version 10.0
&nbsp;