GGURUPiOS
[Swift/Coordinator] coordinator 패턴에서의 childCoordinator 할당 해제 본문
[Swift/Coordinator] coordinator 패턴에서의 childCoordinator 할당 해제
꾸럽 2024. 3. 13. 13:42안녕하세요
최근 개인프로젝트를 진행하면서 적용한 Coordiantor 패턴에 대해 기술하려고 합니다.
일반적으로 구글링을 하다보면 childCoordinator를 부모 Coordinator가 소유하고, 필요에 따라 인스턴스를 찾아 해제하는 방식으로 이루어진 코드를 많이 만나볼 수 있는데요.
뭔가 복잡해보여서 조금 더 찾아보니 좀 더 메모리 관리에 쉽게 적용할 수 있는 방법이 있어 글을 적어봅니다
1. Child - Parent 관계의 Coordinator
protocol Coordinator {
func start()
}
class MainCoordinator: Coordinator {
private var childCoordinators = [Coordinator]()
private var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = ViewController.instantiate()
navigationController.pushViewController(vc, animated: false)
}
}
extension MainCoordinator {
func presentChild(_ child: Coordinator) {
childCoordinators.append(child)
child.onDismissed = { [weak self, weak child] in
guard let self = self,
let child = child else {
return
}
self.removeChild(child)
}
child.start()
}
func removeChild(_ child: Coordinator) {
guard let index = childCoordinators
.firstIndex(where: { $0 === child }) else {
return
}
childCoordinators.remove(at: index)
}
}
위와 같이 코드를 작성했을 때 문제점
위의 코드와 비슷하게 한번이라도 코디네이터 패턴을 써봤다면 아래와 같은 불편함을 겪을 것이다.
- 코디네이터가 시작 될 때 childCoordinator에 수동으로 추가해줘야 함 (deallocated 방지)
- childCoordinator 가 종료되는 시점을 파악해서 제거해줘야 함
여기서 문제가 발생한다
예를들어, 쇼핑몰 앱에서 결제 플로우 코디네이터가 있다고 치자.
유저가 앱을 구경하다 상세페이지에서 결제플로우를 요청하고, 결제 플로우가 특정 버튼으로만 종료되면 파악하기 쉽다.
그렇지만, 유저가 네비게이션의 백버튼을 누르거나, 스와이프로 뒤로가기 요청을 했다고 치자.
그렇다면 그런경우를 다 파악해서 종료시켜줘야 한다.
일일이 파악하기 어렵고, 일일이 각 케이스마다 처리를 해줘야하기 때문에 매우 큰 단점이다.
2. Self-deallocated child coordinators
그렇다면 가장 편하고 이해하기 쉬운 방법은 뭘까요?
아마도 코디네이터가 필요하지 않을 때, 자동으로 그 코디네이터가 종료되면 가장 편할 것입니다.
그것을 위해 아래그림과 같이 참조 체인을 구성하면 됩니다.
- presenter(보통 네비게이션)은 view(VC)를 가지고 있습니다
- viewController는 viewModel에 강한참조
- viewModel 또한 coordinator에 강한참조
- navigation과 coordinator 사이만 약한 참조로 구성하기
위와같이 구성하면 ViewModel의 수명주기와 Cooridantor의 수명주기가 같게됩니다.
원본글과는 조금 다르게, 제가 실제 프로젝트에 적용하면서 바꾼것과 비슷하게 만들었습니다.
Coordinator
coordinator가 presenter를 약한 참조로 소유하게 만들었습니다.
final class DefaultMainCoordinator: MainCoordinator {
// presenter - coordinator Weak Reference
private weak var navigationViewController: UINavigationController?
init(navigationController: UINavigationController) {
self.navigationViewController = UINavigationController
}
func start() { ... }
}
protocol MainCoordinator {
// coordinator actions
// func pushView()
}
최상단 코디네이터에서 navigationViewController는 뷰 계층 구조에서 유지되므로 추가로 reference count를 올릴필요가 없습니다.
따라서 하위 코디네이터들은 weak 으로 선언해도 할당해제되는 문제가 없습니다
ViewModel
뷰모델 역시 coordinator를 강한참조하게 됩니다.
init에서 받아주도록 합시다
final class MainViewModel {
let coordinator: MainCoordinator
init(coordinator: MainCoordinator) {
self.coordinator = coordinator
}
}
사실 여기서 원본글과 조금 다른데, 일단 원본글에서는 protocol MainCoordinator 를 ViewModelDelegate라고 명명합니다.
그렇지만 저는 MainCoordinator의 기능들을 추상화한 것이기 때문에 네이밍을 좀 더 쉽게 하고싶어서 위와같이 작성했습니다.
그리고 원본글에서는, ViewController가 필요한 변수나 접근 메서드들 또한 protocol ViewModelType으로 추상화하는데, 필요에 따라 하시면 될 것 같습니다. (현재는 그냥 추상화 없이 클래스 선언 후 VC에 할당)
즉 중간에 ViewModel 입장에서 2개의 프로토콜을 사용할 수 있습니다.(위에서는 coordinator와의 통신에만 사용)
coordinator와의 통신 -> ViewModelDelegate (위에서는 MainCoordinator)
view와의 통신 -> ViewModelType
ViewController
final class ViewController: UIViewController {
let viewModel: DefaultViewModel
}
뷰컨트롤러(뷰)는 ViewModel을 강한참조 해야합니다. 프로퍼티로 선언해서 init에서 주입해줍니다.
이렇게 함으로써, 뷰가 살아있는 동안 뷰모델을 유지합니다.
장점
코디네이터를 완전히 좌유롭게 관리할 수 있습니다.
스와이프, 뒤로가기, 특정 플로우 종료 같은것들을 고려할 필요 없이 ViewModel이 사라지는 시점에 같이 해제 되므로
관리가 용이 합니다
기존 child-parent 관계에서는 뒤로가기 버튼에 플로우마다 처리를 해줘야 했지만 그럴 필요가 없습니다
단점
뷰 및 뷰 모델의 유지관리가 올바르게 수행되지 않으면 메모리 릭이 발생합니다
-> 뷰가 더 이상 계층구조에 속하지 않더라도 뷰가 해제되지 않으므로 뷰모델과 코디네이터가 유지됨
하지만 위의 단점은 child-parent 관계에서도 마찬가지입니다.
reference
https://medium.com/vptech/coordinator-pattern-in-swift-without-child-coordinators-42d482d15975
'Swift > Architecture' 카테고리의 다른 글
Swift Architecture - Clean Architecture + MVVM (예제) (0) | 2023.04.27 |
---|---|
Swift Architecture - Clean Architecture (0) | 2023.04.27 |
Swift Architecture - MVVM (0) | 2023.04.25 |
Swift Architecture - MVC (0) | 2023.04.25 |