swiftで画面遷移:UINavigationController

  • 224
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

Xcode6とswiftに慣れる練習課題として書いています。

UINavigationContollerの制御は、戻るボタン(leftButton)を使わない場合の方が珍しいはずです。

  • segue、actionをstoryboardで設定する方法
  • NavigationContollerのカスタム化によるsegue利用時のfuncの呼ばれ方の確認
  • segue内でのアニメーション指定

以上をつらつらと書いてみました。
長文になってしまいましたが、どなたかの参考になれば幸いです。

8/28 Xcode6β5でコードが変わっていたところを修正しています。
9/8 Xcode6β7でコードが変わっていたところを修正しています。
9/28 Xcode6.0.1でコードが変わっていたところを修正しています。
10/20 Xcode6.1でprintlnで使用しているNSDateのclass構成が変っていたため修正しています。

スタンダードな遷移

swiftでアニメーションのカスタマイズを書くことをゴールに
一般的な遷移を書く練習からはじめます

storyboard

ViewControllerとSecondViewController間のSegueには
"next"とIdentifierをつけて

スクリーンショット 2014-09-28 3.27.33.png

unwindSegueをつけたSecondViewController上で"back"としています。

スクリーンショット 2014-09-28 3.27.19.png

全体的の構成下記の通りです

スクリーンショット 2014-09-28 3.16.56.png

ViewController

ViewController.swift

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a ni
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
        if (segue.identifier == "next") {
        }
    }

    @IBAction func push(sender : UIButton) {
        performSegueWithIdentifier("next",sender: nil)
    }

    //unwind
    @IBAction func exitTo(segue: UIStoryboardSegue) {
        if (segue.identifier == "back") {
        }

    }
}

カスタマイズした遷移

push、popしたNavigationContollerの動作を左右から上下に変更します。

CATransitionを使う

QuartzCoreが必要でした。
β7以前に不要になっていることを確認したので、importは不要です。

NavigationController

横移動のアニメーションがついてしまったので、サブクラスでoverrideして止めています。
他に、NavigationBar上の戻るボタンを使う場合の実装を追加していますが、完全ではありません。Second以外の前の画面に戻る画面遷移のときに同じアニメーションを実行します。この場合は戻るボタンのところにカスタムボタンを付け・・・という実装が良いかと思います。どうでしょうか。

元のViewControllerへ追加

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func push(sender : UIButton) {
        performSegueWithIdentifier("next",sender: nil)
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
        if (segue.identifier == "next") {
            // pass data to next view
            let transition: CATransition = CATransition()
            transition.duration = 1.0
            transition.type = kCATransitionMoveIn
            transition.subtype = kCATransitionFromBottom

            self.navigationController!.view.layer.addAnimation(transition,forKey: kCATransition)
        }
    }

    //SecondViewController上のボタンから戻るときに通ります
    @IBAction func exitTo(segue: UIStoryboardSegue)
    {
        if (segue.identifier == "back") {
            // pass data to first view
            let transition: CATransition = CATransition()
            transition.duration = 1.0
            transition.type = kCATransitionMoveIn
            transition.subtype = kCATransitionFromTop

            self.navigationController!.view.layer.addAnimation(transition,forKey: kCATransition)
        }
    }
}
SecondViewController.swift
import UIKit

class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(false)

    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func back(sender : UIButton) {
        performSegueWithIdentifier("back",sender: nil)
    }
}
MyNavigationController.swift
import UIKit

class MyNavigationViewController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func pushViewController(viewController: UIViewController, animated: Bool) {
        super.pushViewController(viewController, animated: false)
    }

    //NavigationBar上の戻るボタンで戻るときに通ります
    override func popViewControllerAnimated(animated: Bool) -> UIViewController{

        let transition: CATransition = CATransition()
        transition.duration = 1.0
        transition.type = kCATransitionMoveIn
        transition.subtype = kCATransitionFromTop

        self.view.layer.addAnimation(transition,forKey: kCATransition)
        return super.popViewControllerAnimated(false)!
    }
}

戻るボタンでの遷移

NavigationBar上の戻るボタンを使う場合の実装で不完全だった部分を補足します。
前の画面に戻る画面遷移のときに一律同じアニメーションを実行、ではなく個別の画面で実装できるよう、戻るボタンのところにカスタムボタンを付ける方針で進めます。

スクリーンショット 2014-09-28 12.22.03.png

赤枠の部分に

  • NavigationItem
  • BarButtonItem

を追加しています。

ViewController

(1) Unwindの処理からAnimationをSecondViewControllerのprepareForSegueに移動させます。

(2) segueでunwindされた場合に呼ばれないnavigationControllerのpopViewControllerAnimatedを呼びます。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func push(sender : UIButton) {
        performSegueWithIdentifier("next",sender: nil)
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
        let transition: CATransition = CATransition()
        if (segue.identifier == "next") {
            transition.duration = 1.0
            transition.type = kCATransitionMoveIn
            transition.subtype = kCATransitionFromTop
            self.navigationController!.view.layer.addAnimation(transition,forKey: kCATransition)
        }
        println(__FUNCTION__, __LINE__)
    }

    @IBAction func exitTo(segue: UIStoryboardSegue)
    {
        if (segue.identifier == "back") {
            // segue使用時はpopViewControllerAnimatedが呼ばれないので呼ぶ
            self.navigationController?.popViewControllerAnimated(false);
            println(__FUNCTION__, __LINE__)
        }
    }
}

SecondViewController

