본문 바로가기
🍎 iOS

[Swift] MVVMC 자세히 알아보기 - 튜토리얼 4 (child coordinator 삭제하기)

by 틴디 2022. 9. 5.
728x90
반응형

2022.07.10 - [🍎 iOS/Architecture Pattern] - [Swift] MVVMC 자세히 알아보기 - 튜토리얼 1 (Coordinator와 AppCoordinator 구현하기)

2022.07.11 - [🍎 iOS/Architecture Pattern] - [Swift] MVVMC 자세히 알아보기 - 튜토리얼 2 (childCoordinator로 이동하기)

2022.07.12 - [🍎 iOS/Architecture Pattern] - [Swift] MVVMC 자세히 알아보기 - 튜토리얼 3 (버튼 눌러서 화면 전환하기)

해당 튜토리얼은 위 글과 이어집니다!

 

https://www.pexels.com/ko-kr/photo/12499889/

이전 포스팅에서 Coordinator와 화면 전환에 대해 다루어 보았습니다. MVVMC에서 View와 Coordinator에 대해 중점적으로 다루다 보니 ViewModel과 Model, Service에 대한 내용은 포함되어 있지 않습니다 🥲

 

이번 포스팅에서는 이전 포스팅에서 child coordinator로 이동시 child에 추가해 준 coordinator을 삭제하는 방법에 대해 알아보도록 하겠습니다!


현재까지 구현된 구조는 이렇습니다.

로그인 버튼을 누르면 landingCoordinator에서 loginCoordinator을 child coordinator로 추가하고 로그인 화면으로 화면 이동 합니다. 

로그인 화면에서 x 버튼을 눌러 다시 랜딩 화면으로 돌아올 때 어떻게 하면 landingCoordinator의 child Coordinator인 loginCoordinator을 삭제 할 수 있을까요? 

 

viewController의 life cycle에는 viewWillDisappear가 있습니다. 여기에서 트리거를 발생시켜주면 어떨까요?

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
    }

viewWillDisappear를 사용하면 아래와 같은 상황이 발생하게 됩니다. 

 

- 해당 화면 위에 다른 화면이 덮이는 경우 위 라이프사이클이 호출된다 

👉🏻 로그인 화면 A에서 로그인 관련 사항을 안내해 주는 B화면으로 이동하는 플로우가 있다고 합시다! 같은 Scene이기 때문에 coordinator가 종료되지 않았음에도 화면이 덮이면서 (fullScreen의 경우) 위 함수가 호출됩니다. 따라서 Coordinator가 끝나기도 전에 childCoordinator에서 삭제가 됩니다.

 

라이프사이클을 사용하지 않고, protocol을 작성해서 delegate로 child coordinator을 삭제하는 방법을 사용해 보려고 합니다 (구현하는 사람마다 다양한 방법이 존재합니다. 이 방법이 정답은 아닙니다!)

 

protocol LoginViewDelegate {
    func finishLogin()
}

protocol을 정의해 줍니다. finishLogin은 로그인 성공이 아니라 Login Scene을 끝내는 함수입니다. 

 

LoginViewController.swift로 이동한 뒤 LoginViewDelegate 프로퍼티를 정의해 줍니다.

class LoginViewController: UIViewController {
    weak var viewModel: LoginViewModel?
    var delegate: LoginViewDelegate?
    // .. (생략) ...
    
 }

기존에 작성해 둔 UI 코드에 closeButton이 있을텐데요, 여기에 addTarget 해주겠습니다!

    @objc func finishLogin() {
        delegate?.finishLogin()
    }

버튼이 눌렸을 때 실행될 함수를 작성한 뒤

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        let closeButton = UIButton(frame: CGRect(x: 20, y: view.safeAreaInsets.top + 80, width: 30, height: 30))
        view.addSubview(closeButton)
        closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
        closeButton.addTarget(self, action: #selector(finishLogin), for: .touchDown)
		// ... (생략) ...
    }

closeButton에 addTarget 시켜 줍니다. 혹은 아래와 같이 addAction을 사용해서 구현해 주셔도 됩니다

        let closeButton = UIButton(frame: CGRect(x: 20, y: view.safeAreaInsets.top + 80, width: 30, height: 30))
        view.addSubview(closeButton)
        closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
        closeButton.addAction(UIAction(handler: { _ in
            self.delegate?.finishLogin()
        }), for: .touchUpInside)

(깔끔-)

