티스토리 뷰
UIKit에서 작은 뷰 단위로 점진적으로 SwiftUI로 리팩토링 하는 과정에서 알게 된 내용을 공유하겠습니다.
처음엔 SwiftUI 뷰를 단순히 UIHostingController로 감싸서 UIKit에서 'view.addSubview' 해서 사용하면 되는 줄 알았는데요.
Apple 공식 문서에 따르면 반드시 Container View Controller 패턴을 따라야 하며, 'addChild' -> 'addSubview' -> 'didMove(toParent:)' 3단계를 모두 거쳐야 합니다.
문제 상황
SwiftUI 컴포넌트를 기존 UIKit 프로젝트에 점진적으로 도입하다 보면, 이런 코드를 자주 보게 됩니다
// 흔한 실수 - 많은 개발자들이 이렇게 시도합니다
let hostingController = UIHostingController(rootView: MySwiftUIView())
view.addSubview(hostingController.view) // 이것만으로는 불충분
언뜻 보면 문제없어 보입니다. 실제로 화면에도 잘 나오고요. 하지만 이 방식에는 치명적인 문제가 있습니다.
무엇이 문제일까?
1. 생명주기 이벤트 누락
- 뷰 컨트롤러 생명주기 메서드가 호출되지 않음
- SwiftUI 뷰 내부에서 .onAppear, .onDisappear 같은 생명주기 이벤트가 제대로 동작하지 않을 수 있습니다
2. 메모리 관리 이슈
- 메모리 누수 가능성
- 부모-자식 관계가 제대로 설정되지 않아 메모리 누수가 발생할 가능성이 높아집니다
3. 이벤트 전파 문제
- 터치 이벤트나 키보드 처리 등이 제대로 작동하지 않을 수 있음
- 키보드 이벤트, 터치 이벤트 등이 예상대로 동작하지 않을 수 있습니다.
Apple이 제시한 해결책
[View Controller Programming Guide]
UIKit에서 다른 뷰 컨트롤러를 자식으로 추가할 때는 다음 세 단계를 반드시 거쳐야 한다고 명시되어 있습니다: Apple의 공식 문서에서는 Container View Controller를 구현할 때 올바른 순서를 따라야 한다고 명시하고 있습니다. UIViewController의 didMove(toParent:) 메서드 문서에 따르면, 이 메서드는 뷰 컨트롤러가 컨테이너 뷰 컨트롤러에 추가되거나 제거된 후에 호출되어야 합니다.
"The only requirement from UIKit is that you establish a formal parent-child relationship between the container view controller and any child view controllers."
— View Controller Programming Guide
올바른 구현: 필수 3단계 패턴
Step 1: 자식 뷰 컨트롤러로 추가
let hostingController = UIHostingController(rootView: MySwiftUIView())
addChild(hostingController)
역할
- 뷰 컨트롤러를 현재 뷰 컨트롤러의 자식으로 등록
- 생명주기 메서드(viewWillAppear, viewDidAppear 등)가 자동으로 전달되도록 설정
- 메모리 관리와 이벤트 처리가 제대로 작동하도록 보장
- UIKit이 생명주기를 올바르게 관리할 수 있도록 설정
Step 2: 뷰를 화면에 추가
view.addSubview(hostingController.view)
역할
- hostingController의 뷰를 현재 뷰의 서브뷰로 추가
- 실제로 화면에 보이게 됨
- 사용자에게 시각적으로 표시
Step 3: 부모-자식 관계 완성 알림
hostingController.didMove(toParent: self)
역할
- 자식 뷰 컨트롤러에게 부모가 설정되었다고 알림
- 내부적으로 필요한 설정들이 완료됨
- 내부 초기화 작업 완료
실전 코드
class MainViewController: UIViewController {
private var chartHostingController: UIHostingController<ChartView>?
override func viewDidLoad() {
super.viewDidLoad()
addSwiftUIView()
}
private func addSwiftUIView() {
let swiftUIView = MySwiftUIView()
let hostingController = UIHostingController(rootView: swiftUIView)
// 1단계: 자식 뷰 컨트롤러로 추가
addChild(hostingController)
// 2단계: 뷰를 화면에 추가
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingController.view)
// Auto Layout 설정
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// 3단계: 부모-자식 관계 완성 알림
hostingController.didMove(toParent: self)
// 참조 보관 (제거 시 필요)
self.chartHostingController = hostingController
}
}
제거할 때도 올바른 순서가 필요
SwiftUI 뷰를 제거할 때도 마찬가지로 올바른 순서를 따라야 합니다:
private func removeSwiftUIView() {
guard let hostingController = self.chartHostingController else { return }
// 1단계: 제거될 것임을 알림
hostingController.willMove(toParent: nil)
// 2단계: 뷰를 화면에서 제거
hostingController.view.removeFromSuperview()
// 3단계: 부모-자식 관계 해제
hostingController.removeFromParent()
// 참조 해제
self.chartHostingController = nil
}
왜 이 세 단계가 모두 필요한가?
addChild의 역할
- 뷰 컨트롤러 계층 구조 설정
- 생명주기 이벤트 전달 보장
- 메모리 관리 개선
addSubview의 역할
- 실제 뷰 계층 구조에 추가
- 화면에 시각적으로 표시
didMove(toParent:)의 역할
- 자식 뷰 컨트롤러에게 완전히 설정되었음을 알림
- 내부 초기화 작업 완료
더 나은 개발자 경험을 위한 Extension
매번 이 3단계를 반복하기 번거롭다면, extension으로 추상화할 수 있습니다:
extension UIViewController {
func embedSwiftUI<Content: View>(
_ swiftUIView: Content,
in containerView: UIView? = nil
) -> UIHostingController<Content> {
let hostingController = UIHostingController(rootView: swiftUIView)
let targetView = containerView ?? view
addChild(hostingController)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
targetView.addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: targetView.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: targetView.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: targetView.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: targetView.bottomAnchor)
])
hostingController.didMove(toParent: self)
return hostingController
}
func removeChildViewController(_ child: UIViewController) {
child.willMove(toParent: nil)
child.view.removeFromSuperview()
child.removeFromParent()
}
}
사용법
// 추가
let chartController = embedSwiftUI(ChartView(data: data), in: chartContainer)
// 제거
removeChildViewController(chartController)
요약
- UIHostingController는 UIViewController: 다른 뷰 컨트롤러와 동일하게 Container View Controller 패턴을 따라야 함
- 생명주기 보장: 올바른 패턴을 따라야 SwiftUI의 생명주기 이벤트가 정상 작동
- 메모리 안정성: 부모-자식 관계를 명확히 해야 메모리 누수 방지
- Apple 가이드라인 준수: 공식 문서에서 명시한 방법이므로 향후 호환성 보장
결론
UIKit과 SwiftUI를 함께 사용할 때는 단순히 뷰만 추가하는 것이 아니라, 뷰 컨트롤러 계층 구조도 올바르게 설정해야 합니다.
이 세 가지 단계(addChild → addSubview → didMove(toParent:))를 모두 거쳐야만 UIKit과 SwiftUI가 완전히 호환되어 작동합니다.
이는 Apple의 공식 문서에서 명시한 Container View Controller 패턴의 일부입니다.
SwiftUI와 UIKit을 함께 사용하는 코드를 많이 사용하고 있을텐데요.
단순히 View가 잘 표시된다고 해서 가볍게 넘기지 말고 대규모 프로젝트에서는 이런 작은 차이가 큰 영향을 미칠 수 있으니, 공식문서의 가이드를 따르며 개발하도록 합시다!
참고 자료
View Controller Programming Guide for iOS: Implementing a Container View Controller
View Controller Programming Guide for iOS
developer.apple.com
https://developer.apple.com/documentation/uikit/uiviewcontroller
UIViewController | Apple Developer Documentation
An object that manages a view hierarchy for your UIKit app.
developer.apple.com
'iOS > SwiftUI' 카테고리의 다른 글
SwiftUI - 수식어 적용 순서 (0) | 2021.05.26 |
---|---|
SwiftUI - View, UIHostingController (0) | 2021.04.07 |
- Total
- Today
- Yesterday
- GraphDB
- blendshapes
- ARKit
- rxswift6
- coreml
- blendshape
- SwiftUI
- Lottie
- UIHostingController
- disposeBag
- ios
- C++
- 알고리즘
- 코코아팟
- 프로그래머스
- Reactivex
- Kotlin
- 안드로이드
- 백준
- cocoapods
- rxswift
- boj
- DispatchQueue
- 백준온라인저지
- 카카오인턴십
- Swift weak
- Swift
- Swift unowned
- SWEA
- infallible
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |