[Swift] スワイプで画面切り替えをしたい

  • 5
    Like
  • 0
    Comment

実行環境

  • Xcode(v 9.1)
  • Swift(v 4.0)

sample.gif

はじめに

PagerViewTabHostを実装しているAndroidアプリをiOSに移植させようとした時に、唯一困ってしまったのがこの「スワイプでの画面切り替え」でした。

UIPageViewControllerなるものがあったので「お!いける!」と思ったのも束の間、UIPageViewControllerに他の部品を綺麗に置くことが出来ず、断念...。

そこで「ContainerView」なる物を見つけ、そこに先程のUIPageViewControllerを突っ込み、Tabの代用品としてUISegmentedControlを設置することにしました。
*今回はUISegmentedControlを用いましたが、UILabelやUIButtonでも出来ます。

やるべき事は以下の作業です。
1. ベースとなるView(以下BaseView)に部品設置
2. スクロール時に表示するViewにidを設定
3. コードをゴリゴリ書いていく

以上コードの量も比較的少なく難しいロジックも無いので、理解しやすいかと思います。

1. ベースとなるView(以下BaseView)に部品設置

StoryBoardを使って部品設置、Segue設定をしていきます。

まず「SegmentedControl」と「ContainerView」を設置します。
スクリーンショット 2017-11-12 20.02.09.png

次に「ContainerView」に自動的に生成された「ViewController」を削除し、「PageViewController」に入れ替ます。
*Segueを繋げる時は「ContainerView」をControl+ドラッグで「PageViewController」まで繋げ、Embedを選択して下さい。
スクリーンショット 2017-11-12 20.04.14.png

これでBaseViewへの部品設置は完了です。

2. スクロール時に表示するViewにidを設定

こちらもStoryBoardを使っていきます。

まず「ViewController」を追加します。今回は「FirstView」と「SecondView」の二つ設置しました。
また分かりやすい様に色やラベルを追加してあります。
スクリーンショット 2017-11-12 20.08.11.png

そしてもれなくidを振りましょう。今回はそれぞれ"first"と"second"としました。
Restoration Idも忘れずに!!

3. コードをゴリゴリ書いていく

お待ちかね。みんな大好きコードのお時間です。

まずは全体のコードを添付しておきます。

ViewController.swift
import UIKit

class ViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {

    let idList: [String] = ["first", "second"]

    var pageViewController: UIPageViewController!
    var viewControllers: [UIViewController] = []

    @IBOutlet weak var selectTab: UISegmentedControl!

    override func viewDidLoad() {
        super.viewDidLoad()

        for id in idList {
            viewControllers.append((storyboard?.instantiateViewController(withIdentifier: id))!)
        }

        pageViewController = childViewControllers[0] as! UIPageViewController
        pageViewController.setViewControllers([viewControllers[0]], direction: .forward, animated: true, completion: nil)
        pageViewController.dataSource = self
        pageViewController.delegate = self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        let index = idList.index(of: viewController.restorationIdentifier!)!
        if (index > 0) {
            return storyboard!.instantiateViewController(withIdentifier: idList[index - 1])
        }
        return nil
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        let index = idList.index(of: viewController.restorationIdentifier!)!
        if (index < idList.count - 1) {
            return storyboard!.instantiateViewController(withIdentifier: idList[index + 1])
        }
        return nil
    }

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        let index = idList.index(of: (pageViewController.viewControllers?.first!.restorationIdentifier)!)
        self.selectTab.selectedSegmentIndex = index!
    }

    @IBAction func selectedTab(_ sender: UISegmentedControl) {
        switch sender.selectedSegmentIndex {
        case 0:
            pageViewController.setViewControllers([viewControllers[0]], direction: .reverse, animated: true, completion: nil)
            break
        case 1:
            pageViewController.setViewControllers([viewControllers[1]], direction: .forward, animated: true, completion: nil)
            break
        default:
            return
        }
    }

}

細かく説明していくと、

<初期設定部分>

class ViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {

    let idList: [String] = ["first", "second"]

    var pageViewController: UIPageViewController!
    var viewControllers: [UIViewController] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        for id in idList {
            viewControllers.append((storyboard?.instantiateViewController(withIdentifier: id))!)
        }

        pageViewController = childViewControllers[0] as! UIPageViewController
        pageViewController.setViewControllers([viewControllers[0]], direction: .forward, animated: true, completion: nil)
        pageViewController.dataSource = self
        pageViewController.delegate = self
    }

}

まずidListに先程振り分けたidの配列を定義します。
そしてその配列を用いてviewControllers配列にスクロール時表示するViewControllerを格納していきます。

「ContainerView」に繋がれた「PageViewController」はBaseViewのChildViewControllers配列の先頭に格納されているので、それを引っ張り出してきます。
最後に「UIPageViewControllerDataSource」と「UIPageViewControllerDelegate」と接続するコードを追加して初期設定は完了です。

func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    let index = idList.index(of: viewController.restorationIdentifier!)!
    if (index > 0) {
        return storyboard!.instantiateViewController(withIdentifier: idList[index - 1])
    }
    return nil
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    let index = idList.index(of: viewController.restorationIdentifier!)!
    if (index < idList.count - 1) {
        return storyboard!.instantiateViewController(withIdentifier: idList[index + 1])
    }
    return nil
}

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    let index = idList.index(of: (pageViewController.viewControllers?.first!.restorationIdentifier)!)
    self.selectTab.selectedSegmentIndex = index!
}

「viewControllerBefore」には最初のページ以外で左→右スワイプされた時、1ページ戻る動作を書きます。
一方「viewControllerAfter」には最後のページ以外で右→左スワイプされた時、1ページ進む動作を書きます。
それぞれ現在表示されている「ViewController」のRestroration Idからページ番号を取得しています。

また「didFinishAnimating」にはスワイプが完了した時の動作を書きます。
今回は「SegmentedControl」の選択状態を表示されている「ViewController」とリンクする様にしています。

@IBAction func selectedTab(_ sender: UISegmentedControl) {
    switch sender.selectedSegmentIndex {
    case 0:
        pageViewController.setViewControllers([viewControllers[0]], direction: .reverse, animated: true, completion: nil)
        break
    case 1:
        pageViewController.setViewControllers([viewControllers[1]], direction: .forward, animated: true, completion: nil)
        break
    default:
        return
    }
}

「SegmentedControl」を右クリックしてValue Changedを選択し、コードとリンクしています。
「SegmentedControl」も配列になっているので、その添字とviewControllersをリンクして画面切り替えする様にします。

終わりに

いかがでしたでしょうか。
どうしても「ContainerView」と見てだけで面倒臭そうな気がしてしまいますが、実際に使ってみるとなんてことないですね。「ContainerView」をうまく使えばより複雑な画面構成に対応できそうですし、一つの画面内に全く別のViewが混在する様な時にはスッキリ整理して管理できそうです。

ぜひみなさんもトライして見て下さい!

P.S.

Swift4になってから色々とメソッド名やら記述方法が変わっていて、ドキュメント探しに一苦労しているのは私だけでしょうか。。。