【Swift 3】UIPageViewControllerとUINavigationControllerを使ってスワイプで画面切り替えをしよう

ども。

最近よく見る横スワイプで画面を切り替えるやつを実装してみたので紹介します。

[iOS] SwiftでSmartNewsみたいな、横スワイプで画面を切り替えるライブラリを作りました」を参考に(削ったり付け加えたり)しました。

NavigationControllerを使うのが初めてでなかなか苦戦したのでその辺も含めて紹介しますね〜。

 

まず今回作りたいのはこういうやつ。

 

とりあえず細かいところは置いておいてざっくりと説明していきます。

 

navigationController上にpageViewControllerをのせて、そこに ViewControllerを切り替えているという感じだと思います。

なので既にViewControllerを作っている場合でも大丈夫ですし、切り替えたページごとにViewControllerが配置されているので扱いやすいです。

 

UINavigationControllerのサブクラスを作成する

おもなコードは全てここに書きます。

File > New > File

 

Source > Cocoa Touch Class > Next

 

Subclass of をUINavigationControllerにしてファイルを作成

 

まずは必要なDelegateを書いておきます。

class SwipeNavigationViewController: UINavigationController ,UIPageViewControllerDelegate,UIPageViewControllerDataSource,UIScrollViewDelegate{
         /*  ~~  */
}

・UIPageViewControllerDelegate

・UIPageViewControllerDataSource

・UIScrollViewDelegate

を付け加えます。

 

今回はナビゲーションバーを表示させたくないので消しておきます。

    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationBar.isHidden = true
    }

viewDidLoadに書くことはこんだけ。

 

 

PageViewControllerの設定をしていきます。

    var pageController:UIPageViewController!
    //ページングしたいviewControllerをいれる
    var viewControllerArray:[UIViewController] = []

    //PageViewControllerのセットアップ
    func setupPageViewController(){
        //NavigationControllerのtopViewControllerを生成
        pageController = self.topViewController as! UIPageViewController
        
        //デリゲートを設定
        pageController.delegate = self
        pageController.dataSource = self
        
        //pageControllerに最初のviewControllerを設定
        pageController.setViewControllers([viewControllerArray[0]], direction: .forward, animated: true, completion: nil)


        //後述
        self.syncScrollView()
 
    }

 

このself.topViewControllerはナビゲーションコントローラーのスタックの一番上にあるビューコントローラを返します。

 

syncScrollViewの中身を書いていきます。

    var pageScrollView:UIScrollView!

    func syncScrollView(){
        for view in pageController.view.subviews{
            if view.isKind(of:UIScrollView.self){
                pageScrollView = view as! UIScrollView
                pageScrollView.delegate = self
            }
        }
    }

 

先ほどのpageControllerの中からUIScrollViewを探し出し、それにデリゲートを設定しています。

 

viewWillAppearでsetupPageViewControllerを呼び出しておきます。

    override func viewWillAppear(_ animated: Bool) {
        self.setupPageViewController()
    }

 

スワイプ時の処理を実装しよう

スワイプした時の処理を書いていきます。

その前に現在のインデックスを返す関数を用意しておきます。

    func indexOfController(viewController:UIViewController) -> Int{
        for i in 0 ..< viewControllerArray.count{
            if(viewController == viewControllerArray[i]){
                return i
            }
        }
        return NSNotFound
    }

 

それぞれの方向にスワイプした時の処理を書きます

    //左にスワイプしたとき(ページが進む)
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        //現在の位置を取得
        var index:Int = self.indexOfController(viewController:viewController)
        if(index == NSNotFound){
            return nil
        }
        index += 1
        if(0 <= index && index < viewControllerArray.count){
            return viewControllerArray[index]
        }
        return nil
    }
    
    //右にスワイプしたとき(ページが戻る)
    func pageViewController(_ pageViewController:UIPageViewController,viewControllerBefore viewController:UIViewController) -> UIViewController?{
        var index:Int = self.indexOfController(viewController:viewController)
        if(index == NSNotFound){
            return nil
        }
        index -= 1
        if(0 <= index && index < viewControllerArray.count){
            return viewControllerArray[index]
        }
        return nil
    }

 

