dismiss에 대해서 알아봅시다

dismiss에 대해서 알아봅시다

예전에는 dismiss를 호출하면 호출한 viewController가 dismiss된다고 생각했습니다. 그런데 어떤 경우는 자신이 dismiss되는게 아니라 자신이 present한 viewController가 dismiss되었습니다. dismiss 동작에 대해서 제대로 이해하지 못해서 벌어진 일이었습니다.
그래서 이번에는 dismiss에 대해서 알아보려고 합니다.


먼저, Apple API에 정의된 dismiss를 먼저 알아보겠습니다.

presenting viewController는 presented viewController를 dismiss하는 책임을 가지고 있습니다. 만약 presented viewController에서 dismiss를 호출한 경우는 UIKit이 presenting viewController에 해제 관리를 요청합니다.

여기서 말하는 presentingViewController와 presentedViewController는 무슨 객체일가요?

  • presentingViewController객체는 자신을 화면에 보이도록 만든 viewController입니다.
  • presentedViewController는 자신이 화면에 보이도록 표시한 viewController입니다.

아래 코드로 presentingViewController와 presentedViewController를 설명하겠습니다.

let viewController1 = UIViewController()
let viewController2 = UIViewController()
let viewController3 = UIViewController()

viewController 3개가 존재하는데 viewController1이 화면에 표시되고 있는 rootViewController라고 가정하겠습니다.

viewController1.present(viewController2, animated: true, completion: nil)

viewController1에서 viewController2를 화면에 표시하면 presentingViewController와 presentedViewController는 아래 그림처럼 구성됩니다.

enter image description here

viewController1은 자신을 표시한 객체가 없기 때문에 presentingViewController가 nil이며 viewController2를 표시하기 때문에 presentedViewController는 viewController2를 가리키게 됩니다.
viewControll2는 자신을 표시한 객체가 viewController1이기 때문에 presentingViewController는 viewController1을 가리키며 더이상 화면에 더 표시하는 viewController가 없기 때문에 presentedViewController는 nil로 설정됩니다.

여기서 다시한번 아래코드 처럼 viewController3를 화면에 표시하겠습니다.

viewController2.present(viewController3, animated: true, completion: nil)

viewController3가 화면에 표시되면 presentedViewController와 presenting ViewController는 아래 그림처럼 구성됩니다.

enter image description here

viewController2는 presentedViewController가 이전에는 nil이었지만 viewController3를 화면에 표시하기 때문에 viewController3를 가리키게 됩니다. viewController3는 viewController2에 의해 화면에 표시되기 때문에 presentingViewController는 viewController2를 가리키게 되며 viewController3가 더이상 화면에 표시하는 viewController가 없기 때문에 presentedViewController는 nil이 됩니다.



presentedViewController와 presentingViewController에 대해서 알아보았습니다. 지금부터는 dismiss 동작방식에 대해서 알아보겠습니다.

Apple 문서에 dismiss동작에 대해서 정의된 내용을 보면

여러개의 viewController가 연속으로 화면에 표시되어 표시된 viewController의 스택이 구성된 경우에 스택의 아래에 위치한 viewController에서 dismiss가 호출되면 스택 위에 위치한 viewController와 childeViewController는 즉시 화면에서 사라집니다. 이런 일이 발생하면 맨 위에있는 view만 애니메이션 방식으로 해제되며 중간 viewController는 단순히 스택에서 제거됩니다.

처럼 정의 되어 있습니다.


예제 코드를 통해서 동작방식을 알아보겠습니다.

class ViewController: UIViewController {
    
  var viewController1: UIViewController!
  var viewController2: UIViewController!
  var viewController3: UIViewController!
  var navigationViewController1: UINavigationController!
  var viewController4: UIViewController!
  var viewController5: UIViewController!
  var viewController6: UIViewController!
  var viewController7: UIViewController!