(1) ViewControllerから移動させたprepareForSegueを追加して
segueでの遷移時にアニメーションをつけるよう統一します。

(2) NavigationBar、View上のボタンにIBActionを用意してstoryboardで接続します。
 ※ログの出力のため分けていますが指定する@IBActionは下記の"func backButton(sender : UIButton) "ひとつで間に合います。

SecondViewController.swift
import UIKit

class SecondViewController: UIViewController {
    @IBOutlet weak var leftButtonItem: UIBarButtonItem!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func backButton(sender : UIButton) {
                println(__FUNCTION__, __LINE__)
                performSegueWithIdentifier("back",sender: nil)
    }

    @IBAction func returnByNavigation(sender: AnyObject) {
        println(__FUNCTION__, __LINE__)
        performSegueWithIdentifier("back",sender: nil)
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
        let transition: CATransition = CATransition()
        if (segue.identifier == "back") {
            transition.duration = 1.0
            transition.type = kCATransitionMoveIn
            transition.subtype = kCATransitionFromBottom
            self.navigationController!.view.layer.addAnimation(transition,forKey: kCATransition)
        }
        println(__FUNCTION__, __LINE__)
    }
}

MyNavigationController

本当はこのクラスは使わないようにしたいのですが、pushするときのアニメーションが今の実装からは切れないので、必要です。segueをカスタマイズすれば不要にできるかも。
アニメーションの実行の順番を確認するためにも最初は作った方が良いかと思います。

MyNavigationController.swift
import UIKit

class MyNavigationViewController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func pushViewController(viewController: UIViewController, animated: Bool) {
        println(__FUNCTION__, __LINE__)
        super.pushViewController(viewController, animated: false)
    }

    override func popViewControllerAnimated(animated: Bool) -> UIViewController{
        println(String(NSDate.date().description),__FUNCTION__, __LINE__)
        return super.popViewControllerAnimated(animated)!
    }
}

UIStoryboardSegueを使う

UIViewController内でUIStoryboardSegueをカスタム化することで
アニメーションの制御のみために使用していたNavigationControllerの拡張クラスを捨てることが出来ます。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func push(sender : UIButton) {
        performSegueWithIdentifier("next",sender: nil)
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
        let transition: CATransition = CATransition()
        if (segue.identifier == "next") {
            transition.duration = 1.0
            transition.type = kCATransitionMoveIn
            transition.subtype = kCATransitionFromTop
            self.navigationController!.view.layer.addAnimation(transition,forKey: kCATransition)
        }
        let nextViewController: SecondViewController = segue.destinationViewController as SecondViewController
        println(__FUNCTION__, __LINE__)
    }

    @IBAction func exitTo(segue: UIStoryboardSegue)
    {
        if (segue.identifier == "back") {
            // 戻ったあとの処理をなにか書く
            self.navigationController?.popViewControllerAnimated(false);
            println(__FUNCTION__, __LINE__)
        }
    }
}

class PushNoAnimationSegue: UIStoryboardSegue {
    override func perform(){
        self.sourceViewController.navigationController??.pushViewController(self.destinationViewController as UIViewController, animated: false)
    }
}

ViewControllerに追加したCustomSegueをstoryboardで指定します。
スクリーンショット 2014-09-28 21.12.19.png

これで使用していたMyNavigtionControllerをstoryboardから外すことができます。

実装ポイント

ヘッダーをみる

例えば

let transition: CATransition = CATransition()
transition.duration = 1.0
transition.type = "moveIn"
transition.subtype = "fromTop"

transition.typeやsubtypeに設定するパラメータに迷ったときは
commandを押しながらで目的のプロパティをクリックすると
目的のプロパティを持つクラスのhファイルに遷移します。コメントに必要なパラメータが書かれていることがあります。

objective-cのフローを踏襲

今回そのコメント通りに書いてみましたが、よく見ると同じヘッダーファイルにkではじまる常数があるじゃないですか・・・。

書き換えましょう!

let transition: CATransition = CATransition()
transition.duration = 1.0
transition.type = kCATransitionMoveIn
transition.subtype = kCATransitionFromTop

これでだいぶObjective-cで書いた時のコードと似た感じになりました。

unwindやNavigationControllerでの呼び出されるfunc

遷移先から遷移元に戻ったときの動作については

(1)ViewContoller上のボタンで戻る
(2)NavigationControllerの”戻るボタン”で遷移

この二つは挙動がだいぶ違うのでカスタム化の必要があります。
unwindのコードが実行されているのはsegueで移動した時だけ。
popViewControllerAnimatedが呼ばれるのはデフォルトの戻るボタン利用時なので、意識して実行させる

遷移 unwind segue pushViewController popViewControllerAnimated
segueあり 呼ばれる segue内で固定:true(カスタム化で設定可) 実装なしでは呼ばれない
segueなし 呼ばれない 設定可能 animatedのBool設定可能

Custom Segue Class

UIStoryboardSegueが持っているのはperform()だけで、用途は遷移に関する記述です。
各ViewControllerで書いているCATransitionはこちらにまとめてしまってもすっきりしそうです。
ViewContorllerと同じソース内でClass記述できるのでまとめるのも分割して他のViewContollerと共通クラスにするのも自由度が高そうです。

さいごに

swiftの仕様に振り回されたコードの変更が申し訳ないです。
!とか?とかの調整は、そろそろまとまったかと思って更新しましたが、変らない可能性もゼロではないので、モチベーションが下がりがちです。
これで一旦更新は終了しますので、また進展がありましたら、別記事にまとめます。