次(前)のビューコントローラーがあった場合のみそれを返します。

 

最初からNavigationControllerを表示する時

アプリを開いた時点でNavigationControllerを表示する時はAppDelegate.swift内に処理を書きます。

 

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        let pageController:UIPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        let navigationController:SwipeNavigationViewController = SwipeNavigationViewController(rootViewController: pageController)
        
        //適当にViewControllerを用意
        let demo:UIViewController = UIViewController()
        let demo2:UIViewController = UIViewController()
        let demo3:UIViewController = UIViewController()
        let demo4:UIViewController = UIViewController()
        let demo5:UIViewController = UIViewController()
        demo.view.backgroundColor = UIColor.red
        demo2.view.backgroundColor = UIColor.white
        demo3.view.backgroundColor = UIColor.gray
        demo4.view.backgroundColor = UIColor.orange
        demo5.view.backgroundColor = UIColor.brown
        
        //ページングするViewControllerを設定
        navigationController.viewControllerArray = [demo,demo2,demo3,demo4,demo5]
        
        //アプリを開いた時に最初に呼ばれるようにする
        self.window?.rootViewController = navigationController
        //ナビゲーションコントローラーをキーウィンドウにする
        self.window?.makeKeyAndVisible()
        
        
        return true
    }

 

makeKeyAndVisible()に関しては「iOS開発におけるウィンドウ「UIWindow」の知られざる活用方法とは? #iOS」の記事がわかりやすいです。

