【Interfacing with UIKit編】初心者がswiftUIチュートリアルをやって疑問に思ったことを調査して記していく
はじめに
このswiftUI tutorialは、今までで一番意味不明だったかもしれません。特に、初心者の僕には、そもそもUIkitって何って感じでしたので、理解に苦しみました。
最初に、コード内で使われているメソッドやプロトコルについて触れてからコードを確認していきます。
UIKitとは
公式:UIKit
UIを作成するためのフレームワークです。
UIのフレームワークにはswiftUIもありますが、このUIKitの方が歴史が長く実装できる機能の幅も広いです。今回は、その機能の一部をswiftUIの中で使ってみる!!というチュートリアルとなっています。
UIView と ViewController
UIKit のUIを表示・管理する機能としてUIViewとViewControllerが提供されています。(他にもたくさんありますが、今回はこの二つに重点を置いて進めます)
UIView
画面上の長方形領域のコンテンツを管理するオブジェクト。
公式:UIView
画面領域に表示しているUIの管理をしているクラスですね。
さらに、その下に連なるツリー構造も管理しているようです。
ViewController
UIKit アプリのビュー階層を管理するオブジェクト。
- ビューの内容を更新します。通常は、基礎となるデータの変更に応じて行われます。
- ビューとのユーザーインタラクションへの応答
- ビューのサイズ変更とインターフェイス全体のレイアウトの管理
- アプリ内の他のオブジェクト (他のビュー コントローラーを含む) との調整
UIの階層構造の管理に加え、UIの更新や動作処理の管理もしています。
UIViewRepresentable
UIKit ビューのラッパー。そのビューを SwiftUI ビュー階層に統合するために使用します。
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 インスタンスの適切なメソッドに渡します。
と明記されていますが、
coordinator・transaction・enviroment
の3つが情報として含まれています。
UIViewControllerRepresentable
今回使われているのはこっちです。
UIViewControllerRepresentableインスタンスを使用して、SwiftUI インターフェイスでUIViewControllerオブジェクトを作成および管理します。
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
コンテンツのページ間のナビゲーションを管理するコンテナ ビュー コントローラ。
ページ遷移に伴う変更を管理してくれます。
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
デリゲートのメソッドは、ジェスチャベースのナビゲーションと方向の変更に応じて呼び出されます。
ページ遷移や画面の向きが変わった際にしたい処理を、こいつに指定します。
インスタンスは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(継承)との違いも含めて以下の記事が参考になります。
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
水平方向の一連のドットを表示するコントロール。各ドットは、アプリのドキュメントまたはその他のデータ モデル エンティティ内のページに対応します。
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