  override func viewDidLoad() {
    super.viewDidLoad()
        
    DispatchQueue.main.async {
      self.viewController1 = self.makeViewController(backgroundColor: .red)
      self.present(self.viewController1, animated: true) {
        self.viewController2 = self.makeViewController(backgroundColor: .orange)
        self.viewController1.present(self.viewController2, animated: true) {
          self.viewController3 = self.makeViewController(backgroundColor: .cyan)
          self.viewController2.present(self.viewController3, animated: true) {
            self.viewController4 = self.makeViewController(backgroundColor: .blue)
            self.navigationViewController1 = UINavigationController(rootViewController: self.viewController4)
            self.viewController3.present(self.navigationViewController1, animated: true) {
              self.viewController5 = self.makeViewController(backgroundColor: .gray)
              self.navigationViewController1.pushViewController(self.viewController5, animated: true)
              DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                self.viewController6 = self.makeViewController(backgroundColor: .green)
                self.viewController5.present(self.viewController6, animated: true) {
                  self.viewController7 = self.makeViewController(backgroundColor: .yellow)
                  self.viewController6.present(self.viewController7, animated: true) {
                    print("done")
                  }
                }
              }
            }
          }
        }
      }
    }
  }
    
    
  func makeViewController(backgroundColor: UIColor) -> UIViewController {
      let viewController = UIViewController()
      viewController.modalPresentationStyle = .fullScreen
      viewController.view.backgroundColor = backgroundColor
      return viewController
  }
    
}

위 코드를 동작시켜면 viewController의 stack이 아래 그림처럼 구성됩니다.
enter image description here

present api를 이용해서 viewController를 화면에 표시하고 중간에 navigationViewController를 사용해서 childViewController를 구성했습니다. 각 단계별로 dismiss를 호출해서 어떻게 동작하는지 알아보겠습니다.

  • viewController7에서 호출
    viewController7.dismiss(animated: true, completion: nil)
    
    viewController7의 presentedViewController는 nil이기 때문에 presentingViewController에게 dismiss 요청이 전달됩니다. presentringViewController는 viewController6이고 여기서 dismiss가 실행됩니다. viewController6의 presentedViewController는 viewController7이기 때문에 viewController7이 화면에서 사라집니다.

  • viewController6에서 호출
    viewController6.dismiss(animated: true, completion: nil)
    
    viewController6에서 dismiss를 호출하면 presentedViewController가 viewController7이기 때문에 viewController7이 화면에서 사라집니다.

  • viewController3에서 호출
    viewController3.dismiss(animated: true, completion: nil)
    
    viewController3에서 dismiss를 호출하면 presentedViewController가 navigationViewController1이기 때문에 navigationViewController1이 화면에서 사라집니다. navigationViewcontroller1은 viewController4와 viewController5를 childViewController로 가지고 있는데 이때 모두 같이 화면에서 사라집니다. 또한 navigationViewController1 위의 모든 viewController가 스택에서 제거됩니다.

  • viewController1에서 호출
    viewController1.dismiss(animated: true, completion: nil)
    
    viewController1의 presentedViewController는 viewController2이기 때문에 viewController2 위의 모든 viewController가 stack에세 제거되며 viewController1만 화면에 남아있습니다.

  • navigationViewController1 및 childViewController에서 호출
    navigationViewController1에 포함된 viewController는 presentedViewController와 presentingViewController의 값이 navigationViewController와 같습니다.
    따라서, 어떤 viewController를 dismiss하든 결과는 같습니다.

    1. viewController4.dismiss(animated: true, completion: nil)
    
    2. viewController5.dismiss(animated: true, completion: nil)
    
    3. navigationViewController1.dismiss(animated: true, completion: nil)
    

    1, 2, 3번 코드 모두 동일하게 presentedViewcontroller가 viewController6이기 때문에 스택에서 viewController6과 7이 제거되고 화면에는 navigationController1과 childViewController가 화면에 남아 있습니다.

  • navigationViewController1에서 popViewController 수행

    navigationViewController1.popViewController(animated: true)
    
    // viewController6까지 제거
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        navigationViewController1.dismiss(animated: true, completion: nil)
    }
    

    navigationViewController1에서 popViewController를 수행하면 viewController5는 제거되지만 presentedViewController 상태는 그대로 유지되기 때문에 화면 변화가 없습니다. 다시 viewController6까지 제거해서 navigationViewController 화면으로 돌아와보면 viewController4가 화면에 보이고 viewController5는 pop되어있다는 것을 확인할 수 있습니다.



지금까지 dismiss 동작에 대해서 알아보았습니다. dismiss 동작에서 꼭 알아야하는 핵심은 presentedViewController가 존재하면 그 viewController를 화면에서 해제하고 만약에 존재하지 않다면 presentingViewController에게 위임하기 때문에 이점만 기억하면 dismiss동작을 혼동하지 않을 수 있습니다.



참고:

댓글

이 블로그의 인기 게시물

1. Framework: framework란?

closure에서는 왜 self를 사용해야 할까?