私もあとでじっくり読むためにリンクさせていただきますね(`・ω・´)

 

これでとりあえず一応おおまかな実装は終わりです。

できたのはこんな感じ

 

ここまでで1つ気になるところを修正します。

ビューが表示されてすぐに、ステータスバーの分だけ下にスクロールされています。

これを修正するために

//これがないと上に隙間ができる
pageController.automaticallyAdjustsScrollViewInsets = false

とする必要があります。

これは先ほどのAppDelegate内のpageControllerに対して行います。

PageViewControllerでNavigationbar(またはStatusbar)の高さ分ずれる」を参考にさせていただきました。

 

別ViewからNavigationViewを表示する

最初からナビゲーションビューを呼ばない時です。

その場合は、AppDelegate内ではなく遷移前の画面に書きます。

    @IBAction func buttonTapped(_ sender: Any) {
        let pageController:UIPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        let navigationController:SwipeNavigationViewController = SwipeNavigationViewController(rootViewController: pageController)
        
        let demo:UIViewController = UIViewController()
        let demo2:UIViewController = UIViewController()
        let demo3:UIViewController = UIViewController()
        let demo4:UIViewController = UIViewController()
        let demo5:UIViewController = UIViewController()
        demo.view.backgroundColor = UIColor.red
        demo2.view.backgroundColor = UIColor.white
        demo3.view.backgroundColor = UIColor.gray
        demo4.view.backgroundColor = UIColor.orange
        demo5.view.backgroundColor = UIColor.brown
        //これがないと上に隙間ができる
        pageController.automaticallyAdjustsScrollViewInsets = false
        
        navigationController.viewControllerArray = [demo,demo2,demo3,demo4,demo5]
        
     //画面遷移
        present(navigationController, animated: true, completion: nil)
    }

 

こんな感じ。

これで別のビューからボタンタップでページングされたビューを表示することができます。

ここがなんだかんだめちゃくちゃ時間かかりました。。。画面遷移がいつものperformSegueが使えなかったのがでかい・・・。

 

全てのViewに固定してボタンを表示する

ちょっとみづらいですが、全てのビューの左上に×ボタンを表示してみました。

こんな感じ

SwipeNavigationViewController内に書いています。

    var closeBtn:UIButton!

    override func viewWillAppear(_ animated: Bool) {
        self.setupPageViewController()
        //pageViewControllerの設定が終わったあと
        self.setupView()
    }

    //固定して表示するもの
    func setupView() {
        closeBtn = UIButton()
        closeBtn.frame = CGRect(x:10,y:10,width:50,height:50)
        closeBtn.setTitle("×", for: .normal)
        closeBtn.addTarget(self,action:#selector(SwipeNavigationViewController.closeBtnTapped(sender:)),for:.touchUpInside)
        pageController.view.addSubview(closeBtn)
    }


    func closeBtnTapped(sender:UIButton){
        dismiss(animated: true, completion: nil)
    }

 

このボタンをaddSubviewしている箇所が結構肝。というかめっちゃひっかかりました。

色々addSubviewできるviewがあるんですが、ページングされているところのみに表示したい場合はここで良さそうです。

self.viewとかにすると、ページングされているビューからさらに遷移した先にも表示されてしまいます。

 

StoryBoard上にあるViewControllerをページングする

既にStoryBoard上にあるViewControllerをいくつかページングする場合です。

まずはStoryBoard上のViewConrollerにIDをつけておきます。

 

このIDを使ってインスタンスを作成します。

    @IBAction func buttonTapped(_ sender: Any) {
        let pageController:UIPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        let navigationController:SwipeNavigationViewController = SwipeNavigationViewController(rootViewController: pageController)
        
        pageController.automaticallyAdjustsScrollViewInsets = false
        
        //FirstViewController
        let firstView = self.storyboard?.instantiateViewController(withIdentifier: "first") as! FirstViewController
        //SecondViewController
        let secondView = self.storyboard?.instantiateViewController(withIdentifier: "second") as! SecondViewController

        navigationController.viewControllerArray = [firstView,secondView]
        
        present(navigationController, animated: true, completion: nil)
    }

 

こんな感じです。

 

とりあえずこんなもんです。最後に全体コード載せておきます。

 

全体コード

ViewController

//
//  ViewController.swift
//  SwipeNaviTest
//
//  Created by Reo on 2017/01/20.
//  Copyright © 2017年 Reo. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.blue
        // Do any additional setup after loading the view, typically from a nib.
    }
    @IBAction func buttonTapped(_ sender: Any) {
        let pageController:UIPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        let navigationController:SwipeNavigationViewController = SwipeNavigationViewController(rootViewController: pageController)
        
        //これがないと上に隙間ができる
        pageController.automaticallyAdjustsScrollViewInsets = false
        
        let firstView = self.storyboard?.instantiateViewController(withIdentifier: "first") as! FirstViewController
        let secondView = self.storyboard?.instantiateViewController(withIdentifier: "second") as! SecondViewController

        navigationController.viewControllerArray = [firstView,secondView]
        
        present(navigationController, animated: true, completion: nil)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

 

SwipeNavigationViewController

//
//  SwipeNavigationViewController.swift
//  SwipeNaviTest
//
//  Created by Reo on 2017/01/20.
//  Copyright © 2017年 Reo. All rights reserved.
//

import UIKit


class SwipeNavigationViewController: UINavigationController ,UIPageViewControllerDelegate,UIPageViewControllerDataSource,UIScrollViewDelegate{
    
    required override init(nibName nibNameOrNil:String?,bundle nibBundleOrNil:Bundle?){
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    required override init(rootViewController:UIViewController){
        super.init(rootViewController:rootViewController)
    }
    
    var pageScrollView:UIScrollView!
    var viewControllerArray:[UIViewController] = []
    var pageController:UIPageViewController!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationBar.isHidden = true
    }
    
    override func viewWillAppear(_ animated: Bool) {
        self.setupPageViewController()
        self.setupView()
    }
    
    //PageViewControllerのセットアップ
    func setupPageViewController(){
        //NavigationControllerのtopViewControllerを生成
        pageController = self.topViewController as! UIPageViewController
        
        //デリゲートを設定
        pageController.delegate = self
        pageController.dataSource = self
        
        //ページコントローラーにビューコントローラーを設定
        pageController.setViewControllers([viewControllerArray[0]], direction: .forward, animated: true, completion: nil)
        
        
        self.syncScrollView()
    }
    
    func syncScrollView(){
        for view in pageController.view.subviews{
            if view.isKind(of:UIScrollView.self){
                pageScrollView = view as! UIScrollView
                pageScrollView.delegate = self
            }
        }
    }
    
    //固定して表示するもの
    var closeBtn:UIButton!
    func setupView() {
        closeBtn = UIButton()
        closeBtn.frame = CGRect(x:10,y:10,width:50,height:50)
        closeBtn.setTitle("×", for: .normal)
        closeBtn.addTarget(self,action:#selector(SwipeNavigationViewController.closeBtnTapped(sender:)),for:.touchUpInside)
        pageController.view.addSubview(closeBtn)
    }
    func closeBtnTapped(sender:UIButton){
        dismiss(animated: true, completion: nil)
    }
    
    //左にスワイプしたとき(ページが進む)
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        var index:Int = self.indexOfController(viewController:viewController)
        if(index == NSNotFound){
            return nil
        }
        index += 1
        if(0 <= index && index < viewControllerArray.count){
            return viewControllerArray[index]
        }
        return nil
    }
    
    //右にスワイプしたとき(ページが戻る)
    func pageViewController(_ pageViewController:UIPageViewController,viewControllerBefore viewController:UIViewController) -> UIViewController?{
        var index:Int = self.indexOfController(viewController:viewController)
        if(index == NSNotFound){
            return nil
        }
        index -= 1
        if(0 <= index && index < viewControllerArray.count){
            return viewControllerArray[index]
        }
        return nil
    }
    
    //viewControllerのindexを返す
    func indexOfController(viewController:UIViewController) -> Int{
        for i in 0 ..< viewControllerArray.count{
            if(viewController == viewControllerArray[i]){
                return i
            }
        }
        return NSNotFound
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    
}

AppDelegate

//
//  AppDelegate.swift
//  SwipeNaviTest
//
//  Created by Reo on 2017/01/20.
//  Copyright © 2017年 Reo. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
       /*
        
        let pageController:UIPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        let navigationController:SwipeNavigationViewController = SwipeNavigationViewController(rootViewController: pageController)
        pageController.automaticallyAdjustsScrollViewInsets = false

        let demo:UIViewController = UIViewController()
        let demo2:UIViewController = UIViewController()
        let demo3:UIViewController = UIViewController()
        let demo4:UIViewController = UIViewController()
        let demo5:UIViewController = UIViewController()
        demo.view.backgroundColor = UIColor.red
        demo2.view.backgroundColor = UIColor.white
        demo3.view.backgroundColor = UIColor.gray
        demo4.view.backgroundColor = UIColor.orange
        demo5.view.backgroundColor = UIColor.brown
        
        navigationController.viewControllerArray = [demo,demo2,demo3,demo4,demo5]
        
        
        self.window?.rootViewController = navigationController
        self.window?.makeKeyAndVisible()
 
 */
        
        
        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }


}

AppDelegate内の部分はとりあえずコメントアウト

 

 

今回initの部分は特にoverrideすることがないからいらないと思うんですが一応書いておきます。どうなんだろうか。書かなきゃなんだろうか。

 

やっぱりナビゲーションバーや現在何ページ目なのかわかる方法が必要かなぁと思うんですが今回はついてないです。元にした方のコードにはついています。

とりあえずは以前に私が書いた記事の「UITabBarController上をスワイプで画面遷移する方法【Swift】」にアニメーションつけたと思っておきます。

デザイン的にその辺を考えていなかったのですが、追加した際にはまた記事書きますね。

 

それではでは。

参考記事はその都度貼ったのではしょり(´・ω・`)

2018/04/14追記 Gistにあげました。
Swift4でチェックしてGistにあげました。


本記事ではStoryboard上にあるFirstViewControllerを持ってきていましたが、こっちではそのままFirstViewControllerもってきてます。

本記事のままでも#selectorの関数に@objcをつければSwift4で動作します〜。
Swift

Comments...

コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS
more