문제 해결
리뷰어인 제임스와의 리뷰를 통해 생각해본 내용을 정리해보았다.
다음과 같이 할일을 추가하기위해 present된 뷰의 RightBarButton에 RxCocoa를 사용하여 버튼 탭 이벤트를 연결시켜 주었다.
func setupRightBarButtonItem() -> UIBarButtonItem {
let rightBarButtonItem = UIBarButtonItem(systemItem: .done)
rightBarButtonItem.rx.tap.bind {
self.doneButtonDidTapped()
}.disposed(by: disposedBag)
return rightBarButtonItem
}
func doneButtonDidTapped() {
if isNewTask != nil {
guard let extractedTask = extractCurrentTask() else {
return
}
viewModel?.createTask(to: extractedTask, at: .TODO)
} else {
editButtonDidTapped()
}
self.dismiss(animated: true)
}
우측 Done
버튼을 눌렀을 경우의 이벤트 stream을 바인딩하여 dismiss되도록 하였다.
다만 다음과 같이 셀을 반복해서 클릭할 시
아래처럼 메모리가 해제가 안되는 현상을 발견할 수 있었다.
이 문제의 원인으로 다음과 같이 추측해보았다.
하지만 ViewModel의 문제가 아님을 확인했으므로 Rx관련 코드에서 어떤 문제가 일어나는것 같았으나 정확히 문제를 확인할 수 없었다.
위의 코드에서 UIBarButtonItem
을 래핑한 Rx코드는 다음과 같다
extension Reactive where Base: UIBarButtonItem {
/// Reactive wrapper for target action pattern on `self`.
public var tap: ControlEvent<()> {
let source = lazyInstanceObservable(&rx_tap_key) { () -> Observable<()> in
Observable.create { [weak control = self.base] observer in
guard let control = control else {
observer.on(.completed)
return Disposables.create()
}
let target = BarButtonItemTarget(barButtonItem: control) {
observer.on(.next(()))
}
return target
}
.take(until: self.deallocated)
.share()
}
return ControlEvent(events: source)
}
}
guard 부분을 보면 tap gesture는 create으로 observable을 생성하고 있다.
Observable을 생성하는 방법중에 create과 just 등이 있지만 just가 좀 더 넓은 개념을 포함하고 있다.
final private class Just<Element>: Producer<Element> {
private let element: Element
init(element: Element) {
self.element = element
}
override func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable where Observer.Element == Element {
observer.on(.next(self.element))
observer.on(.completed)
return Disposables.create()
}
}
create은 단지 Observable을 생성하지만, just는 create + onNext + onComplete(또는 onError) + Disposable.create을 포함한다. 반면에 create은 Disposable 타입을 스트림에 넘겨주지 않고있다.
코드를 보면 tap 제스쳐 자체가 onComplete로 이벤트 스트림을 종료시키지 않고 있고, 여기에 바인딩된 클로저가 self.doneButtonDidTapped()
와 같이 TodoAddViewController를 참조하고 있었다.
결과적으로 tap 제스쳐 자체에서 onComplete되지 않으므로 bind의 클로저가 강한참조로 엮여있어 TodoAddViewController가 deinit되지 않은것이었다.
버튼은 대게 일회성이 아닌 여러번 눌림을 상정하고 사용된다.
let target = BarButtonItemTarget(barButtonItem: control) {
observer.on(.next(()))
}
tap 제스쳐 내부의 타겟을 정해주는 부분에서 이부분에서 complete를 시전한다면 어떻게 될까?
아마 1회용 버튼액션이 등록될 것같다. 이벤트가 들어오는걸 rx가 메모리에 올라간 상태로 대기하고 있어야하는데 첫번째 눌리면서 onComplete되어 더이상 이를 수행하지 않기 때문이다.
결국 Tap 제스쳐를 사용하여 bind할 경우 왠만하면 [weak self]를 사용해야 하는것이다.
사실 [weak self]를 rx 사용하는 부분에서 대부분 사용하면 내가 했던 실수는 해결할 수 있다고 한다. 하지만 이런, "왜 쓰는지를 아는 것" 즉 알고 쓰는게 중요하다고 생각했다.