[RxSwift] AirPortClone: Handling Memory Leak

Junyoung Park·2022년 12월 28일
0

RxSwift

목록 보기
22/25
post-thumbnail

#10 Handling Memory Leak on Back of ViewController - RxSwift MVVM Coordinator iOS App

AirPortClone: Handling Memory Leak

구현 목표

  • RxCoordinator 패턴 사용 시 발생 가능한 메모리 누수 방지

구현 태스크

  • 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 클래스
  • isCompletedonNavigationBack 파라미터에서 구현될 커스텀 클로저, 현재는 널 값
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 클래스에서 관리해주고 있기 때문이다. 무엇이 우선일까...?

profile
JUST DO IT

0개의 댓글