이제 닫기 버튼을 누르면 delegate의 finishLogin을 호출하게 되고 로그인 화면에서 랜딩화면으로 이동해 주면 됩니다. 그렇다면 랜딩화면에서 로그인 화면으로 이동 시켜 줬던 LoginCoordinator가 LoginViewModel를 따르면 되겠죠?

extension LoginCoordinator: LoginViewDelegate {
    func finishLogin() {
        guard let loginNavigationController = loginNavigationController else { return }
        loginNavigationController.dismiss(animated: true) {
            self.finish()
        }
    }
}

현재 loginViewController를 present 하고 있는 loginNavigationController에서 dismiss 하고 그 뒤 finish를 호출합니다. 

finish는 childCoordinator을 삭제하는 로직을 담는데 Coordinator 프로토콜을 따르는 경우 extension 으로 구현해 놓은 finish 함수가 호출됩니다. 따라서 오버라이드 해서 사용해 주어야 합니다. 아래 코드를 LoginCoordinator의 start() 함수 아래 작성해 줍니다

    func start() {
        let loginViewModel = LoginViewModel()
        loginViewController.viewModel = loginViewModel
        
        loginNavigationController = UINavigationController(rootViewController: loginViewController)
        loginNavigationController?.modalPresentationStyle = .fullScreen
        guard let loginNavigationController = loginNavigationController else {
            return
        }
        
        navigationController.present(loginNavigationController, animated: true)
    }
    
    func finish() {
        
    }

랜딩 코디네이터에서 로그인 코디네이터로 이동했으므로 랜딩 코디네이터에서 로그인 코디네이터를 삭제하는 코드를 작성해 주어야 합니다. 아래 프로토콜을 정의해 줍니다.

protocol LandingCoordinatorDelegate {
    func didFinish(from coordinator: Coordinator)
}

Coordinator 타입을 받기 때문에 랜딩 화면에서 로그인 코디네이터, 회원 가입 코디네이터가 다시 랜딩 화면으로 돌아 왔을 때 여기에 각 child coordinator를 전달해 주면 됩니다.

LandingCoordinator의 extension으로 구현해 줍니다

extension LandingCoordinator: LandingCoordinatorDelegate {
    func didFinish(from coordinator: Coordinator) {
        removeChildCoordinator(coordinator)
        debugPrint(childCoordinators)
    }
}

Coordinator 타입을 전달 받아 childCoordinaor에서 해당 coordinator을 삭제합니다. 삭제 관련 로직은 이전 포스팅에 작성되어 있습니다!

이제 didFinish를 호출하면서 Coordinator 자신을 전달해 주면 됩니다. 다시 LoginCoordinator로 이동합니다. 프로퍼티를 선언해 주세요

var delegate: LandingCoordinatorDelegate?

이전에 작성해 둔 LoginCoordinator의 finish 함수에 코드를 작성해 주고 loginViewController의 delegate에 LoginCoordinator을 전달합니다.

    func finish() {
        delegate?.didFinish(from: self)
    }
    func start() {
        let loginViewModel = LoginViewModel()
        loginViewController.viewModel = loginViewModel
        loginViewController.delegate = self // 1
        
        loginNavigationController = UINavigationController(rootViewController: loginViewController)
        loginNavigationController?.modalPresentationStyle = .fullScreen
        guard let loginNavigationController = loginNavigationController else {
            return
        }
        
        navigationController.present(loginNavigationController, animated: true)
    }

이제 delegate를 연결해 줍니다

extension LandingCoordinator: LandingViewDelegate {
    func goToLogin() {
        let loginCoordinator = LoginCoordinator(navigationController)
        loginCoordinator.parentCoordinator = self
        loginCoordinator.delegate = self
        childCoordinators.append(loginCoordinator)
        loginCoordinator.start()
    }
}

빌드하고 실행하면 랜딩 화면 -> 로그인 -> x 버튼을 누르면 랜딩화면으로 이동하면서 childCoordinator을 삭제하는 것을 확인 할 수 있습니다.

x 버튼을 누르게 되면 LoginCoordinator에서 loginViewController을 화면상에서 dismiss 시키고 finish를 호출합니다. 그 후 LandingCoordinator의 child coordinator을 삭제하는 코드가 실행되면서 child Coordinator을 정리합니다.

다음 포스팅에서는 로그인 성공을 가정하고 로그인 화면에서 홈화면으로 이동해보겠습니다!

728x90
반응형

댓글