💡

【Interfacing with UIKit編】初心者がswiftUIチュートリアルをやって疑問に思ったことを調査して記していく

2023/10/01に公開

はじめに

このswiftUI tutorialは、今までで一番意味不明だったかもしれません。特に、初心者の僕には、そもそもUIkitって何って感じでしたので、理解に苦しみました。

最初に、コード内で使われているメソッドやプロトコルについて触れてからコードを確認していきます。

UIKitとは

公式:UIKit
UIを作成するためのフレームワークです。

UIのフレームワークにはswiftUIもありますが、このUIKitの方が歴史が長く実装できる機能の幅も広いです。今回は、その機能の一部をswiftUIの中で使ってみる!!というチュートリアルとなっています。

UIView と ViewController

UIKit のUIを表示・管理する機能としてUIViewとViewControllerが提供されています。(他にもたくさんありますが、今回はこの二つに重点を置いて進めます)

UIView

画面上の長方形領域のコンテンツを管理するオブジェクト。

公式:UIView

画面領域に表示しているUIの管理をしているクラスですね。
さらに、その下に連なるツリー構造も管理しているようです。

ViewController

UIKit アプリのビュー階層を管理するオブジェクト。

  • ビューの内容を更新します。通常は、基礎となるデータの変更に応じて行われます。
  • ビューとのユーザーインタラクションへの応答
  • ビューのサイズ変更とインターフェイス全体のレイアウトの管理
  • アプリ内の他のオブジェクト (他のビュー コントローラーを含む) との調整

公式:UIViewController

UIの階層構造の管理に加え、UIの更新や動作処理の管理もしています。

UIViewRepresentable

UIKit ビューのラッパー。そのビューを SwiftUI ビュー階層に統合するために使用します。

UIViewRepresentable

UIViewをswiftUIで使えるようにするためのプロトコルです。

UIView作成時に一度だけ呼ばれるメソッド

func makeUIView(context: Self.Context) -> Self.UIViewType

swiftUIからの変更を受けてViewを更新するメソッド

func updateUIView(Self.UIViewType, context: Self.Context)

上記の二つのメソッドは必ず実装する必要があります。

引数のcontextはUIViewRepresentableContextのtypealias(別名)です。
公式に

UIViewRepresentableContext構造体には、システムの現在の状態に関する詳細が含まれています。ビューを作成および更新するとき、システムはこれらの構造の1つを作成し、それをカスタム UIViewRepresentable インスタンスの適切なメソッドに渡します。

と明記されていますが、
coordinatortransactionenviroment
の3つが情報として含まれています。

UIViewControllerRepresentable

今回使われているのはこっちです。

UIViewControllerRepresentableインスタンスを使用して、SwiftUI インターフェイスでUIViewControllerオブジェクトを作成および管理します。

公式:UIViewControllerRepresentable

UIViewControllerをswiftUIで使用するためのプロトコルです。

UIViewController作成時に一度だけ呼ばれるメソッド

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

swiftUIからの変更を受けてViewControllerを更新するメソッド

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

contextはUIViewRepresentableで説明したものとほとんど一緒で、
UIViewControllerRepresentableContextのtypealiasです。

coordinator

一度紹介していますが、ここで少し説明します。

UIKitの変更をswiftUIに伝達するためのインスタンスを保有します。
なので、動作を伴うオブジェクトを作成する場合はcoordinatorを使用することになります。
このチュートリアルでは、ページ遷移をになっています。

インスタンスはcontextのプロパティから参照することができます。

context.coordinator

ただ、coordinator用のクラスは自作する必要があります。
作成には

func makeCoordinator() -> Self.Coordinator

を使います。

coordinatorはプロトコル内のCoordinatorタイプとして宣言されており

@MainActor public let coordinator: Representable.Coordinator

Coodinatorタイプは、makeCoodinatorの戻り値にて決定されます。
なので、coordinator用のクラスの名前は任意です。
このチュートリアルで言えば、Coordinatorクラスが該当します。

UIPageViewController

コンテンツのページ間のナビゲーションを管理するコンテナ ビュー コントローラ。

公式:UIPageViewController

ページ遷移に伴う変更を管理してくれます。

