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)
}
}
}
'📱🍎 iOS' 카테고리의 다른 글
[iOS] Singleton 싱글톤 패턴이란 무엇이고, 단점과 대안은 무엇인가? (0) | 2023.02.14 |
---|---|
[iOS] DIContainer : 의존성 주입을 위한 객체 관리하기 (0) | 2022.12.05 |
[iOS] Coordinator (2) - 코디네이터 패턴, 예제와 함께 구현하기 (0) | 2022.11.25 |
[iOS] Coordinator (1) - 코디네이터란 무엇인가? 개념 이해하기 (0) | 2022.11.25 |
[iOS] Advanced coordinators in iOS 해석 및 정리 (0) | 2022.11.25 |