안녕하세요 🙌
최근 진행하고 있는 프로젝트에서 뷰계층에 상관없이 특정화면을 띄울 필요가 있었습니다.
Present를 사용하는 방법도 있지만,
화면이 많아짐에 따라 화면전환과 관련된 비즈니스 로직을 담당하는 객체를 명확하게 지정해줄 필요가 있다고 생각하였습니다.
예를들어 로그인화면에서 로그인을 성공하면 메인 화면으로 이동하고, 실패하면 비밀번호 재설정등과 같은 화면으로 이동하는 것과 같은 로직을 의미합니다.
이러한 로직을 Coordinator라는 객체를 통해서 관리하는 것이 Coordinator패턴입니다.
코디네이터 패턴을 구현하는 방법은 사람마다, 프로젝트마다 조금씩 다르지만
보통 Coodinator는 ViewController를 가지며 start매서드를 사용하여 해당 ViewContoller를 뷰계층에 삽입합니다.
해당 포스팅에서는 제가 사용한 방식에 대해 설명해보겠습니다.
Coordinator
Coordinator는 UINavigationController를 가집니다.
자신이 보유한 ViewController를 해당 네비게이션에 삽입하기 위해서입니다.
popViewController는 네비게이션에서 뷰컨트롤러를 제거합니다.
Coordinator의 뷰컨트롤러가가 현재 최상단에 있다면 해당 화면이 사라지게 됩니다.
따라서 이 함수는 현재 ViewController가 스택의 최상단에 있을 때 호출해야 의도한대로 동작함으로 주의가 필요합니다.
자식 Coordinator
Coordinator는 자식 Coordinator를 가질 수 있습니다.
부모와 자식 Coodinator가 동일한 네비게이션 컨트롤러를 사용한다면,
start를 호출하는 시점이 부모사 항상 자식보다 우선됨으로
쌓여지는 뷰계층과 Coordinator계층은 서로 일치하게됩니다.
이렇게 할 경우 Coordinator와 ViewController를 안전하게 메모리에서 해제실 킬 수 있습니다.
화면이 없는데 Coordinator는 메모리에 존재하는 것과 같은 상황을 효율적으로 예방할 수 있습니다.
너무 많은 Coodinator
ViewContoller와 Coodinator가 1대1 관계를 가진채로 개발을 계속하다보니,
생각보다 너무 많은 Coodinator들이 생성되어 특정타입을 찾는데 혼동이 왔습니다.
그리고 하나의 도메인을 컨트롤하는 Coordinator가 필요하다고 생각하게 되었습니다.
예를들어 회원가입의 경우 번호, 이름 그리고 비밀번호 등을 입력받는데 서로다른 화면에서 진행되며 다른 Coordinator를 사용합니다.
비슷한 종류에 대한 Coodinator들을 하나로 묶어줄 필요가 있었습니다.
그러다 보니 다수의 Coodinator를 보유하는 Coodinator와 단순히 화면 하나를 관리하는 Coodinator
이렇게 2종류로 분류가 되게되었고, 두 타입이 conform하는 타입을 분리하여 분리하였습니다.
두 타입은 ParentCoordinator와 ChildCoordinator입니다.
ParentCoodinator는 자식 Coodinator를 가질 수 있고 관리할 수 있습니다.
ChildCoodinator는 ViewController를 참조하고 Coodinator가 종료됨을 명시할 수 있습니다.
※ ChildCoodinator ViewController를 weak참조합니다. (추후 설명)
ParentCoordinator는 여러 Coordinator를 관리하며
특정화면을 생성하는 매서드를 사용하여 원하는 Coordinator를 생성하고 화면을 네비게이션에 삽입합니다.
아래는 제가 진행하는 프로젝트에서 로그인관련 Coordinator에 정의한 매서드 코드 입니다.
만약에 비밀번호 찾기 화면에서, 새로운 비밀번호 설정으로 이동하려면 어떻게 해야할까요?
이 기능을 구현하기 위해 ChildCoordinator는 부모 Coordinator를 필요에 따라 참조합니다.
코드에서도 해당 참조가 이뤄짐을 확인할 수 있습니다.
그리고 해당 부모를 통해서 기능을 실행합니다.
아래는 자식 Coordinator의 뷰컨트롤러 코드입니다.
뷰컨트롤러는 자신의 Coordinator를 참조하고 이를 통해서 ParentCoordinator의 코드를 호출할 수 있습니다.
계층간 참조가 발생하여 메모리 누수가 발생할 수 있습니다.
Parent - Child구조에서 RC를 확인하면 아래와 같습니다.
그림을 보면 순환참조가 발생함으로, 삭제시 적절하게 순환 고리를 끊어주어야 합니다.
특정 ChildCoordinator의 삭제과정은 다음과 같습니다.
우선 ViewController를 네비게이션에서 pop합니다.(popViewController호출)
그 후 자식 Coordinator를 부모 Coordinator에서 제거합니다. (parent?.removeChildCoordinator)
메모리 누수 없이 모든 객체가 해제되는 것을 확인할 수 있습니다.
여기서 ChildCoorinator가 ViewController weak으로 참조하지 않으면 Coordinator와 ViewController모두 사라지지 않게 됩니다.
모든 자식 Coordinator지우기
화면이 깊어지게되어 뷰계층이 깊이 쌓인 상황에서 한번에 모든 ViewController를 삭제하려면 어떻게 해야할까요?
아래 매서드를 통해서 그 기능이 가능하도록 구현하였습니다.
해당 매서드는 다소 구현이 복잡합니다.
먼저 extension으로 구현된 코드입니다.
func clearChildren() {
print(self, childCoordinators, navigationController.viewControllers)
let lastCoordinator = childCoordinators.popLast()
var middleViewControllers: [UIViewController?] = []
childCoordinators.reversed().forEach { coordinator in
if coordinator is ChildCoordinator {
let child = coordinator as! ChildCoordinator
child.viewControllerRef?.cleanUp()
if let middleViewController = child.viewControllerRef {
middleViewControllers.append(middleViewController)
}
self.removeChildCoordinator(child)
}
}
navigationController.viewControllers = navigationController.viewControllers.filter({
viewController in
!middleViewControllers.contains(where: { $0 === viewController })
})
if lastCoordinator is ParentCoordinator {
(lastCoordinator as! ParentCoordinator).clearChildren()
} else {
if let lastCoordinator {
if lastCoordinator is ChildCoordinator {
(lastCoordinator as! ChildCoordinator).viewControllerRef?.cleanUp()
}
self.removeChildCoordinator(lastCoordinator)
lastCoordinator.popViewController()
}
}
print("종료", self, childCoordinators, navigationController.viewControllers)
}
먼저 기본적으로 아래 그림처럼 작동합니다.
우선 가장 바깥쪽에 있는(현재 ParentCoordinator기준 최상단) Coordinator를 자식 Coordinator배열에서 빼냅니다.
그 후 나머지 자식 Coordinator의 뷰컨트롤러를 모두 네비게이션에서 제외하고, Coordinator도 삭제합니다.
여기서 한가지 분기점이 있습니다.
최상단 Coordinator는 ParentCoordinator혹은 ChildCoordinator일 수 있습니다.
만약 ChildCoordinator일 경우, 해당 Coordinator의 ViewController는 애니메이션을 적용해야 합니다.
Parent일 경우, 재귀적으로 가장 최상단의 ViewController를 탐색합니다.
글을작성하다 보니, 최상단이 아닌 중간단계에 ParentCoordinator가 존재하는 경우도 고려해야 했습니다. 😅
현재까지는 그러한 구조가 없지만 추후에 해당내용을 추가하여 포스팅하겠습니다.
Xcode Instrunments를 활용한 메모리 누수확인
Allocation도구를 사용하면 생성된 인스턴스의 참조횟와 매모리 해제 시점등을 확인할 수 있습니다.
아래 영상의 왼쪽 리스테 타입에 생성되고 화면이 사라짐과 동시에 메모리 해제되는 것을 확인할 수 있습니다.
아래 포스팅은 해당 패턴을 구현하는데 큰도움이 되었습니다.
예제 레포지토리도 있어 이해가 쉽습니다!
'iOS공통' 카테고리의 다른 글
[Swift] GCD와 Swift concurrency에 대해 (0) | 2024.07.22 |
---|---|
[RxSwift] self순환 참조에 대해 (0) | 2024.07.19 |
[iOS] Alamofire request flow (1) | 2024.06.15 |
[Extension] Action Extension, AppGroups과 함께 사용해보기 (0) | 2024.03.27 |
[Swift] Combine 4편: Combine을 사용한 비동기 코드처리 (0) | 2023.09.05 |