seong_hye, the developer

Swift) 책과 같이 페이지 넘기는 애니메이션에 대해 알아보기 (UIPageViewController) 본문

IOS/UIKit

Swift) 책과 같이 페이지 넘기는 애니메이션에 대해 알아보기 (UIPageViewController)

seong_hye 2025. 6. 25.

 

페이지를 넘길 때 종이가 말려서 넘어가는 듯한 느낌을 주고 싶을 때 사용하는 기능에 대해 알아보자

책을 넘기는 듯한 Page Curl은 SwiftUI에서는 기본 기능이 없어 UIPageViewController를 래핑해 사용함


📘UIPageViewController

UIKit의 컨테이너 뷰 컨트롤러

여러 페이지를 관리하고 스와이프 제스처나 애니메이션을 통해 페이지 간 전환을 제공

⚠️ Page Curl은 iPhone에서 제약이 존재(스택 구조 / 회전 등), 최신 iOS UI 가이드에선 스크롤형을 더 권장


🔹 생성 방법

let pageVC = UIPageViewController(
	transitionStyle: .scroll,
    navigationOrientation: .horizontal,
    options: nil
)

🔍 TransitionStyle

.scroll

가장 많이 사용됨, 수평/수직 스크롤 형태의 전환, 연속 페이징 가능

 

.pageCurl

종이 넘기듯 "책 넘김" 효과 (iPad에서 주로 사용됨, iPhone에선 제한적)

 

🔍 Navigation Orientation

.horizontal

좌우 넘김

 

.vertical

상하 넘김


🔹 주요 프로토콜

🔍 DataSource

페이지 전환 시 어떤 VC를 보여줄 지 지정하는 코드

func pageViewController(_ pvc: UIPageViewController, viewControllerBefore vc: UIViewController) -> UIViewController ?

func pageViewController(_ pvc: UIPageViewController, viewControllerAfter vc: UIViewController) -> UIIViewConttroller?

 

🔍 Delegate

페이지 전환 완료, 방향 등을 감지하는 코드

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

🔹 CODE

import SwiftUI
import UIKit

struct PageCurlPager<Page: View>: UIViewControllerRepresentable {
    let pages: [Page]

    func makeUIViewController(context: Context) -> UIPageViewController {
        let vc = UIPageViewController(transitionStyle: .pageCurl,	// 책 넘김 효과
                                      navigationOrientation: .horizontal)	// 좌우
      	
        // 페이지 컨트롤러 설정
        vc.dataSource = context.coordinator
        vc.delegate = context.coordinator
        
        // 첫 페이지 설정
        if let first = context.coordinator.controllers.first {
            vc.setViewControllers([first], direction: .forward, animated: false)
        }
        // 양면처럼 보이게 하려면 true + spineLocation 조정
        vc.isDoubleSided = false
        return vc
    }

    func updateUIViewController(_ uiVC: UIPageViewController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(self, pages.map { UIHostingController(rootView: $0.edgesIgnoringSafeArea(.all)) })
    }

    final class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        let parent: PageCurlPager
        let controllers: [UIViewController]
        init(_ parent: PageCurlPager, _ controllers: [UIViewController]) {
            self.parent = parent
            self.controllers = controllers
        }

        func pageViewController(_ pvc: UIPageViewController,
                                viewControllerBefore vc: UIViewController) -> UIViewController? {
            guard let idx = controllers.firstIndex(of: vc), idx > 0 else { return nil }
            return controllers[idx - 1]
        }

        func pageViewController(_ pvc: UIPageViewController,
                                viewControllerAfter vc: UIViewController) -> UIViewController? {
            guard let idx = controllers.firstIndex(of: vc), idx < controllers.count - 1 else { return nil }
            return controllers[idx + 1]
        }
    }
}

// 사용 예시
struct ContentView: View {
    var body: some View {
        PageCurlPager(pages: (0..<5).map { i in
            ZStack {
                Color(hue: Double(i)/5, saturation: 0.5, brightness: 0.9)
                Text("Page \(i)").font(.largeTitle.bold())
            }
        })
    }
}

 

📌 결과 화면

 


🔹 SwiftUI의 View를 넘기는 코드

UIPageViewController를 래핑해 사용할 때 UIViewController 배열을 넘기게 됨

하지만 SwiftUI View 배열을 넘기고 싶은 경우에는 UIHostingController(rootView:)로 감싸면 됨

class Coordinator: NSObject, UIPageViewControllerDataSource {
    var parent: PageCurlPager
    var controllers: [UIViewController]

    init(_ parent: PageCurlPager) {
        self.parent = parent
        // SwiftUI View → UIHostingController 변환
        self.controllers = parent.views.map { UIHostingController(rootView: $0) }
    }
    ...
}

🔹 SwiftUI만 코드 짜는 것이 가능한가?

SwiftUI만으로는 PageCurl 효과를 자연스럽게 재현하는 것은 힘듦

이팩트 자체가 3D 메쉬 변형, 동적 그림자, 종이 말림까지 필요하기 때문

~> UIKit의 UIPageViewController를 래핑해 SwiftUI에서 쓰는게 제일 간단, 안정적


🔹 사용하는 경우

책 / 잡지 / 앨범 뷰어 -> .pageCurl

온보딩 화면 (여러 화면 슬라이드) -> .scroll + pageControl

분량 많은 컨텐츠를 페이지 단위로 나누어 표현해야 할 때 사용

 

🔍 Page Control ( 페이지 인디케이터)

여러 페이지 중 현재 페이지 위치를 점(●○○) 형태로 보여주는 UI

UIKit의 UIPageControl을 직접 사용하거나 SwiftUI의 TabView와 .page 스타일을 쓰면 자동으로 사용됨

 

UIPageControl을 따로 추가해 Delegate에서 currentPage를 업데이트

UIPageViewController는 UIPageControl을 자동 관리하지 않으므로 수동으로 붙여야 함

func makeUIView(context: Context) -> UIPageControl {
    let control = UIPageControl()
    control.currentPageIndicatorTintColor = .black
    control.pageIndicatorTintColor = .lightGray
    control.addTarget(context.coordinator, action: #selector(Coordinator.changed), for: .valueChanged)
    return control
}
// 페이지 전환 완료 → PageControl 업데이트
func pageViewController(_ pvc: UIPageViewController,
                        didFinishAnimating finished: Bool,
                        previousViewControllers: [UIViewController],
                        transitionCompleted completed: Bool) {
    if completed, let currentVC = pvc.viewControllers?.first,
       let index = pages.firstIndex(of: currentVC) {
        pageControl.currentPage = index
    }
}

🔹 주의할 점

.pageCurl은 iPhone에선 UI제약이 있고, SplitView / iPad 환경에서 더 자연스러움

메모리 관리: 페이지가 많으면 UIHostingControoler를 매번 만들지 않고 캐싱을 고려해야 함


 

Comments