Bibi's DevLog 🤓🍎

[iOS - Coordinator] 🛠 childCoordinator 관리를 위해 coordinator - viewcontroller 쌍 만들기 본문

📱🍎 iOS

[iOS - Coordinator] 🛠 childCoordinator 관리를 위해 coordinator - viewcontroller 쌍 만들기

비비 bibi 2022. 11. 28. 17:46

문제

AppCoordinators에서 childCoordinators를 관리한다.

  • 새로운 화면을 띄우기 위해 coordinator와 viewcontroller를 만들고, 생성한 coordinator를 childCoordinators에 추가
  • 기존 화면이 사라지면 UINavigationControllerDelegate를 통해 알림을 받고, childCoordinators에서 해당 viewcontroller를 관리하는 coordinator를 삭제해야 함
    • 여기에서 fromCoordinator를 각 뷰컨으로 하나씩 형변환해 보고, 되면 그 뷰컨을 관리하는 coordinator를 childCoordinators로부터 제거함
    • 각 뷰컨으로 하나씩 형변환하는 데서 중복 코드가 너무 많이 발생함
extension AppCoordinator: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { // navCon이 새 뷰컨을 보여준 직후 호출됨
        // 어떤 뷰컨으로부터 이동했는지 확인
        // MARK: 뭐하는 코드인지 공부하기
        guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
            return
        }

        // navStack에 존재한다면 - pop이 아닌 push된 것이므로 별도의 처리가 필요하지 않음
        if navigationController.viewControllers.contains(fromViewController) {
            return
        }

        // navStack에 존재하지 않음 = pop됨 : 해당 뷰컨의 coordinator를 childCoordinators에서 지워야 함
        // MARK: 모든 뷰컨의 코디네이터 관리하면서도 코드중복 피하는 방법..?
        if fromViewController as? LoginViewController != nil,
            let coordinator: LoginCoordinator = container.resolve() {
            removeChildCoordinator(child: coordinator)
        }
        if fromViewController as? ReposViewController != nil,
            let coordinator: ReposCoordinator = container.resolve() {
            removeChildCoordinator(child: coordinator)
        }
        if fromViewController as? IssueViewController != nil,
            let coordinator: IssueCoordinator = container.resolve() {
            removeChildCoordinator(child: coordinator)
        }
        if fromViewController as? NewIssueViewController != nil,
            let coordinator: NewIssueCoordinator = container.resolve() {
            removeChildCoordinator(child: coordinator)
        }
        if fromViewController as? OptionSelectViewController != nil,
            let coordinator: OptionSelectCoordinator = container.resolve() {
            removeChildCoordinator(child: coordinator)
        }

        print(navigationController.viewControllers)
        print(childCoordinators)
    }
}

계획

  • 뷰컨과 코디네이터 쌍을 관리하는 enum을 만들기로 함
  • 현재 흐름에서는…
    • 코디네이터(AppCoordinator) → 뷰컨(자식coordinator) 순으로 생성된다
    • 각 코디네이터와 뷰컨마다 생성 시점이 다르다 - 다른 뷰컨에서의 선택 결과에 따라 다르게 그려지는 뷰컨이 많기 떄문 (예를 들어 ReposVC → IssueVC)
  • 뷰컨 생성 시점에 쌍이 되는 코디네이터가 존재함
    • 뷰컨 생성 시점에 container를 활용해 뷰컨과 코디네이터를 같이 넣어야 하나? → 일단 이렇게 진행

현재 상태

  • AppCoordinator에서 각 코디네이터 생성 시 그 객체를 컨테이너에 등록
  • 각 코디네이터에서 모델, 뷰컨트롤러 생성 시 그 객체를 컨테이너에 등록
    • 이 때 컨테이너 입장에서 뷰컨을 등록될 때 그 타입이 UIViewController라면
    • 위의 흐름상 관련 코디네이터는 이미 등록되어 있을 것이므로, 뷰컨트롤러-코디네이터 쌍을 따로 저장
    • 이 때 뷰컨트롤러는 UIViewController가 아닌 실제 뷰컨 타입으로, 코디네이터도 Coordinator가 아닌 실제 코디네이터 타입으로 저장
  • 특정 뷰컨이 pop될 때, 그 뷰컨이 어떤 뷰컨인지 판단하고, 저장해 둔 뷰컨트롤러-코디네이터 쌍을 찾아 관련 코디네이터를 삭제
    • 이 때 컨테이너가 특정 뷰컨을 넘겨주면, 그 뷰컨과 쌍인 코디네이터를 반환해 삭제할 수 있도록 해줌

결과 : 어떤 화면으로부터 벗어날 때, 뷰컨트롤러와 관련 코디네이터를 함께 사라지도록 처리함

문제는 코드 관리 위치가 바뀌고 편의성이 조금 높아졌을 뿐, 여전히 반복코드가 남아있다는 것ㅠ

// Container

