[Swift]PageViewControllerを使用したチュートリアルを実装する過程を記録してみた

# 始めに
可愛いチュートリアル画面を探していたときに、Figmaのアプリを見つけた。
ログインする前に紹介ページを簡単に見れるのは良いなと思ったので作成してみる。

:::note warn
Storyboardでなく、コードでの実装となりますが、
Storyboardでやる場合も何となく雰囲気は掴めると思います！
:::

# 作成したい画面
上部がスライドできるようになっている！
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/36a3b9d9-c519-f0d5-74f3-3b8403a9d6f1.jpeg"> 　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/785a93a3-95cd-b255-0880-f418daa13122.jpeg"> 　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/8d3437d7-e4d0-ed4d-8558-9b10bba43701.jpeg">

# 実装するには？
実装するには下記の知識が必要そうだ。
* ContainerView
* UIPageViewController
* UIPageControl

# ContainerViewとは？
ChatGPTに聞いてみた。
> Swiftでは、ContainerViewは親ビュー内に他のビューコントローラのコンテンツを埋め込むための便利な方法です。ContainerViewは、親ビューコントローラ内に子ビューコントローラのビューを配置し、子ビューコントローラのライフサイクルを管理します。

Figmaの例で考えると、スライド箇所には別のViewControllerが埋め込まれているということ！
使い方については、下記を参考にする！

https://qiita.com/hohohehetoto/items/eefb3a38da9b776c9513

# UIPageViewControllerとは？
ChatGPTに聞いてみた。
> UIPageViewControllerは、スワイプジェスチャーやボタンなどのユーザーアクションに応じて、異なるビューコントローラを表示することができます。これにより、画面をスライドすることで複数のコンテンツページを操作することができます。典型的な使用例は、本のページめくりのようなアプリケーションや、画像ギャラリーなどです。
>
> UIPageViewControllerは、ページ間のトランジションやアニメーションを管理し、表示するビューコントローラのスタックを管理します。さまざまな表示モードをサポートし、横スクロールや縦スクロール、ページめくりのようなアニメーションなどをカスタマイズすることもできます。
>
> UIPageViewControllerは、ページ間のナビゲーションを簡単に実装するための便利なクラスであり、多くのアプリケーションで使用されています。

使い方については、下記を参考にするとわかりやすい！

https://naoya-ono.com/swift/swift-uipageviewcontroller/

https://bombrary.github.io/blog/posts/iphone-pageview/

https://qiita.com/temoki/items/5702ea5d92049c460d74

# UIPageControlとは？
ChatGPTに聞いてみた。
> UIPageControlは、iOSアプリケーションでページングコンテンツを管理するためのユーザーインターフェース要素です。UIPageControlは、画面の下部などに表示され、現在のページ位置を示すドットまたはインジケーターを表示します。
>
> 通常、UIPageControlはUIScrollViewやUIPageViewControllerなどのビューコンテナと組み合わせて使用されます。ユーザーがスクロールすると、UIPageControlのドットも対応するように更新され、現在表示されているページの位置を示します。
>
> UIPageControlは、通常は現在のページとそれ以外のページを区別するために、異なる色やスタイルのドットを使用します。ユーザーはドットをタップすることで、特定のページに直接移動することもできます。
>
> UIPageControlは、ユーザーにコンテンツのスクロール可能性を示し、ページ間のナビゲーションを簡単にするための視覚的な手段として非常に便利です。

使い方については、下記を参考にするとわかりやすい！

https://itnext.io/ios-custom-uipagecontrol-with-uipageviewcontroller-2d02db5ab090

:::note warn
UIPageControlは、UIPageViewControllerにてデフォルトで使用できるようになっているため、新規で作成しなくても大丈夫！

:::

# 実際にコードを書いていく
使い方を知れたところで実際に使って慣らしていく！
## 実践: Figmaアプリを真似る
1\. レイアウトの作成
どの端末でも潰れないようAutoLayoutを設定する。

完成図
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/985eae78-3108-0737-657d-8022d195339f.png">  <img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/45c03a9c-1efc-6b59-6c6c-0927282dd5af.png">

大事なのは、`containerView`を`UIView`で定義してあること。このUIViewにPageViewControllerを追加して表示する。
```swift:AuthenticationViewController.swift
import UIKit

class AuthenticationViewController: UIViewController {
    private let containerView = UIView()
    // 省略
}
```
2\. PageViewControllerに表示する`IntroductionViewConotroller`を作成する。
Figmaアプリの仕様は、全てのページでImageとTextが表示されているので、一つのViewControllerを共通して使うように設定する。
```swift:IntroductionViewController.swift
import UIKit

class IntroductionViewController: UIViewController {
    
    private let stackView = UIStackView()
    private let introductionImageView = UIImageView()
    private let introductionLabel = UILabel()
    
    
    // MARK: - View Life Cycle
    // ①
    init(introductionImage: UIImage, introductionText: String) {
        introductionImageView.image = introductionImage
        introductionLabel.text = introductionText
        super.init()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    // 省略

}


```
①について、初期化時に`UIImage`と`String`をもらうように設定している。これにより、`IntroductionViewController`を複数使い回すことができるようになる。

