[SwiftUI/UIKit] UIViewControllerRepresentable 알아보기 + 쇼츠처럼 페이지뷰 만들기

양재현·2025년 8월 23일

UIViewControllerRepresentable

UIViewControllerRepresentable는 UIViewController를 SwiftUI 에서 사용가능하게 해주는 프로토콜이다.

이 프로토콜을 채택하게 되면

func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType

func updateUIViewController(Self.UIViewControllerType, context: Self.Context)

이 두 메서드를 필수적으로 구현 해야 한다.

  • makeUIViewController(context:) 메서드는 UIKit 뷰 컨트롤러를 생성하고 초기화하는 역할을 한다. SwiftUI가 화면에 뷰를 처음 표시할 때 단 한 번만 호출되며, 이 메서드는 뷰 컨트롤러를 생성하고 반환하는 책임만을 가진다.

  • updateUIViewController(_:context:) 메서드는 SwiftUI의 상태가 변경될 때마다 뷰 컨트롤러의 상태를 업데이트하는 역할을 한다. SwiftUI 뷰의 @State나 @Binding과 같은 프로퍼티가 변경되면, SwiftUI는 이 메서드를 호출해 UIKit 뷰 컨트롤러에 최신 데이터를 전달할 수 있도록 해준다.

여기서 추가적으로 Coordinator 라는 개념이 있는데 Coordinator는 SwiftUI 뷰와 연동되는 UIKit 뷰 컨트롤러의 대리자(delegate) 역할을 하는 클래스다. 한 마디로, SwiftUI와 UIKit 사이의 소통을 원활하게 만들어주는 다리라고 할 수 있다 !

Coordinator 를 사용하면, Cocoa에서 자주 쓰이는 패턴들을 구현할 수 있다. 예를 들어, delegate 패턴, data source 패턴, 그리고 target-action을 통한 사용자 이벤트 처리 같은 것들이 있다.

쇼츠 형태의 페이지 뷰 만들기

파일 구성은 이렇게 해주었다.

PageViewController

import SwiftUI
import UIKit


struct PageViewController<Page: View>: UIViewControllerRepresentable {
    var pages: [Page]
    @Binding var currentPage: Int
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .vertical)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        
        return pageViewController
    }
    
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
    }
    
    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController
        var controllers = [UIViewController]()
        
        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }
        
        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return controllers.last
            }
            return controllers[index - 1]
        }
        
        
        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == controllers.count {
                return controllers.first
            }
            return controllers[index + 1]
        }
        
        func pageViewController(
            _ pageViewController: UIPageViewController,
            didFinishAnimating finished: Bool,
            previousViewControllers: [UIViewController],
            transitionCompleted completed: Bool) {
                if completed,
                   let visibleViewController = pageViewController.viewControllers?.first,
                   let index = controllers.firstIndex(of: visibleViewController) {
                    parent.currentPage = index
                }
            }
    }
}

func makeCoordinator()

이 메서드에서는 Coordinator 인스턴스를 생성하고 반환한다. Coordinator는 UIPageViewController의 델리게이트데이터 소스 역할을 하며, UIKit에서 발생한 이벤트를 SwiftUI로 전달하는 중요한 역할을 한다.


makeUIViewController(context:)

이 메서드는 UIPageViewController 인스턴스를 처음 생성하고 초기 설정하는 역할을 한다. SwiftUI가 이 뷰를 화면에 처음 띄울 때 단 한 번만 호출된다.

transitionStyle: .scroll: 페이지 넘김 애니메이션 스타일을 스크롤로 설정한다.
navigationOrientation: .vertical: 페이지 넘김 방향을 수직으로 설정한다.
pageViewController.dataSource = context.coordinator: UIPageViewController의 데이터 소스를 위에서 만든 Coordinator 인스턴스로 지정한다. 데이터 소스는 뷰 컨트롤러에 어떤 뷰를 표시할지 알려주는 역할을 한다.
pageViewController.delegate = context.coordinator: UIPageViewController의 델리게이트를 Coordinator 인스턴스로 지정한다. 델리게이트는 페이지 넘김이 완료되었을 때와 같은 이벤트를 처리한다.


updateUIViewController(_:context:)

이 메서드는 SwiftUI의 currentPage 값이 변경될 때마다 호출되어, UIPageViewController의 현재 표시 페이지를 업데이트 한다.

pageViewController.setViewControllers(...): UIPageViewController에게 특정 뷰 컨트롤러를 표시하도록 지시한다. 여기서는 context.coordinator.controllers 배열에서 currentPage에 해당하는 뷰 컨트롤러를 가져와 보여준다.


Coordinator 클래스

-> UIPageViewController의 델리게이트데이터 소스 역할을 수행하는 핵심 클래스다.

var parent: PageViewController: 부모인 PageViewController 구조체에 대한 참조를 가진다. 이를 통해 @Binding으로 선언된 currentPage와 같은 프로퍼티에 접근할 수 있다.

var controllers = [UIViewController](): parent.pages 배열의 각 SwiftUI 뷰를 UIHostingController를 사용해 UIViewController로 변환하여 저장한다. UIHostingController는 SwiftUI 뷰를 UIKit 환경에서 사용할 수 있게 해주는 래퍼 역할을 한다.


func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController)

현재 페이지의 이전 페이지에 해당하는 UIViewController를 반환한다. 이 메서드를 통해 이전 페이지로 스와이프할 때 어떤 뷰를 보여줄지 결정한다. 페이지가 첫 번째일 경우 마지막 페이지를 반환하여 무한 순환 스크롤을 구현한다.


 func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController)

현재 페이지의 다음 페이지에 해당하는 UIViewController를 반환한다. 마찬가지로 마지막 페이지일 경우 첫 번째 페이지를 반환한다.


 func pageViewController(
            _ pageViewController: UIPageViewController,
            didFinishAnimating finished: Bool,
            previousViewControllers: [UIViewController],
            transitionCompleted completed: Bool)

페이지 넘김 애니메이션이 완료되었을 때 호출되는 델리게이트 메서드다.

if completed, ...: 애니메이션이 성공적으로 완료되었을 때만 실행된다.
let visibleViewController = pageViewController.viewControllers?.first: 현재 화면에 보이는 뷰 컨트롤러를 가져온다.
let index = controllers.firstIndex(of: visibleViewController): 보이는 뷰 컨트롤러의 인덱스를 찾는다.
parent.currentPage = index: 찾은 인덱스를 부모 뷰의 @Binding 변수인 currentPage에 할당한다. 이 부분이 바로 UIKit에서 SwiftUI로 데이터가 다시 전달되는 핵심적인 과정이다.

PageView

import SwiftUI

struct PageView<Page: View>: View {
    var pages: [Page]
    @State private var currentPage = 0
    
    var body: some View {
        PageViewController(pages: pages, currentPage: $currentPage)
    }
}

PageViewControllerApp

import SwiftUI

@main
struct PageViewControllerApp: App {
    var body: some Scene {
        WindowGroup {
            PageView(pages: [
                Color.green,
                Color.orange,
                Color.purple
            ])
        }
    }
}

실제 구현 영상


🍎참고

https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit

0개의 댓글