【RxSwift】Reactive の Extension を作成する際にメモリリークが発生した


環境

  • Xcode 12.3
  • RxSwift 5.1.1

目的

  • TabTestViewControllerself.viewDidLayoutSubviews() 呼び出しを検知しその際の self.index を流す Observable<Int> を作成したい
TabTestViewController.swift

// こんな感じで利用する currentIndexDidChange を Extension で実装
self.rx.currentIndexDidChange
    .subscribe(self.indexSubject)
    .disposed(by: self.disposeBag)

当初実装

  • 以下のように実装するとメモリリークが発生する
TabTestViewController+rx.swift

extension Reactive where Base: TabTestViewController {

    var currentIndexDidChange: Observable<Int> {

        // メモリリークが発生
        return sentMessage(#selector(base.viewDidLayoutSubviews))
            .map { _ in base.currentIndex }
            .distinctUntilChanged()
            .share(replay: 1)
    }
}

原因

  • base がクロージャ内部で強参照されていることによる

間違い: self を弱参照しようとした

  • 「クロージャの中では selfを弱参照」と手癖で実装しようとしたところコンパイルエラー
    • Reactive<Base> は構造体のため weak が使えない
TabTestViewController+rx.swift

extension Reactive where Base: TabTestViewController {

    var currentIndexDidChange: Observable<Int> {

        // 'weak' may only be applied to class and class-bound protocol types, not 'Reactive<Base>'
        return sentMessage(#selector(base.viewDidLayoutSubviews))
            .map { [weak self] _ in

                guard let self = self else { fatalError() }

                return self.base.currentIndex
            }
            .distinctUntilChanged()
            .share(replay: 1)
    }
}

解決: base を弱参照とする

  • TabTestViewController はクラスのため weak で弱参照することができる
TabTestViewController+rx.swift
extension Reactive where Base: TabTestViewController {

    var currentIndexDidChange: Observable<Int> {

        // viewDidLayoutSubviewsのタイミングでcurrentIndexの変化を検知する
        return sentMessage(#selector(base.viewDidLayoutSubviews))
            .map { [weak base] _ in

                guard let base = base else { fatalError() }

                return base.currentIndex
            }
            .distinctUntilChanged()
            .share(replay: 1)
    }
}