SwiftUI には UIPageViewController に変わるViewがありません(2020/06/05現在)。
そのため、UIViewControllerRepresentable を継承したラッパーを構造体(ここでは PageViewController と呼ぶ)を作成し、それを保持する SwiftUI 表示用のView(ここでは PageView とする)を作成する必要があります。
基本的なソースコードは、以下のApple公式チュートリアルのコードをほぼそのまま使っています。
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
【SwiftUI】PageViewをSwiftUIで実現する方法
PageViewController
先ずは大元の UIPageViewController をラップした PageViewController です。
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
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
}
}
}
}
現在のページ番号は、@Binding var currentPage: Int で管理しており、後述の ContentView が保持している currentPage が、これまた後述の PageView を経由してバインディングされています。
PageView
続いて、ContentView に組み込む PageView を作成します。
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@Binding var currentPage: Int
init(_ views: [Page], currentPage: Binding<Int>) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
self._currentPage = currentPage
}
var body: some View {
VStack {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
}
}
}
イニシャライザーで、ページ要素の View を配列を受け取り、UIHostingController に変換し保持します。
また、ページ番号を管理する変数を @Binding として受け取ります。
実際の View の中身は、先ほど作成した PageViewController がセットされています。
使用例
今回は、3つの View を PageView に格納しスワイプで切り替えられるようになっています。
import SwiftUI
struct ContentView: View {
@State private var currentPage = 0
var body: some View {
VStack {
PageView([
AnyView(Page1()),
AnyView(Page2()),
AnyView(Page3())
], currentPage: $currentPage)
}
}
}
struct Page1: View {
var body: some View {
Text("Page1")
}
}
struct Page2: View {
var body: some View {
Text("Page2")
}
}
struct Page3: View {
var body: some View {
Text("Page3")
}
}
ページ番号を管理する変数が、@State private var currentPage です。これを PageView にバインドさせ、さらに PageView 側で PageViewController にバインドされて行きます。
PageView に渡す View の配列要素は、同じ型の View でなければなりません。そのため、異なる型の View を渡したい場合(そういった場合の方が多いと思いますが)、AnyView に変換して格納する必要があります。
最後に、今回はサンプルではページングがループするようになっていますが、ループさせたくない場合は、以下のように、次のページが最初・最後の場合に nil を返すようにします。
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
// ループさせない場合は nil を返す
return nil
// 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 {
// ループさせない場合は nil を返す
return nil
// return parent.controllers.first
}
return parent.controllers[index + 1]
}
以上
コメントを残す