下記ブログの転載です。
https://rc-code.info/ios/post-184/
iOSにて、Timer
をライフサイクルから破棄する事が不可能な場合において、deinitで破棄する方法を備忘録。
Timer
はそれ自体を保持しているクラスをdelegateとして持つ事が多いと思います。
この場合循環参照になるので明示的に破棄しなければなりませんが、willDisappear
などのライフサイクル系delegateを利用できない場合に、deinit
にて破棄できるようにした参考例です。
Timer.scheduledTime関数 の使い方
Timer.scheduledTime関数
の基本的な使い方は下記にまとめましたので、ご参考ください。
【Swift】加算型Timer(ストップウォッチみたいな)の作り方 〜Timerの基本的な使い方〜
Timer の破棄タイミングがない状況
通常 ViewController
などで Timer
を利用するのであれば、willDisappear
あたりのdeleageで破棄すれば良いかと思います。
ですが、こうしたライフサイクル系のdelegateが充実していないクラスや、複雑な処理を組んでおり deinit<
タイミングでのみ破棄を行いたい場合などあるかと思います。
しかし、Timer
が循環参照に陥っている限り、そのクラスの deinit
は走りません。
したがって、
「複雑な処理を行なっているから、ViewControllerが破棄されるタイミングでTimerも破棄したいのにー!!!」
という願いが叶わなくなります。
コードで説明するとこんな感じです。
final class ViewController {
private var timer: Timer? // Timerを保持する変数
func startTimer() {
timer = Timer.scheduledTimer(
timeInterval: 5,
target: self, // ←ここで self を渡している
selector: #selector(handleTimer(_:)),
userInfo: nil,
repeats: true
)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 画面遷移とかでは破棄したくないので、ここでは Timer を破棄できない。。
// かといって、外から破棄するタイミングもない。。。
// そうだ!Σ(☆∀☆〃) キラーン!!
// deinit で破棄すれば確実じゃないか!
}
deinit {
// 上記で timer を保持する viewController を timer が保持しているので、循環参照になります
// self が循環参照に陥っているので、deinit が呼ばれない。
}
}
Timer を deinit で破棄する方法
上記の通り、原因は Timer
の循環参照にあるので、これを回避すれば deinit
は呼ばれるようになるわけです。
そこで Timer
を保持するクラスを別途作成し、プロパティではそのクラスを持つ ようにします。
そうする事で、Timer保持クラス
をプロパティで持つクラスは deinit
が走るので、そのタイミングで Timer保持クラス
が持つ Timer
を破棄してあげれば良いわけです。
コードで書くとこんな感じです。
final class ViewController {
// MARK: - internal class
class LeakAvoider: NSObject {
private weak var delegate: ViewController?
fileprivate weak var timer: Timer?
init(_ delegate: ViewController) {
self.delegate = delegate
}
fileprivate func startTimer() {
guard timer == nil else { return }
timer = Timer.scheduledTimer(
timeInterval: 1,
target: self,
selector: #selector(handleTimer(_:)),
userInfo: nil,
repeats: true
)
}
fileprivate func stopTimer() {
timer?.invalidate()
timer = nil
}
@objc private func handleTimer(_ timer: Timer) {
delegate?.handleTimer(timer)
}
}
private lazy var leakAvoider: LeakAvoider! = {
return LeakAvoider(self)
}()
deinit {
leakAvoider?.stopTimer()
}
func startTimer() {
leakAvoider.startTimer()
}
// Timer からの handler
fileprivate func handleTimer(_ timer: Timer) {
print("Timer fired: \(timer.timerInterval)")
}
}
こちらの構成であれば、クラスの deinit
を起点に Timer
を破棄する事ができると思います。
破棄タイミングに困ったら、こちらをお試しくださいまし!
参考ドキュメント
公式RunLoopドキュメント
公式RunLoop.Modeドキュメント
公式ThreadProgrammingGuide