なぜDelegateをプロパティに持つとweakを指定しなければいけないの?


はじめに

strongやweakなど所有属性を適当につけると、
メモリリークを起こすと、先輩方によく突っ込まれませんか?
どういうことなのか、整理してみました。

よく言われるケースは、
OutletやDelegate、Closuresなどですかね?
SwiftLintでもワーニングが出ます。
今回は、Delegateを例に説明してみます。

サンプルの説明

1st画面、2nd画面、3rd画面の3画面の構成とします。

1st画面の仕様

ボタンを押下すると、2nd画面へ遷移します。

FirstViewController.swift
import UIKit

final class FirstViewController: UIViewController {

    @IBAction func didTap2ndScene(_ sender: UIButton) {

        let secondVC = UIStoryboard.viewController(storyboardName: "Main",
                                             identifier: "SecondViewController")
        self.navigationController?.pushViewController(secondVC!, animated: true)

    }
}

2nd画面の仕様

3rd画面のインスタンスをプロパティで管理します。
2nd画面から3rd画面のDelegateプロパティに自分自身をセットします。

ボタンを押下すると、3rd画面へ遷移します。

SecondViewController.swift
import UIKit

final class SecondViewController: UIViewController {

    var thirdVC: ThirdViewController?

    @IBAction func didTap3rdScene(_ sender: UIButton) {

        thirdVC = UIStoryboard.viewController(storyboardName: "Main",
                                             identifier: "ThirdViewController")
        if let vc = thirdVC {
            vc.delegate = self
            self.navigationController?.pushViewController(vc, animated: true)
        }
    }
}

extension SecondViewController: ThirdDelegate {

    func completion() {
        print(#function)
    }
}

3rd画面の仕様

delegateをプロパティで管理します。(所有属性を省略し、strongとします。)

ボタンを押下すると、delegateで2nd画面へ通知し、画面を閉じます。

ThirdViewController.swift
import UIKit

protocol ThirdDelegate: class {
    func completion()
}

final class ThirdViewController: UIViewController {

    var delegate: ThirdDelegate?

    @IBAction func didTapGoBack(_ sender: UIButton) {

        delegate?.completion()
        self.navigationController?.popViewController(animated: true)
    }
}

では、どのタイミングでメモリリークが起こるのでしょうか?

正解は、2nd画面を閉じたタイミングでメモリリークを起こします。

理由としては、2nd画面は、3rd画面のインスタンスをstrongで保持しています。
また、3rd画面は、delegateをstrongで保持しています。
つまり、循環参照となり、お互いのオブジェクトを参照している状態になっています。

これによりInstrumentsのLeak Checksでモニタリングしても、メモリリークを検出します。

それでは、どうするか?

3rd画面で管理しているdelegateの所有属性をweakにします。
または、2nd画面で管理している3rd画面のインスタンスをweakにします。
これにより循環参照を防止します。

ThirdViewController.swift
import UIKit

protocol ThirdDelegate: class {
    func completion()
}

final class ThirdViewController: UIViewController {

    weak var delegate: ThirdDelegate?

    @IBAction func didTapGoBack(_ sender: UIButton) {

        delegate?.completion()
        self.navigationController?.popViewController(animated: true)
    }
}

確認したところ、InstrumentsのLeak Checksでモニタリングしても、メモリリークは発生しなくなりました。

まとめ

ARCってよくわからないなという方は、
InstrumentsのLeak Checksでモニタリングしてみると勉強になりそうですよ。

Closuresも考え方は同じです。
該当クラスからクロージャーを参照し、クロージャー内から該当クラスのプロパティを参照しているため、
循環参照が発生し、メモリリークを起こします。

しかしながら、可能であれば変数のスコープを狭くし、ローカル変数で管理すれば、
よりメモリリークを防ぐことができそうですね。