【Swift】カスタムSegueとカスタムUnwindSegueを作成する【Xcode7.1/iOS9】

UINavigationControllerがどうしても使えなくて諦めて普通のSegueで対応しているところです。

navigationControllerなら左右にスライドアニメーションで遷移する(らしい)がどうにもこうにも上手く使えないのでカスタムSegueで左右にスライドするアニメーションを作成しました。

 

※UnwindのアニメーションはiOS9でのみの対応となります。iOS7,8では動きませんでした。

※iOS9上ではこちらの方法で動作しますが、iOS8以下では動作しません。

iOS8以下での必要なコードをこちらの記事に書きました

【Swift】カスタムUnwindSegueを作成する【iOS8編】

 

 

カスタムSegueを作成する

まずは新規ファイルでカスタムSegue用のファイルを作ります

File > New > File….からCocoaTouchClassを選択します。

スクリーンショット-2015-12-05-17.58.05

 

そしてSubclassをUIStoryboardSegueに設定してファイルを作成します

スクリーンショット-2015-12-05-17.58.15

 

作成ファイルを編集していきます。

override func perform(){
      //ここにコードを書いていきます
}

perofmメソッドをoverrideしてその中にコードを書いていきます。

まずは遷移前後のViewControllerのインスタンスをそれぞれ作成。

//遷移前のViewControllerのインスタンスを作成
let firstVCView = self.sourceViewController.view as UIView!
//遷移後のViewControllerのインスタンスを作成
let secondVCView = self.destinationViewController.view as UIView!

 

次に画面の縦横をそれぞれ取得しときます

//画面の横の長さを取得
let screenWidth = UIScreen.mainScreen().bounds.size.width
//画面の縦の長さを取得
let screenHeight = UIScreen.mainScreen().bounds.size.height

 

そしてアニメーション前の遷移後のビューの位置を設定しておきます。

//遷移後のビューを画面の外(右側)にだしておく
secondVCView.frame = CGRectMake(screenWidth, 0.0, screenWidth, screenHeight)

 

遷移先をサブビューに設定しておく(?)

let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(secondVCView, aboveSubview: firstVCView)

UIWindowについてよくわかっていないのですが、こういうことらしい・・

At this point, the view of the second view controller is not a subview of the app’s window yet. So, before we implement the actual animation, it’s obvious that we must add it to the window. This will be achieved using the insertSubview(view:aboveSubview:) method of the app’s window object. As you see next, at first we access the window object, and then we add the destination view:

参照元:A Beginner’s Guide to Animated Custom Segues in iOS 8

英語が読めないわけじゃなくてですね・・・・

ほわぁ〜っとしたのはわかるんですけど説明できないというか、そのまま訳せばいいんですけども。。。

先にwindow objectにアクセスして、遷移前の画面の上に遷移後のビューをのせる、ということ?

 

次にアニメーションをつけます

//0.4秒で遷移する
UIView.animateWithDuration(0.4, animations: { () -> Void in
   //遷移前のビューを現在の位置から画面幅分移動する
      firstVCView.frame = CGRectOffset(firstVCView.frame, -screenWidth, 0.0)
      //遷移後のビューを現在の位置(画面外)から画面幅分移動する
      secondVCView.frame = CGRectOffset(secondVCView.frame, -screenWidth, 0.0)
      }) { (Finished) -> Void in
       self.sourceViewController.presentViewController(self.destinationViewController as UIViewController,
       animated: false,
       completion: nil)
}

このアニメーションの中をいじることで色々なアニメーションをつけることができます。

まずは画面に表示されている遷移前のビューを画面外に押しやります。

それと同時に遷移後のビューを画面外から画面上に移動させます。(先ほど設定した位置からの移動になります)

 

アニメーションが終了したら画面を表示させます。animetedをtrueにしてしまうとスライド後に再度下から表示するアニメーションをしてしまいます。

つまりwindowは見かけ上の画面を取得するということなのかな。むむむ。わからん。

 

Storyboard上でカスタムセグエにしておくことを忘れずに

スクリーンショット-2015-12-05-18.50.59

Classが先ほど作成したファイル名になっていればOKです。

 

カスタムUnwindSegueを作成する

次にカスタムUnwindSegueを作成していきます。

なかなか上手くいかなくて大変でしたが、成功したら意外と単純でした。

 

UnwindSegueを作成する際には、まず遷移先(前のページ)にこのコードを書いておきます

@IBAction func unwindToSubject(segue:UIStoryboardSegue){
  //戻った後の処理を書く
 }

これがなければUnwindを設定することができます。

FirstViewController からSecondViewControllerに遷移していて、そこからUnwindでFirstViewControllerに戻る場合には、FirstViewControllerの方にコードを書きます。

 

そしてStoryboard上でSegueをつなげます。

スクリーンショット-2015-12-05-19.00.39

 

このときにUnwindSegueのClassに作成しておいたUnwindSegueのファイルを設定します。(作り方はSegueと同様)

 

スクリーンショット-2015-12-05-19.46.52

 

 

以前まではこの設定ができなかったそうで、設定ができない場合の記事は色々出てきたんですが、ない場合の記事が出てこなくてひっかかってしまいました。

結局やったことはSegueの時とほとんど同じです。

 

先ほどと同様にUnwindSegueのファイルを作成します。

import UIKit