3\. PageViewControllerを作成する。
基本的なPageViewControllerの設定について理解できていれば問題なくできる。
```swift:MainPageViewController.swift
import UIKit

class MainPageViewController: UIPageViewController {
    
    private var controllers = [UIViewController]()
    // ①
    private let pageData: [(imageName: String, text: String)] = [
        ("page1", "study hard"),
        ("page2", "worry a lot"),
        ("page3", "grow up")
    ]
    
    
    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        setUpPageViewController()
    }
    
    private func setUpPageViewController() {
        // ②
        pageData.forEach {
            let introductionVC = IntroductionViewController(introductionImage: UIImage(named: $0.imageName)!,
                                                            introductionText: $0.text)
            controllers.append(introductionVC)
        }
        setViewControllers([controllers[0]], direction: .forward, animated: true)
        self.dataSource = self
    }
    
}


// MARK: - UIPageViewControllerDataSource

extension MainPageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController),
           index > 0 {
            return controllers[index - 1]
        }
        return nil
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if let index = controllers.firstIndex(of: viewController),
           index < controllers.count - 1 {
            return controllers[index + 1]
        }
        return nil
    }
}

```
PageViewControllerの設定については、["UIPageControllerとは？"](https://qiita.com/drafts/1e81514c420324c8a4c6/edit#uipageviewcontroller%E3%81%A8%E3%81%AF)で確認する。それ以外の箇所について説明を書く。①については、pageViewConotrollerに表示する情報をページ毎に配列で持っている。ここの配列を修正するだけでページの削除と追加が可能になるので楽！②については、配列を順番に回していき、必要な数分IntroductionViewControllerを生成している。

4\. ContainerViewにPageViewControllerを表示する。

完成図
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/98ce8224-18e1-9bd8-4565-fb546c1d3cfe.png">  <img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/06b46bc9-bb07-4221-e35d-1a21a9b0b14b.png">  <img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/ae772977-37cd-4c7b-3dcd-cd4048078593.png">  <img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/1fdf6467-aa33-7b5c-da77-fc4e501fb0d8.png">
基本的なContainerViewの設定について理解できていれば問題なくできる。
```swift:AuthenticationViewController.swift
import UIKit

class AuthenticationViewController: UIViewController {

    private let containerView = UIView()

    //　省略

    private func setUpMainPageViewController() {
        // ①
        let mainPageVC = MainPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        addChild(mainPageVC)
        mainPageVC.view.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(mainPageVC.view)
        // ②
        NSLayoutConstraint.activate([
            mainPageVC.view.topAnchor.constraint(equalTo: containerView.topAnchor),
            mainPageVC.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            mainPageVC.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            mainPageVC.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])
        mainPageVC.didMove(toParent: self)
    }
}
```
①については、["ContainerViewとは？"](https://qiita.com/drafts/1e81514c420324c8a4c6/edit#containerview%E3%81%A8%E3%81%AF)を参考にすれば理解できる。②については、AutoLayoutの設定になるので、下記記事を参考にする。

https://qiita.com/hohohehetoto/items/aecdb0c00686b070660c

5\. UIPageControlを使用して、ページ数を可視化する。

完成図
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/76d1e6da-02ae-50ba-ec7f-37a26a7066dd.jpeg">　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/29abb769-710f-1fa9-3539-5ea4554a9261.jpeg">　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/d4542455-5790-6e90-49a6-cf82148c029a.jpeg">　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/2456bfc8-f8f6-58c8-31f4-3546b5be8d5a.png">

```swift:MainPageViewController.swift
import UIKit

class MainPageViewController: UIPageViewController {
    
    private var controllers = [UIViewController]()

    
    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        setUpPageViewController()
    }
    
    private func setUpPageViewController() {
        // 省略
        // ①
        self.delegate = self
        // ②
        UIPageControl.appearance().pageIndicatorTintColor = .systemGray6
        UIPageControl.appearance().currentPageIndicatorTintColor = .black
    }
    
}

// MARK: - UIPageViewControllerDelegate
// ①
extension MainPageViewController: UIPageViewControllerDelegate {
    func presentationCount(for pageViewController: UIPageViewController) -> Int {
        return controllers.count
    }

    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
        return 0
    }
}

```
①について、Indicatorを表示するために必要な処理。`presentationCunt`メソッドで総数を返す。`presentationIndex`で最初に表示するViewControllerの`Index`を返す。ここでの処理を忘れると表示されないので注意する。Indicatorを表示する処理については、["現在のページ位置の表示"](https://qiita.com/temoki/items/5702ea5d92049c460d74#%E7%8F%BE%E5%9C%A8%E3%81%AE%E3%83%9A%E3%83%BC%E3%82%B8%E4%BD%8D%E7%BD%AE%E3%81%AE%E8%A1%A8%E7%A4%BA)に詳しく載っているので、読んでおくと良い！例えばボタンをタップした場合でもスクロールをさせたいとなると追加して処理を書く必要も出てくるので‥。
②について、マルポチの背景色と、選択された場合の色を指定している。`UIPageControl.appearance()`に対して設定をすると、全てのUIPageControlに対して設定が適用されるので注意する。色の変更処理に関しては、["ページインジケータの色を変える"](https://qiita.com/temoki/items/5702ea5d92049c460d74#%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%B1%E3%83%BC%E3%82%BF%E3%81%AE%E8%89%B2%E3%82%92%E5%A4%89%E3%81%88%E3%82%8B)に詳しく載っているので、読んでおくと良い！
# 終わりに
ここまでご覧いただきありがとうございました！
また、優れた知識と経験を共有してくださる皆様に感謝です！本当にありがとうございました！