애플의 SwiftUI 튜토리얼
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
위 URL 로 가서 프로젝트 파일을 다운받자. 튜토리얼은 SwiftUI와 UIKit 사이의 통합에 집중하였기에, 그외 data asset 이나 뷰 요소들은 설명이 없고 프로젝트 파일을 다운받아야 한다.
UIViewRepresentable 또는 UIViewControllerRepresentable 프로토콜을 준수하는 타입을 만듦으로써 UIKit 요소를 SwiftUI에 추가한다.
SwiftUI는 UIKit 요소의 라이프 사이클을 관리하고 적절한 때에 업데이트 해준다.
PageViewControoler.swift 파일을 만들고 UIViewControllerRepresentable 프로토콜을 준수하는 구조체를 하나 만들자.
페이지 뷰 컨트롤러는 UIViewController 를 담은 배열을 변수로 가진다. 배열의 각 요소는 스크롤링할때 나타나는 각 페이지가 된다.
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
}
UIPageViewController 를 형성하는 makeUIViewController(context:) 메소드를 추가한다.
SwiftUI는 뷰를 표시할 때가 됐을때 makeUIViewController 메소드를 한번 호출한다.
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
return pageViewController
}
}
PageViewController 구조체에 updateUIViewController(_:context:) 메소드를 추가한다. setViewControllers 를 호출해, view controller 들이 담긴 배열의 첫번째 요소를 디스플레이에 표시하기 위해서이다.
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[0]], direction: .forward, animated: true)
}
step 1~3에서 만든 PageViewController 구조체를 SwiftUI View 에 추가한다. 새로 PageView.swift 파일을 만들자.
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map { element in
UIHostingController(rootView: element) }
}
var body: some View {
PageViewController(controllers: viewControllers)
}
}
UIHostingController 는 UIViewController 의 서브클래스이다.
PageViewController 구조체 안에 NSObject 클래스를 상속하는 Coorniator 클래스를 만들자.
SwiftUI는 makeUIViewController 메소드와 updateUIViewController 메소드를 호출할때마다 coordinator를 두 함수의 context로 전달해 준다.
class Coordinator: NSObject {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
}
coordinator 를 생성하는 메소드 makeCoordinator()를 PageViewController 구조체 안에 만든다.
SwiftUI는 makeUIViewController 메소드가 호출되기 전에 앞서 makeCoordinator() 메소드를 호출한다.
SwiftUI에서 기존 코코아 패턴(델리게이트, 데이터 소스, 타겟-액션 이벤트)는 coordinator 를 사용해서 구현한다.
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
step 1 에서 만든 Coordinator 클래스가 UIPageViewControllerDataSource 프로토콜을 준수한다고 선언한다.
makeUIViewController 메소드 내부에 pageViewController.dataSource = context.coordinator 를 추가한다.
coordinator 로 만들어진 컨텍스트가 makeUIViewController 로 전달되고, context 의 coordinator 는 makeUIViewController 가 만들어낼 뷰의 데이터 소스가 된다.
PageView 내부에 @State 프로퍼티를 추가하고, 해당 프로퍼티를 PageViewController 뷰와 바인딩 한다.
PageViewController 내부에 @Binding var currentPage: Int 를 추가하고, 뷰를 업데이트 해주는 updateViewController 내부에 currentPage 변수를 추가한다.
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var currentPage: Int
...
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
...
}
PageView 내부에는 @State var currentPage = 0 를 추가한뒤, PageViewController 에 인자로 currentPage 를 전달한다. @State 가 붙은 프로퍼티는 변수명 앞에 $ 심볼을 붙여줘야 바인딩이 이루어진다.
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
}
}
@State var currentPage 변수의 초기값을 변경하면서 바인딩이 잘 이루어졌는지 체크
PageViewController 내부 Coordinator 클래스가 UIPageViewControllerDelegate 프로토콜을 준수하도록 추가한다. Coordinator 클래스 안에 pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) 메소드를 추가한다.
페이지 변경 애니메이션이 끝나면 SwiftUI는 pageViewController 메소드를 호출하고, 지금의 뷰컨트롤러의 인덱스를 찾고 바인딩된 변수를 업데이트 한다.
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController)
{
parent.currentPage = index
}
}
}
커스텀 페이지 컨트롤을 더하는것은 최상단 링크를 참조.