dataSource
複数枚の時は

weak open var dataSource: UIPageViewControllerDataSource? 

へUIPageViewControllerDataSourceプロトコルに準拠したインスタンスの入ったcoordinatorを参照させる。
coordinatorを介さないと、ページ遷移の動作が行われません。

今回のチュートリアルでは、ページが3枚あるのでdataSourceを使用しています。

ページが一枚のときは

func setViewControllers(
    _ viewControllers: [UIViewController]?,
    direction: UIPageViewController.NavigationDirection,
    animated: Bool,
    completion: ((Bool) -> Void)? = nil
)

でUIViewControllerを指定

delegate

デリゲートのメソッドは、ジェスチャベースのナビゲーションと方向の変更に応じて呼び出されます。

delegate

ページ遷移や画面の向きが変わった際にしたい処理を、こいつに指定します。
インスタンスはUIPageViewControllerDelegateに準拠している必要があります。

コード確認

PageViewController.swift

公式のコード
import SwiftUI
import UIKit


struct PageViewController<Page: View>: UIViewControllerRepresentable {
    var pages: [Page]
    @Binding var currentPage: Int

    // ここでcontext.coordinatorのタイプを指定している
    // 今回は、Coordinatorクラス型
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    // ここで、UIViewControllerを作成
    // 最初に一度だけ呼ばれる
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            // アニメーションスタイル指定
            transitionStyle: .scroll,
            // 向きを水平に指定
            navigationOrientation: .horizontal)
        // 複数枚なのでdataSourceへUIPageViewControllerDataSourceに準拠したクラスを
        // coordinatorを介して参照させる
        pageViewController.dataSource = context.coordinator
        // ページ遷移後の処理を含むメソッドを持つUIPageViewControllerDelegateに準拠したクラスを
        // coordinatorを介して参照させる
        pageViewController.delegate = context.coordinator

        return pageViewController
    }


    // swiftUIからの変更を受けて呼ばれる
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        // update時に表示するページをセット
        pageViewController.setViewControllers(
            [context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
    }


    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController
        // UIViewController型の配列作成
        //  controllers: UIViewController = [] と同義
        var controllers = [UIViewController]()


        init(_ pageViewController: PageViewController) {
            parent = pageViewController
            // UIHostingControllerはswiftUIのViewをUIKitで扱えるようにするラッパー
            controllers = parent.pages.map { UIHostingController(rootView: $0) }
        }
        
        // 左にスクロールした際に実行
        // 戻り値:表示されるUIViewControllerの前のUIViewController
        func pageViewController(
            _ pageViewController: UIPageViewController,
            // 現在のUIViewController
            viewControllerBefore viewController: UIViewController) -> UIViewController?
        {
            // 引数の配列内におけるindexを取得
            // 取得に失敗でnilを返す
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            // indexが0の場合は、前がないので最後の要素を返す
            if index == 0 {
                return controllers.last
            }
            // 現在のUIViewControllerの一つ前の要素を返す
            return controllers[index - 1]
        }

        // 右にスクロールした際に実行
        // 戻り値:表示されるUIViewControllerの後のUIViewController
        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController?
        {
            // 引数の配列内におけるindexを取得
            // 取得に失敗でnilを返す
            guard let index = controllers.firstIndex(of: viewController) else {
                return nil
            }
            // indexが配列の最後の時は、次の要素がないので最初の要素を返す
            if index + 1 == controllers.count {
                return controllers.first
            }
            // 現在のUIViewControllerの一つ後の要素を返す
            return controllers[index + 1]
        }

        // ページ遷移や画面の向きが変わった際に実行
        func pageViewController(
            //
            _ pageViewController: UIPageViewController,
            didFinishAnimating finished: Bool,
            previousViewControllers: [UIViewController],
            // ページ遷移や画面の向きが正常に行われた場合にtrue
            transitionCompleted completed: Bool) {
            if completed,
               // 現在表示されているUIViewControllerを取得
               let visibleViewController = pageViewController.viewControllers?.first,
               // index取得
               let index = controllers.firstIndex(of: visibleViewController) {
                // indexをswiftUIの変数currentPageに入れて返す
                parent.currentPage = index
            }
        }
    }
}

