본문 바로가기

iOS

ViewController 생명주기의 몰랐던 점 (feat. 트러블 슈팅)

 

ViewController 생명주기 호출 순서

iOS 개발을 한다면 ViewController 생명주기는 모두가 알고 있을 거예요. 

 

viewDidLoad
-> viewWillAppear
-> viewDidAppear
-> viewWillDisappear
-> viewDidDisappear

 

다른 뷰컨트롤러로 넘어가는 상황에서 현재 뷰컨트롤러의 viewWillDisppear가 먼저 호출될까 전환되는 뷰컨트롤러의 viewWillAppear가 먼저 호출될까? 도 디버깅을 해본다면 바로 알 수 있습니다.

 

테스트를 위해서 뷰컨트롤러의 모든 생명주기에 print문을 찍어볼게요.

 

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        print("✨ FirstViewController viewDidLoad")
    }

    @IBAction func buttonDidTap(_ sender: Any) {
        
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        print("✨ FirstViewController viewWillAppear")
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        print("✨ FirstViewController viewDidAppear")
    }

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

        print("✨ FirstViewController viewWillDisappear")
    }

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

        print("✨ FirstViewController viewDidDisappear")
    }
}

final class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "두번째"
        view.backgroundColor = .yellow
        print("🚀 SecondViewController viewDidLoad 🚀")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        print("🚀 SecondViewController viewWillAppear 🚀")
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        print("🚀 SecondViewController viewDidAppear 🚀")
    }

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

        print("🚀 SecondViewController viewWillDisappear 🚀")
    }

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

        print("🚀 SecondViewController viewDidDisappear 🚀")
    }
}

 

그리고  FirstViewController에서 SecondViewController로 네비게이션 push를 해보겠습니다. 

 

 

FirstViewController의 viewWillDisappear가 먼저 호출되고 다음 SecondViewController의 viewWillAppear가 호출되는 것을 볼 수 있어요. 이 호출 순서가 중요할까? 라고 생각할지 모르겠지만 두 화면에서 각각 애니메이션을 처리하고 있을 때 부드러운 애니메이션 효과를 주기 위해 필요합니다. 

 

다음 뒤로가기 버튼을 눌러볼게요.

 

FirstViewController와 SecondViewController 각각을 생각해보면, 모두 

 

viewDidLoad
-> viewWillAppear
-> viewDidAppear
-> viewWillDisappear
-> viewDidDisappear

 

위와 같은 호출순서를 따르고 있어요.

 

그런데, 이번에 만난 이슈에서는 이 호출 순서보다 한가지 재밌는 예외 케이스(?)를 발견했어요. 

 

viewWillAppear 다음에 항상 viewWillDisappear 호출된다?  ❎

 

viewWillAppear 메소드가 호출된 후에 viewWillDisappear 메소드가 항상 호출되지 않습니다.그리고, viewWillDisappear 메소드 다음에도 항상 viewDidDisappear 메소드가 호출되지 않습니다. 

 

어떤 경우에 호출되지 않을까요?

 

네비게이션 push를 통한 화면전환에서 스와이프 제스처를 이용해서 뒤로 가려다 취소를 한 경우입니다.

그 상황을 녹화해봤어요. SecondViewController에서 뒤로가기 버튼을 눌러 뒤로가는 것이 아닌, 제스처를 통해서 뒤로가려고 했다가 중간에 취소한 케이스입니다. 

 

 

뷰컨트롤러 생명주기 호출 순서는 아래와 같습니다.

 

 

먼저, FirstViewController를 보면, 호출 순서는 다음과 같습니다. 

viewWillAppear
-> viewWillDisappear
-> viewDidDisappear

 

뷰컨트롤러가 보여질려고 했지만 취소되어 사라집니다. viewWillAppear가 호출된 이후에 viewDidAppear가 호출되지 않았습니다!

 

다음, SecondViewController를 보면 호출 순서는 아래와 같습니다. 

viewWillDisapppear
-> viewWillAppear
-> viewDidAppear 

 

뷰컨트롤러가 사라지려고 했지만 취소되어 다시 보입니다. 

 

이 관계를 보았을 때, 

 