class myUnwindSegue: UIStoryboardSegue {
    override func perform() {
    //ViewControllerのインスタンスを取得する
        let firstVCView = destinationViewController.view as UIView!
        let secondVCView = sourceViewController.view as UIView!
        
    //画面の縦横を取得する
        let screenHeight = UIScreen.mainScreen().bounds.size.height
        let screenWidth = UIScreen.mainScreen().bounds.size.width
        //戻った先のビューを画面外に設置する。
        firstVCView.frame = CGRectMake(-screenWidth, 0.0, screenWidth, screenHeight)
        //戻った先のビューを現在の画面の上にのせる
        let window = UIApplication.sharedApplication().keyWindow
        window?.insertSubview(firstVCView, aboveSubview: thirdVCView)
        
        UIView.animateWithDuration(0.5, animations: { () -> Void in
            //現在のビューを画面外に移動させる。
            secondVCView.frame = CGRectOffset(secondVCView.frame, screenWidth, 0.0)
            //戻った先のビューを画面上に移動させる。
            firstVCView.frame = CGRectOffset(firstVCView.frame, screenWidth, 0.0)
            
            }) { (Finished) -> Void in
                //現在の画面を閉じる
                self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
        }
    }
    
}

 

こんな感じ。

Segueと異なるところは画面の閉じ方ぐらいでしょうか。

あとはFistViewControllerとSecondViewControllerの役割が反対になるので注意が必要です。

 

補足しました

iOS8以下ではさらにこちらのコードも必要です

    override func segueForUnwindingToViewController(toViewController: UIViewController, fromViewController: UIViewController, identifier: String?) -> UIStoryboardSegue? {
            if let id = identifier {
                print("id=identifier")
                let unwindSegue = myUnwindSegue(identifier: id, source: fromViewController, destination: toViewController, performHandler: { () -> Void in
                })
                return unwindSegue
            }
        return super.segueForUnwindingToViewController(toViewController, fromViewController: fromViewController, identifier: identifier)!
}

ざっと別記事も書きましたが必要なのはこのコードだけです。【Swift】カスタムUnwindSegueを作成する【iOS8編】

iOS9でもStoryboard上Classを設定しない場合はこちらのコードが必要です。

 

 

できたコードを見ると、なんだ(´・ω・`)って感じなんですがなかなかにハマりました(笑)

Xcode7.1環境ですが、Unwindセグエもカスタムセグエがしやすくなっていて嬉しいですね。

遷移後にエラーが発生した・・・・

SegueのコードをコピーしてそのままUnwindに貼り付けて、その後FirstVCViewとSecondVCViewを入れ替えただけのコードにしたところこんなエラーが出ました。

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally an active

 

こいつをやっつけるのに時間がかかったわけなんですが

このエラーは「同時タップによる多重遷移」のせいらしいです。つまり画面を複数開いてしまうことによるエラーです。

最初のカスタムSegueでは、アニメーション後に遷移先の画面を新しく表示させています。

カスタムUnwindSegueでは、アニメーション後に遷移前の画面を閉じることで、遷移後の画面を表示しています。

UnwindSegueでは遷移後の画面を開くコードがまた別のところにあるというわけだろうか。それとも上に順繰り重なっていっているからなのだろうか。要はモーダルで重なっているのと同じ?なのか。

詳しいことはあまり理解できていませんが、上のように対処することでエラーを回避できました。

 

とにかくアニメーションが思った通りに動いてくれるとすごく嬉しいし楽しいです。

これでもうセグエなんて怖くないぜ!!!!!(なおnavigationControllerはめちゃくちゃ手強い模様)

 

間違っているところを発見した方やUIWindowについての説明がわかりやすくできる方がいたらぜひ教えてください。

 

 

 

2018/04/29追記 Swift4に対応しました。
対応したらコードが全然違うものになりました。
っていうかやり方も全然違う感じになってしまった。


一応上記そのままSwift4に移行したコードを書いてみたんですが、
Unbalanced calls to begin/end appearance transitionsってエラーでちゃんとアニメーションしませんでした。

なので「ios - Unbalanced calls to begin/end appearance transitions with custom segue - Stack Overflow」を参考に上記のように変更しました。

アニメーション部分を自由にいじって実際に試してみてください〜。
CustomSegue自体は本記事通りでおkです。


unwindの方はとりあえず以下の部分だけ変えれば動きます。

       UIView.animate(withDuration: 0.5, animations: {
            mock.frame.origin.x = 0
        }) { (isCompletion) in
            if isCompletion {
                //unwindの時はここだけ変える
                self.source.dismiss(animated: false, completion: { mock.removeFromSuperview() })
            }else {
                mock.removeFromSuperview()
            }
        }
単純に先ほどのコードを1行だけdismissに変更しました。 こんな感じになります。 これだと両方ともが上に重なる形で遷移してしまうので Unwindの方は現在表示されているviewが元の場所に戻る形になるように実装してみました。

想定通りの動きになってよかった。


要は遷移前に今のviewをスクリーンショットとして残して置いて、そいつを後から動かすってことをしてます。


CustomSegue、CustomUnwindSegueの両方ともSwiftファイルのみこちらで、後のIB上での実装方法等は本記事通りです。

ではでは。
わりとスッとできて成長を感じるぜ・・・!
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

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

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

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

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

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

iOS

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

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS
more