func registerPair(viewController: UIViewController) {
        // 받아온 UIViewController타입의 뷰컨을 형변환 또는 타입연산 후, 적절한 코디네이터와 함께 저장
        let name = String(describing: type(of:viewController))
        switch viewController { // enum을 적용하고 싶음..
        case is LoginViewController:
            if let coordinator: LoginCoordinator = resolve(),
               let typedViewController = viewController as? LoginViewController {
                registeredViewControllerCoordinator[typedViewController] = coordinator
            }
        case is ReposViewController:
            if let coordinator: ReposCoordinator = resolve(),
               let typedViewController = viewController as? ReposViewController {
                registeredViewControllerCoordinator[typedViewController] = coordinator
            }
        case is IssueViewController:
            if let coordinator: IssueCoordinator = resolve(),
               let typedViewController = viewController as? IssueViewController {
                registeredViewControllerCoordinator[typedViewController] = coordinator
            }
        case is NewIssueViewController:
            if let coordinator: NewIssueCoordinator = resolve(),
               let typedViewController = viewController as? NewIssueViewController {
                registeredViewControllerCoordinator[typedViewController] = coordinator
            }
        case is OptionSelectViewController:
            if let coordinator: OptionSelectCoordinator = resolve(),
               let typedViewController = viewController as? OptionSelectViewController {
                registeredViewControllerCoordinator[typedViewController] = coordinator
            }
        default:
            return
        }
}

func resolvePair(of viewController: UIViewController) -> Coordinator? {
        return registeredViewControllerCoordinator[viewController]
}

해결 : enum을 사용해 개선

  • 뷰컨트롤러 이름을 case로, 코디네이터 이름을 rawValue로 갖는 enum을 생성함
  • case 순회를 위해 CaseIterable 프로토콜 준수
enum ViewControllerCoordinator: String, CaseIterable {
    case LoginViewController = "LoginCoordinator"
    case ReposViewController = "ReposCoordinator"
    case IssueViewController = "IssueCoordinator"
    case NewIssueViewController = "NewIssueCoordinator"
    case OptionSelectViewController = "OptionSelectCoordinator"
}
  • 컨테이너에서 register, resolve할 때 키를 String(describing: type(of: T.self)) 에서 String(describing: T.self) 로 변경
    • 클래스 이름을 그대로 저장하기 위해 이렇게 변경함.
    • 🤔 기존에도 클래스 이름을 그대로 저장하기 위해 String(describing: type(of: T.self)) 를 사용한 건데 왜인지 클래스명 이 아닌 클래스명.Type 으로 저장되고 있었다. 추가 공부 필요!
// Container
// model, viewcontroller, coordinator를 생성 시점에 등록함
func register<T>(_ object: T) {
    let key = String(describing: T.self) // String(describing: type(of: T.self)) 와의 차이점?? 타입명 vs 타입명.Type
    registeredObjects[key] = object
    if let viewControllerObject = object as? UIViewController {
        registerPair(viewController: viewControllerObject)
    }
}

// let value: Type = container.resolve() 로 사용
func resolve<T>() -> T? {
    let key = String(describing: T.self)
    guard let object = registeredObjects[key],
          let object = object as? T else {
        print("⚠️\(key)는 register되지 않음")
        return nil
    }
    return object
}
  • 컨테이너에서 뷰컨트롤러를 받아와, 연관 코디네이터와 함께 저장하는 작업을 한다.
    • 기존 코드(주석처리) 는 is 를 사용해 일일히 존재하는 모든 뷰컨트롤러 타입과 비교해봤다면,
    • 개선한 코드는 enum의 모든 케이스를 순회해 뷰컨트롤러와 이름을 비교하여, 일치하면 그 case의 rawValue인 코디네이터 이름을 가져옴
    • 가져온 코디네이터 이름으로 저장된 코디네이터를 resolve함
    • 그리고 받아온 viewController와 resolve해온 coordinator를 묶어 저장
// Container

func registerPair(viewController: UIViewController) {
        // 받아온 UIViewController타입의 뷰컨을 형변환 또는 타입연산 후, 연관 코디네이터와 함께 저장
        let viewControllerName = String(describing: type(of:viewController))

        let allViewController = ViewControllerCoordinator.allCases
        for oneCase in allViewController {
            let oneCaseName = String(describing: oneCase) // Enum case의 이름을 String으로 변환

            if oneCaseName == viewControllerName {
                let coordinatorName = oneCase.rawValue
                guard let coordinator = registeredObjects[coordinatorName],
                      let castedCoordinator = coordinator as? Coordinator else {
                    return
                }
                registeredViewControllerCoordinator[viewController] = castedCoordinator
            }
        }
}
  • 이를 통해 뷰가 추가될 때 마다 반복해야 했던 중복코드를 모두 없앨 수 있었고,
  • 새 뷰가 추가될 때 ViewControllerCoordinator enum에 case 뷰컨트롤러이름 = "코디네이터이름" 만 추가하면 더이상의 추가 작업이 필요없어짐! 👍

결론적으로 처음 문제가 된, AppCoordinator의 UINavigationControllerDelegate extension에서 자식 코디네이터를 찾아서 삭제하는 코드는 이렇게 깔끔하게 바뀌었다.

extension AppCoordinator: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { // navCon이 새 뷰컨을 보여준 직후 호출되어, 어떤 뷰컨으로부터 이동했는지 확인 가능
        guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
            return
        }

        if navigationController.viewControllers.contains(fromViewController) {
            return
        }

        if let coordinator = container.resolvePair(of: fromViewController) {
            removeChildCoordinator(child: coordinator)
        }
    }
}
private func removeChildCoordinator(child: Coordinator) {
    for (index, coordinator) in childCoordinators.enumerated() {
        if coordinator === child { // 참조 비교
            childCoordinators.remove(at: index)
        }
    }
}