viewWillAppear 뒤에 항상 viewDidAppear가 호출되지 않는다.

viewWillDisappear 뒤에 항상 viewDidDisappear가 호출되지 않는다.

 

라고 할 수 있습니다.

 

또 더 유추해본다면 뷰컨트롤러의 생명주기는 서로에게 영향을 미칩니다. appear가 호출되면 뷰가 사라지기 위해 disappear가 무조건 호출되어야하고, disappear가 호출된다면 해당 뷰가 다시 appear되기 위해선 appear가 호출되어야 합니다. 

 

즉,

will과 did의 상관없이 appear과 disappear의 관계는 항상 붙어다니는 관계

라고 생각할 수 있습니다.

 

will과 did의 관계도 생각해보면,

will 다음에 항상 did는 올 수도 있고 안 올수도 있지만,

did 전에는 항상 will이 와야합니다.

 

이말은 아래의 사실과 같다고 할 수 있어요.

 

viewDidAppear 전에는 항상 viewWillAppear가 호출되어야한다.

viewDidDisappear 전에는 항상 viewWillDisappear가 호출되어야한다. 

 


조금 더 실험해볼게요. 

modal present 화면 전환에서는?

위의 상황에서 SecondViewController를 ThirdViewController라고 생각하고만 바꿔서 화면전환은 모달로 다시 테스트해보겠습니다!

 

modal present하는 상황은 presentation이 fullscreen인 상황과 아닌 상황 2가지로 나눠서 볼게요.

두가지 상황을 나누는 이유는 fullscreen이 아닌 경우 FirstViewController의 disappear 생명주기 메소드가 호출되지 않기 때문이에요. 뷰컨트롤러가 완전히 사라지지 않고 뒤에 스냅샷으로 남아있기 때문에 다른 케이스입니다. 

 

먼저, fullscreen부터 실험을 해볼게요. 이 케이스는 일반적인 케이스입니다! 그니까 원래 생각했던 방식대로 뷰컨트롤러 생명주기가 실행돼요. 

FirstViewController와 ThirdViewController 모두 생각했던 일반적으로 실행됩니다. 

viewDidLoad
-> viewWillAppear
-> viewDidAppear
-> viewWillDisappear
-> viewDidDisappear

 

그럼 fullscreen이 아닌 모달인 경우에는 어떻게 될까요? 

 

네. 이번에도 모달을 닫으려는 제스처를 취소하는 경우 viewWillAppear 이후 viewDidAppear 함수가 호출되지 않고, viewWillDisappear 이후 viewDidDisappear 함수가 호출되지 않습니다! 아래 영상같은 경우입니다.

 

 

빨간 네모 박스를 친 부분을 보신다면 

viewWillDisappear
-> viewWillAppear
-> viewDidAppear 

 

위 호출 순서를 볼 수 있습니다. 

 

 


이 사실을 알고 난 후, viewDidAppear에 애니메이션 효과를 시작하고, viewWillDisappear에서 사라지는 애니메이션 효과를 주어 더 부드럽게 효과를 개선할 수 있었습니다. 

 

개선 전, 뒤로가기 제스쳐에서 행성이 남아있음.

 

 

개선 후, 뒤로가기 제스쳐에 애니메이션 효과 적용 

 

 

 

영상으로는 이 부드러움이 안담기네요.. 나중에 출시하면 링크 달아두겠습니다!

 


 

UIAdaptivePresentationControllerDelegate

제스쳐를 이용한 뒤로가기를 어떻게 감지할 수 있는지에 대해서도 알면 좋을 것 같아 더 찾아봤습니다.

 

A set of methods that, in conjunction with a presentation controller, determine how to respond to trait changes in your app. 

 

해당 프로토콜을 UIPresentationController 객체에 채택한다면, 제스처로 뒤로가는 상황을 알 수 있습니다!

이전 뷰컨트롤러와 다음 뷰컨트롤러 두 곳 모두에서 유저가 제스처로 인해 뒤로갔는지에 대한 여부를 알 수 있을 것 같아요.

이 프로토콜도 나중에 알아봐야되겠어요.

 

읽어주셔서 감사합니다!