#10 Handling Memory Leak on Back of ViewController - RxSwift MVVM Coordinator iOS App
Rx
및 Coordinator
패턴 사용 시 발생 가능한 메모리 누수 방지Coordinator
패턴 사용 시 발생하는 child
관리 문제 해결protocol Drawable {
var viewController: UIViewController? { get }
}
extension UIViewController: Drawable {
var viewController: UIViewController? { return self }
}
Drawable
이라는 커스텀 프로토콜을 따르도록 선언viewController
변수는 자기 자신을 리턴하는 옵셔널 변수typealias NavigationBackClosure = (() -> ())
protocol RouterProtocol {
func push(_ drawable: Drawable, isAnimated: Bool, onNavigationBack: NavigationBackClosure?)
func pop(_ isAnimated: Bool)
}
Drawable
, 즉 뷰 컨트롤러 또한 해당 파라미터로 들어올 수 있으며 클로저 또한 함께 받기onNavigationBack
에서는 네비게이션 백 버튼이 눌릴 때의 행동 또한 함께 규정 가능final class Router: NSObject {
private let navigationController: UINavigationController
private var closures: [String: NavigationBackClosure] = [:]
init(navigationController: UINavigationController) {
self.navigationController = navigationController
super.init()
self.navigationController.delegate = self
}
}
private
으로 클로저를 딕셔너리로 가지고 있음description
을 통해 해다 클로저를 찾을 수 있음extension Router: RouterProtocol {
func push(_ drawable: Drawable, isAnimated: Bool, onNavigationBack closure: NavigationBackClosure?) {
guard let viewController = drawable.viewController else { return }
if let closure = closure {
closures.updateValue(closure, forKey: viewController.description)
}
navigationController.pushViewController(viewController, animated: isAnimated)
}
func pop(_ isAnimated: Bool) {
navigationController.popViewController(animated: isAnimated)
}
func executeClosures(_ viewController: UIViewController) {
guard let closure = closures.removeValue(forKey: viewController.description) else { return }
closure()
}
}
extension Router: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let previousController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
guard !navigationController.viewControllers.contains(previousController) else {
return
}
executeClosures(previousController)
}
}
didShow
를 통해, 네비게이션이 나타났을 때 이전 뷰 컨트롤러를 찾아낼 수 있음gaurd
문은 더 이상 현재 네비게이션 컨트롤러가 해당 뷰 컨트롤러를 가지고 있지 않음을 다시 한 번 확인하는 코드. 즉 메모리에 남아 있지 않아야 하는 뷰 컨트롤러야만 한다는 뜻class BaseCoordinator: Coordinator {
var childCoordinator: [Coordinator] = []
var isCompleted: (()->())?
func start() {
fatalError("Children should be implemented in start func")
}
}
Coordinator
가 기본으로 따르는 BaseCoordinator
클래스isCompleted
는 onNavigationBack
파라미터에서 구현될 커스텀 클로저, 현재는 널 값override func start() {
let router = Router(navigationController: self.navigationController)
let searchCoordinator = SearchCoordinator(router: router)
self.add(coordinator: searchCoordinator)
searchCoordinator.isCompleted = { [weak self, weak searchCoordinator] in
guard let coordinator = searchCoordinator else { return }
self?.remove(coordinator: coordinator)
}
searchCoordinator.start()
window.rootViewController = navigationController
window.makeKeyAndVisible()
}
AppCoordinator
클래스의 start()
함수add
를 통해 이제 네비게이션 이동에 추가할 자식 coordinator
에 값을 추가한다면, 반대로 백 버튼 등 네비게이션 팝을 했을 때 자식 coordinator
값 또한 제거해야 함isCompleted
클로저에서 해당 뷰 컨트롤러가 팝될 때 현재까지 기록된 child
에서 해당 coordinator
를 제거해주기로 함네비게이션 푸시/팝 등 최하단 부에서 자동으로 관리해주던 영역까지
coordinator
를 통해 관리해주어야 한다. (왜냐하면coordinator
패턴을 따를 때 현 시점에서는 모든 이동 과정 역시 커스텀으로 구현한coordinator
클래스에서 관리해주고 있기 때문이다. 무엇이 우선일까...?