下記ブログの転載です
https://rc-code.info/ios/post-167/
Swiftにてストップウォッチの様な 加算型のTimer を作りたかったので、備忘録。
今回は Timer
の scheduledTime関数
を利用して、加算型Timerを作成します。
Timer.scheduledTimer のサンプル
まずは先に全体のサンプルを。。
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)")
}
}
各箇所を詳しく説明
① Timer のスケジューリングと保持
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
が格納されます。
② Timer の間隔秒を総秒数に加算する
@objc private func handleTimer(_ timer: Timer) {
timerTotalDuration += timer.timeInterval
print("timer fired. total: \(timerTotalDuration)")
}
今回は加算型のタイマーを作るのが目的なので、クラスが持つ var timerTotalDuration
に経過した時間を加算していきます。
timeInterval
に 5
を設定していたら5秒毎に handleTimer関数
が実行され、timer.timeInterval
に格納されている 5
が timerTotalDuration
に加算されていきます。
③ Timer のスケジューリングを破棄
timer?.invalidate()
今回はストップウォッチのように タイマーを一時停止 できるようにします。
ですが、 Timer
のスケジュール自体をストップすることは出来ず、invalidate関数 で一度 Timer
自体を破棄しなければいけません。
再度タイマーを動かしたい時には scheduledTimer関数
をもう一度実行して、同じ設定の Timer
を作り直す必要があります。
上記サンプルでは、startTimer()
で Timer
が生成され、stopTimer()
で破棄されます。
ですが経過した秒数は timerTotalDuration
に保持してあるので、再開したい場合は startTimer()
を実行し、新しい Timer
で計測を再開します。
④ Timer のスケジューリングを破棄し、総秒数を0に戻す
stopTimer()
timerTotalDuration = 0
この箇所はいわゆる初期化になります。
stopTimer関数
で Timer
を破棄(ストップ)し、加算していた総秒数を 0
に戻します。
これで、再度 startTimer関数
が実行された際には 0
からのカウントになります。
⑤ Timer のスケジューリング重複を回避
guard timer == nil else { return }
最後に⑤の箇所になりますが、この箇所は Timer
を利用する際に気をつけたい部分 となります。
scheduledTimer関数
実行前に、var timer
に Timer
が格納されていないかチェックしていますが、これは必ず行った方が良い判定です。
というのも、scheduledTimer関数
を実行すれば Timer
をいくらでも生成できてしまい、その都度新しい Timer
が返却されてしまうからです。
例として下記のように実行した場合、参照を無くしたタイマーが発火し続ける事になります。
// 一つ目のタイマーを生成
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
に指定したインスタンスも解放されなくなります。
// 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関数
を呼ばなければなりません。
そういったリスクを踏まえ、⑤の処理は必ず入れておく事をおすすめします。
合わせて注意したい事
【Swift】Timerがズレる (遅延する) 時の原因と対処法
【Swift】Timerの強参照が破棄できない時の対処法(deinit での破棄方法)
Info. ドキュメント
Info. サンプルテスト環境
Mac OS: 10.13.6
Xcode: Version 10.0