티스토리 뷰

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)

요약

  1. UIHostingController는 UIViewController: 다른 뷰 컨트롤러와 동일하게 Container View Controller 패턴을 따라야 함
  2. 생명주기 보장: 올바른 패턴을 따라야 SwiftUI의 생명주기 이벤트가 정상 작동
  3. 메모리 안정성: 부모-자식 관계를 명확히 해야 메모리 누수 방지
  4. Apple 가이드라인 준수: 공식 문서에서 명시한 방법이므로 향후 호환성 보장

결론

UIKit과 SwiftUI를 함께 사용할 때는 단순히 뷰만 추가하는 것이 아니라, 뷰 컨트롤러 계층 구조도 올바르게 설정해야 합니다.

이 세 가지 단계(addChild → addSubview → didMove(toParent:))를 모두 거쳐야만 UIKit과 SwiftUI가 완전히 호환되어 작동합니다.

이는 Apple의 공식 문서에서 명시한 Container View Controller 패턴의 일부입니다.

SwiftUI와 UIKit을 함께 사용하는 코드를 많이 사용하고 있을텐데요.

단순히 View가 잘 표시된다고 해서 가볍게 넘기지 말고 대규모 프로젝트에서는 이런 작은 차이가 큰 영향을 미칠 수 있으니, 공식문서의 가이드를 따르며 개발하도록 합시다!

 

 

참고 자료

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html

 

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
링크
«   2025/10   »
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
글 보관함