증상
- 모달을 단계별 dismiss 체이닝으로 닫을 때
- 애니메이션이 여러 번 재생.
- 간헐적으로 닫힘 실패가 발생.
영향
- 사용자 입장에서는 화면이 들썩이는 느낌 → UX 저하
- 코드 측면에서도 completion 순서 의존 → 취약한 흐름
원인 분석
- 단계별 체이닝은 중간 단계의 존재와 completion 타이밍에 강하게 의존
- 시스템 피커(카메라/포토) 등 시스템 VC가 개입하면 흐름이 더 복잡
- iPad 멀티-씬, 외부 디스플레이 등 윈도우/씬 분기도 고려 대상
대안 비교
체이닝: presentingViewController를 따라가며 하나씩 닫기
- 장점: 이해하기 쉽고 바로 구현 가능
- 단점: 애니메이션 다중 표시, 흐름 취약, 단계 변경에 취약
프레젠터 루트에서 일괄 dismiss: 현재 VC의 최상위 presenter를 찾아 한 번에 닫기
- 장점: 모달 한 번에 종료, 애니메이션 1회
- 고려: 씬/윈도우 구성이 복잡하면 루트 탐색 로직 필요
Coordinator 연계: 일괄 해제 후 루트 네비 상태 복구까지 포함한 구조화
- 장점: 복잡한 플로우에서 재사용/가시성 높음
- 단점: 현재 프로젝트 규모/타임라인 대비 과설계
최종 결정
- 루트 기준 일괄 dismiss로 전환
- 프로젝트 규모, 시한, 복잡도 대비 가성비 최상
- iOS 15+ 이후에는 활성 UIWindowScene의 key window 루트를 기준으로 dismiss하는 방식 권장
구현
간단 해법(현재 씬 루트에서 한 번에 닫기)
// 현재 씬의 루트에서 모달 체인을 한 번에 닫기
self.view.window?.rootViewController?.dismiss(animated: true)
- 장점: 한 줄, 적용이 즉시 가능
- 주의: 멀티-씬 환경(iPad Split View 등)에서는 해당 씬의 window를 잡고 있는지 확인
iOS 15+ 안전 해법(활성 UIWindowScene 기준)
extension UIApplication {
var activeKeyWindow: UIWindow? {
connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }?
.windows.first { $0.isKeyWindow }
}
}
// 프로젝트 전역 유틸로 사용
func dismissAllModals(animated: Bool = true, completion: (() -> Void)? = nil) {
UIApplication.shared.activeKeyWindow?
.rootViewController?
.dismiss(animated: animated, completion: completion)
}
// 사용 예
dismissAllModals()
- 포인트: “지금 사용자에게 보이는 씬”의 key window 루트에서 일괄 dismiss
- 효과: 시스템 피커 포함 상황, iPad 멀티-씬에서도 일관된 동작
“프레젠터 루트”에서 닫기(대안 B) — 참고용
extension UIViewController {
/// 나를 포함한 모달 체인을 '나를 띄운 최하단 presenter'까지 거슬러가서 한 번에 닫음
func dismissToPresentingRoot(animated: Bool = true, completion: (() -> Void)? = nil) {
var root = self
while let presenter = root.presentingViewController {
root = presenter
}
root.dismiss(animated: animated, completion: completion)
}
}
- 장점: 현재 VC 관점에서 명확
- 주의: 씬/윈도우 분기가 섞이면 window 기반 접근이 더 안정적
Coordinator와 연계(대안 C) — 규모 확대 시
dismissAllModals { [weak self] in
// 루트 화면 상태 복구(예: 홈 탭으로 이동)
self?.appCoordinator.switch(to: .home)
self?.appCoordinator.rootNavigationController.popToRootViewController(animated: false)
}
- 언제? 큰 앱에서 “모달 해제 + 네비 상태 재구성”이 반복될 때
테스트 & 체크리스트
디바이스/씬
- iPhone(일반, SE 3세대 등 소형 기기)
- iPad 11″, 13″ / Split View / Slide Over
- 외부 디스플레이 연결(가능 시)
플로우
- 피커(카메라/포토) → 편집 → 확인 → 한 번에 닫힘
- 중간 단계 스킵(예: 확인 없이 취소)에서도 항상 1회 애니메이션
- 닫힌 뒤 원래 화면 상태 정상 (스크롤 위치, 탭/네비 선택 등)
안정성
- 모든 dismiss 호출이 메인 스레드
- isModalInPresentation = true인 화면에서 명시적 dismiss만 허용되는 점 확인
- VoiceOver 사용 시, 닫힌 뒤 초점 이동이 예상대로 동작
결과 & 효과
- 애니메이션 1회로 단순화 → 화면 전환이 자연스러워짐
- 닫힘 실패 케이스 제거 → 흐름 신뢰성 상승
- 구현 난이도 대비 효과가 커, UX/안정성/유지보수성 모두 개선
회고
- “빨리 만들 수 있는 체이닝”보다 장기적으로 안정적인 루트 기반 dismiss가 유지보수에 유리.
- iOS 13 이후 멀티-씬 지원 환경에는 window/scene 맥락을 정확히 잡는 습관이 중요.
- 규모가 커지면 Coordinator에 일괄 dismiss + 상태 복구를 공통화하는 편이 팀 생산성을 높임.