SwiftUIビュー階層をUIKitで管理できるようにするために、UIHostingControllerでラップ。

「UIPageViewControllerDataSource」と「UIPageViewControllerDelegate」
に準拠するための3つのメソッド
UIPageViewControllerDataSource に準拠するために実装するメソッド2つ

// 左スクロールで呼ばれる
// 指定されたビューコントローラーの 前 のビュー コントローラーを返します。
//必須
func pageViewController(UIPageViewController, viewControllerBefore: UIViewController) -> UIViewController?
// 右スクロールで呼ばれる
// 指定されたビューコントローラーの 後 のビュー コントローラーを返します。
// 必須
func pageViewController(UIPageViewController, viewControllerAfter: UIViewController) -> UIViewController?

UIPageViewControllerDelegate に準拠するために実装するメソッド

// ジェスチャー駆動の遷移が完了した後に呼び出される
// 実行したい処理を中に書く
// 必須ではない
func pageViewController(UIPageViewController, didFinishAnimating: Bool, previousViewControllers: [UIViewController], transitionCompleted: Bool)

delegate(委譲)は数あるデザインパターンの一つです。
inheritance(継承)との違いも含めて以下の記事が参考になります。
https://ikenox.info/blog/inheritance-and-delegation-and-interface/

PageControl.swift

公式のコード
import SwiftUI
import UIKit

struct PageControl: UIViewRepresentable {
    var numberOfPages: Int
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    // swiftUIで表示するUIView作成
    // 最初に一度だけ呼ばれる
    func makeUIView(context: Context) -> UIPageControl {
        // ページ位置を示すドットのUIを作成
        let control = UIPageControl()
        // ドットの数を指定
        control.numberOfPages = numberOfPages
        control.addTarget(
            // メソッドのあるオブジェクトを指定
            context.coordinator,
            // 実行するメソッドを指定
            action: #selector(Coordinator.updateCurrentPage(sender:)),
            // 指定したメソッドのトリガーになるイベントを指定
            for: .valueChanged)

        return control
    }

    // swiftUIからの変更を受けて呼び出される
    // 引数1:自身を指す(Viewオブジェクト)
    // 引数2:現在の状態に関する情報を含む構造体
    func updateUIView(_ uiView: UIPageControl, context: Context) {
        uiView.currentPage = currentPage
    }

    // swiftUIへ変更をすたえるためのクラス
    class Coordinator: NSObject {
        var control: PageControl

        init(_ control: PageControl) {
            self.control = control
        }

        // イベント発行時に実行される
        @objc
        func updateCurrentPage(sender: UIPageControl) {
            // swiftUIの持つ変数の値を変更
            control.currentPage = sender.currentPage
        }
    }
}

makeUIViewでUIViewを作成しています。
今回は、ページの位置を示す白いドットのUIです。

UIPageControl

水平方向の一連のドットを表示するコントロール。各ドットは、アプリのドキュメントまたはその他のデータ モデル エンティティ内のページに対応します。

公式:UIPageControl

numberOfPagesでドットの数を指定し、addTargetでタッチ・ドラッグに伴う処理をViewに紐づけています。

addTargetの引数について

func addTarget(
    // 実行したいメソッドを持つオブジェクト
    _ target: Any?,
    // 実行するメソッド
    action: Selector,
    // 指定したメソッドのトリガーになるイベント
    for controlEvents: UIControl.Event
)

actionの #selector() はobject-cの記法のようです。
controlEventsに指定するイベントの一覧はUIControl.Eventに載ってます。
今回の valueChanged

コントロールをタッチもしくはドラッグするか、その他の方法で操作すると、一連の異なる値が出力されます。

と公式に書かれています。
「一連の異なる値が出力されます。」の意味がよくわかりませんが、
タッチやドラッグがトリガーになってるんだと思います。

トリガー発生でcurrentPageを変更して、swfitUIに変更が伝達された後、ViewControllerが更新されるという流れですね。

あとは、PageViewで表示して終了です。

まとめ

まじで意味不な部分が多くて困りました。

間違いがあればお気軽にコメントください。

最後までお付き合い頂きまして、誠にありがとうございました。

Discussion