LoginSignup
2
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2018-12-19

下記ブログの転載です。
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
 

2
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6