【Swift】Timerの強参照が破棄できない時の対処法（deinit での破棄方法）

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


iOSにて、**`Timer`** を**ライフサイクルから破棄する事が不可能な場合**において、**deinitで破棄する方法**を備忘録。
**`Timer`** はそれ自体を保持しているクラスをdelegateとして持つ事が多いと思います。
この場合**循環参照になるので明示的に破棄しなければなりません**が、**`willDisappear`** などのライフサイクル系delegateを利用できない場合に、**`deinit`** にて破棄できるようにした参考例です。
&nbsp;

## Timer.scheduledTime関数 の使い方
**`Timer.scheduledTime関数`** の基本的な使い方は下記にまとめましたので、ご参考ください。
<a href="https://rc-code.info/ios/post-167/" target="_blank" rel="noopener">【Swift】加算型Timer（ストップウォッチみたいな）の作り方 〜Timerの基本的な使い方〜</a>
&nbsp;

## Timer の破棄タイミングがない状況
通常 **`ViewController`** などで **`Timer`** を利用するのであれば、**`willDisappear`** あたりのdeleageで破棄すれば良いかと思います。
ですが、こうしたライフサイクル系のdelegateが充実していないクラスや、複雑な処理を組んでおり **`deinit<`** タイミングでのみ破棄を行いたい場合などあるかと思います。

しかし、**`Timer`** が循環参照に陥っている限り、そのクラスの **`deinit`** は走りません。
したがって、

「複雑な処理を行なっているから、ViewControllerが破棄されるタイミングでTimerも破棄したいのにー！！！」

という願いが叶わなくなります。

コードで説明するとこんな感じです。

```swift:
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 が呼ばれない。
    }
}
```
&nbsp;

## Timer を deinit で破棄する方法
上記の通り、原因は **`Timer`** の循環参照にあるので、これを回避すれば **`deinit`** は呼ばれるようになるわけです。

そこで **`Timer` を保持するクラスを別途作成し、プロパティではそのクラスを持つ** ようにします。
そうする事で、**`Timer保持クラス`** をプロパティで持つクラスは **`deinit`** が走るので、そのタイミングで **`Timer保持クラス`** が持つ **`Timer`** を破棄してあげれば良いわけです。

コードで書くとこんな感じです。

```swift:
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`** を破棄する事ができると思います。
破棄タイミングに困ったら、こちらをお試しくださいまし！
&nbsp;

## 参考ドキュメント
<a href="https://developer.apple.com/documentation/foundation/runloop" target="_blank" rel="noopener">公式RunLoopドキュメント</a>
<a href="https://developer.apple.com/documentation/foundation/runloop/mode" target="_blank" rel="noopener">公式RunLoop.Modeドキュメント</a>
<a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html" target="_blank" rel="noopener">公式ThreadProgrammingGuide</a>
&nbsp;