Carthage で Framework を公開する

先日 Framework を作って Carthage でインストールできるようにしたので、その手順を書いておきます。
作ったものはこちらで紹介しています。

komaji504.hateblo.jp

Workspace や Project を作る

ディレクトリ構成

Workspace や Project のディレクトリ構成ですが、今回は以下のようにしてみました。
あくまで一例なので、他の方が作られたものを参考にしたりして、自分好みでわかりやすい構成にしていけば良いのかなと思います。

GradientAnimationView
├── Demo
│   ├── Demo.xcodeproj
│   │   ├── project.pbxproj
│   │   └── project.xcworkspace
│   │       └── contents.xcworkspacedata
│   ├── Source
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   │   └── AppIcon.appiconset
│   │   │       └── Contents.json
│   │   ├── Base.lproj
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── ViewController.storyboard
│   │   ├── Info.plist
│   │   └── ViewController.swift
│   └── demo.gif
├── GradientAnimationView
│   ├── GradientAnimationView.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   │   └── contents.xcworkspacedata
│   │   └── xcshareddata
│   │       └── xcschemes
│   │           └── GradientAnimationView.xcscheme
│   └── Source
│       ├── GradientAnimationView.h
│       ├── GradientAnimationView.swift
│       └── Info.plist
├── GradientAnimationView.xcworkspace
│   ├── contents.xcworkspacedata
│   └── xcshareddata
│       └── WorkspaceSettings.xcsettings
├── LICENSE
└── README.md

Workspace を作る

Workspace は必須ではないのですが、作ってその中に Framework とそれを使ったデモアプリを入れておくと、Framework を開発しながらすぐにデモアプリで動作確認ができるので開発が楽になります。
利用者もデモアプリを実行して Framework がどんなものかを簡単に触ることができるので親切かと思います。

Demo Project を作る

デモアプリとなる Project で、 Framework の開発時にはこの Project を実行して動作確認しながら進めて行きます。
今回は Single View App の Project を Demo という名前で作りました。

Framework Project を作る

Cocoa Touch Framework として Project を作ります。

開発する

あとは思いのままに Framework を作っていきます。 インタフェースには Public や Open を適切に付与します。これらを付与したもの以外は外部から参照できません。

ビルド対象を Framework に変更してビルドすることで、 Public や Open が付与されたものを Demo Project から参照できるようになるので、 Demo Proejct の方で Framework を使ったアプリを作っていきます。

動作確認する際には、ビルド対象を Demo に変更して実行するだけです。

Cathage でインストールできるようにする

以下の手順で開発した Framework の Scheme を Shared にすることでインストールできるようになります。

ビルド対象を選択するところから Manage Schemes を選択します。

f:id:komaji504:20180614235738p:plain

Framework Project の右端にある Shared のチェックボックスにチェックを入れます。

f:id:komaji504:20180614235733p:plain

READEME を書く

概要や使い方を書きます。

Shields.io: Quality metadata badges for open source projects

こちらから適切なバッジを選んだり作ったりしてつけるとそれっぽくなります。 以下のようなバッジを簡単に作ることができます。

Language Platform Carthage compatible License

また、View 系の Framework の場合、動作がわかる GIF を載せておくと親切と思います。 GIF ファイルは Quick Time Player と ffmpeg から簡単に作れます。雑ですが、以下のようにして作れます。

動画を撮影する

  • Quick Time Player を起動
  • メニューバー
    • ファイル
      • 新規画面収録 (または Control + Command + N)
  • 録画ボタンをクリック
  • 録画対象を選択して録画
    • 全体であればクリックするだけ
    • 一部であればドラッグすることで録画対象を選択できる
      • その後録画ボタンをクリック
        • 録画ボタンをクリックすると同時にカーソルを素早く移動すればカーソルを映さないこともできる
  • 録画を止める
    • Control + Command + Esc

動画をGIFに変換する

brew install ffmpeg
ffmpeg -i input.mov -r 10 output.gif

リリースする

タグをアップロードする

git コマンドでタグをつけてリモートリポジトリにプッシュします。

git tag 0.1.0 -m "コメント"
git push origin 0.1.0

リリースノートを作成する

GitHub の Release ページの Draft a new release ボタンをクリックするとリリース作成画面が開きます。 f:id:komaji504:20180615001135p:plain


リリース対象のタグ・リリースの名前・リリース内容を埋めます。 f:id:komaji504:20180615001132p:plain


すると以下のようなリリースノートが作成されます。 f:id:komaji504:20180615001139p:plain

※上の画像は以前 LabelPicker という Framework を作ったときのものです。

おわりに

以上で作業は完了です。
初めて作った時は色々と手こずったのですが、 carthage update で自分が作った Framework がインストールできた時は感動しました!
初めての方もぜひチャレンジしてみてください〜

グラデーションをアニメーションさせるライブラリ作った【iOS】

FacebookiOS アプリでは、起動時にコンテンツを表示するまでの間、テキストや画像を表す灰色で塗りつぶした View を表示しています。そうすることで、この後にどういったコンテンツが表示されるのかということを伝えることができます。

加えて、ローディングインジケータとして、 UIAcitivityIndicator のようなぐるぐるを表示するのではく、その灰色で塗りつぶした View をグラデーションさせ、グラデーションをアニメーションさせています。
コンテンツとは切り離されたぐるぐるを表示するのではく、コンテンツそのものにエフェクトを加えることで、より自然な表現になっていると思います。

今回は、この Facebook アプリと同じ表現をするためのライブラリを作ったので紹介します。

GradientAnimationView

github.com

同じようなことができるライブラリはたくさんあるかと思いますが、その中でも一番シンプルになっているかと思います。 自分がやりたかったことは、グラデーションを左から右にスライドするアニメーションをさせるということだけだったので、それだけができるようにしています。

Carthage に対応していますが、1ファイルだけなのでコピペするのでも良いです。

使い方

対象の View に対して addSubView するだけです。

let gradientAnimationView = GradientAnimationView()
view.addSubview(gradientAnimationView)

そうすると、このようなグラデーションが左から右へアニメーションする View を作ることができます。

GradientAnimationView

オプション

以下を設定できます。

// 角丸にする
gradientAnimationView.cornerRadius = 5.0

// グラデーションの両端の色
gradientAnimationView.sideColor = .gray

// グラデーションの中央の色
gradientAnimationView.centerColor = .lightGray

// アニメーションの時間
gradientAnimationView.animationDuration = 1.2

// アニメーション間の時間
gradientAnimationView.waitingDuration = 0.6

今後

Storyborad から設定できたら便利かと思うのですが今はできないので対応したいと思います。

UICollectionView の UICollectionReusableView の位置がおかしい時

class SampleListViewController: UIViewController, UICollectionViewDatasource, UICollectionDelegate {
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.register(
                UINib(nibName: "FooterIndicatorView", bundle: nil),
                forSupplementaryViewOfKind: UICollectionElementKindSectionFooter,
                withReuseIdentifier: "FooterIndicatorView"
            )
        }
    }
    var footerIndicatorView: UICollectionReusableView!

    .
    .
    .

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        guard kind == UICollectionElementKindSectionFooter else {
            return UICollectionReusableView()
        }
        
        if footerIndicatorView != nil {
            footerIndicatorView = collectionView.dequeueReusableSupplementaryView(
                ofKind: kind,
                withReuseIdentifier: "FooterIndicatorView",
                for: indexPath
            )
        }
        
        return footerIndicatorView
    }
}

UICollectionView のフッターにローディングインジケータを表示したくて、上記の用に実装していたとします。
インジケータのアニメーションを操作するために、インジケータのインスタンスが生成されたらプロパティに保持しています。

また、インジケータのインスタンスを毎回生成するのが勿体無いと思って、インスタンスが作られていなかったら (nil だったら) dequeue してプロパティに格納して以降はそれを使い回すというように実装していました。

位置がおかしい

この時、 インジケータがセクションの最後のセルのすぐ下に表示されずに、常に UICollectionView の最上部 (しかもセルに被って) 表示されてしまうというバグに遭遇しました。

対応

色々試していたところ、インスタンスが作られていなかったら (nil だったら) という if 文を消して、毎回 dequeue するようにしたら、意図した通りに表示されました。

なぜ?

Apple のドキュメントを読んでみたのですが、関係ありそうなことは書いていなかったので (たぶん) 、とりあえずライフサイクル的にダメなんだろうなあという感じでした。

dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:) - UICollectionView | Apple Developer Documentation

for-case 文【Swift実践入門読書メモ】

Swift実践入門を読んでみて、気になったところのメモです。

for-case 文

  • 第3章 3節 pp.91-92

Sequence プロトコルに準拠した型の要素のうち、パターンにマッチするものだけを列挙する繰り返し文です。

for 文にも case によるパターンマッチが行えて

let numbers: [Int] = [1, 2, 3, 4, 5]

for case 2..4 in numbers {
    ...
}

とか

enum Food {
    case hamburger, pizza, sushi
}

let foods: [Food] = [
    .hamburger,
    .pizza,
    .sushi
]

for case .hamburger in foods {
    ...
}

みたいなことができます。


いまいち使い所がわからなかったですが、 以下のエントリで書いた if-case といい今回の for-case といい、色々なところでパターンマッチが行えるのかふむふむと思いました。

komaji504.hateblo.jp

今後パターンマッチを行う際には、どの 〇〇-case を使うとスッキリ書けるかを意識しながらコードを書いてみようと思います。

何か良きケースがあったら追記します。

合わせて読みたい

  • パターンマッチ
    • 第3章 6節 pp.100-104

引数のクロージャの属性【Swift実践入門読書メモ】

Swift実践入門を読んでみて、気になったところのメモです。

引数のクロージャの属性

  • 第4章 3節 pp.120-124

@escaping 属性

関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す属性です。

こんな感じで使われます。

func execClosure(_ closure: @escaping () -> Void) {
    ...
}

ということは、逆に言えば @escaping 属性がついていなければスコープ外で保持されることがないということです。そのため、 クロージャ内で self を参照していたりしても循環参照にならないため、 [weak self] 等を用いて弱参照にする必要がなくなります。

以前、以下のエントリで循環参照について考えた際には、どんな時に循環参照になってしまうのか考えるのが大変なので、問答無用で [weak self] を使用するのが楽そうと思いましたが、 @escaping 属性の有無で判別することができるようです。

komaji504.hateblo.jp

でも結局 @escaping 属性をつけるかどうかは人間が考える必要があって大変、ということはなく @escaping 属性をつけるべきときにつけていない場合は、コンパイルエラーとなるので気づくことができます。

class SampleClass {
    var closure: (() -> Void)?
    func execClosure(_ closure: () -> Void) {
        self.closure = closure
    }
}
Playground execution failed: error: SampleApp.playground:4:24: error: assigning non-escaping parameter 'closure' to an @escaping closure
        self.closure = closure
                       ^
SampleApp.playground:3:24: note: parameter 'closure' is implicitly non-escaping
    func execClosure(_ closure: () -> Void) {
                       ^
                       @escaping

そのため、@escaping 属性がついていれば循環参照の恐れがあるので、クロージャによってキャプチャされる変数は [weak self] 等を用いて弱参照にしておく、ついていなければ強参照のまま利用するというようにすると良さそうです。

@autoclosure 属性

引数をクロージャで包むことで遅延評価を実現するための属性です。

引数の遅延評価が有効なケース

以下の printResults(result1:result2:) は、第一引数の値が true なら最初の分岐以降の処理は実行されません。

func printResults(result1: Bool, result2: Bool) {
    if result1 {
        print("result1 is true")
    } else if result2 {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }

処理の順序はこんな感じになります。

  • result1 を評価する
    • true なら print("result1 is true") して終了
    • false なら result2 を評価する
      • true なら print("result1 is false, result2 is true") して終了
      • false なら else 節に入って print("result1 is false, result2 is true") して終了

ですが、以下のように引数に関数の実行結果を直接渡している場合には、渡された関数は関数内の処理に入る前に実行されます。

printResult(result1: result1(), result2: result2())
  • result1() を評価する ← 関数の処理前
  • result2() を評価する ← 関数の処理前
  • result1() の結果である result1 を評価する
    • true なら print("result1 is true") して終了
    • false なら result2() の結果である result2 を評価する
      • true なら print("result1 is false, result2 is true") して終了
      • false なら else 節に入って print("result1 is false, result2 is true") して終了

このとき、 result1() の結果が true になるのであれば result2() の評価は無駄になってしまいます。
これを防ぐために遅延評価が有効となります。

クロージャで包む

関数の実行結果を直接渡すと関数内の処理に入る前に実行されてしまいますが、以下のようにクロージャで包むと遅延評価を行うことができます。

func printResults(result1: Bool, result2: () -> Bool) {
    if result1 {
        print("result1 is true")
    } else if result2() {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }
}

printResults(result1: true, result2: { return result2() })

ですが、これだと printResults(result1:result2:) のインターフェースが変わってしまいます(第二引数に渡す型が Bool から () -> Bool に変わっている) 。
そこで @autoclosure の登場です。

@autoclosure をつけると暗黙的にクロージャに包んでくれるので、関数のインターフェースは変わらずに関数を実行することができます。

func printResults(result1: Bool, result2: @autoclosure () -> Bool) {
    if result1 {
        print("result1 is true")
    } else if result2() {
        print("result1 is false, result2 is true")
    } else {
        print("result1 is false, result2 is true")
    }
}

printResults(result1: true, result2: result2())

@autoclosure をつけるとインターフェースが変わらないのは確かに良いなあと思いました。

ですが、あまり遅延評価を要するコードを書く機会がない (書くべきところに気づけていない?) ので、自分が使うことはあまりないような気がしました。

合わせて読みたい

  • 弱参照による循環参照への対処
    • 第11章 2節 pp.274-275
  • クロージャ
    • 第11章 3節 pp.276-285

if-case 文 【Swift実践入門読書メモ】

Swift実践入門を読んでみて、気になったところのメモです。

if-case 文

  • 第3章 2節 p.81

if-case を用いることで、以下のようなパターンマッチによる分岐が行えます。

if case 0...5 = 3 {
    print ("パターンにマッチしています")
}

比較対象が式の右辺にくるということと、 = が使われていることから、あまり直感的ではないのかなあという気がしました 。
そのため、上記のような Range を用いた式を書く場合には contains(:_)~= で事足りるということから if case を積極的に使う必要もなさそうかなと思いました。
ですが、Enum の場合には使い所がありそうなので、全体を統一するという目的で Range においても if case を使っていくのはありなのかなと思ったりしました。

Enum の場合には使い所がありそう

ですが、以下のように associated value が定義されている場合、 == を使った式で分岐させようとするとコンパイルエラーとなってしまいます。

enum Food {
    case hamburger(name: String)
}

let cheeseBurger = Food.hamburger(name: "cheese")

if cheeseBurger == .hamburger {
    print("ハンバーガー")
}
error: binary operator '==' cannot be applied to operands of type 'Food' and '_'
if cheeseBurger == .hamburger {
   ~~~~~~~~~~~~ ^  ~~~~~~~~~~

かと言って switch 文を使うと記述量も多くなってしまうので、こういう時には if-case を使うとシュッとかけて良さそうだなあと思いました。

if case .hamburger = cheeseBurger {
    print("ハンバーガー")
}

合わせて読みたい

  • パターンマッチ
    • 第3章 6節 pp.100-104

画面遷移のアニメーションをカスタムする

UINavigationController を使った画面遷移のアニメーションのデフォルトは、横からスライドするアニメーションですが、これをカスタムすることができます。

最近、アニメーションをカスタムしたりといったようにカスタムトランジションについて学んでみたので、実装方法を書いてみたいと思います。

登場人物

アニメーションを管理するので少しコード量が多くなってしまって複雑に感じるかと思うのですが、登場するクラスとプロトコルに関しては以下の3つだけです。

  • UINavigationController (クラス)
  • UINavigationControllerDelegate (プロトロル)
  • UIViewControllerAnimatedTransitioning (プロトコル)

カスタムトランジションのざっくりとした流れとしては、以下の通りです。

  1. 画面遷移のメソッドが呼ばれる (UINavigationController)
  2. 画面遷移のアニメーションを決定する (UINavigationControllerDelegate)
  3. アニメーションをする (UIViewControllerAnimatedTransitioning)

UIViewControllerAnimatedTransitioning

画面遷移時に行うアニメーションを定義するためのプロトコルです。
アニメーションをカスタムするためには、このプロトコルに準拠したオブジェクトを用意する必要があります。カスタムアニメーションに必要な最低限実装すべきメソッドは以下の2つです。

以下、上記メソッドの引数名は省略します。

transitionDuration

アニメーションにかける時間を定義するためのメソッドです。

animateTransition

遷移時に行うアニメーションを定義するためのメソッドです。このメソッド内でどういったアニメーションを行うのかを実装していきます。

アニメーションは、 このメソッドの引数として渡ってくる UIViewControllerContextTransitioning を用いて実装していくことになります。 このクラスが、遷移前の View 、遷移後の View、アニメーションを描写するための View を持っているので、これらを取り出してアニメーションを組み立てていきます。

なお、プッシュ時、ポップ時と共にこちらのメソッドが呼ばれるため、内部で分岐してアニメーションを切り替える必要があります。
しかし、このメソッドの引数として渡ってくる UIViewControllerContextTransitioning にはプッシュ、ポップを判別するための API が用意されていないため、自前で用意する必要があります。

UINavigationControllerDelegate

この Protocol にカスタムトランジションの様々な設定を行うためのメソッドが用意されています。 今回はアニメーションを変更するだけなので、以下のメソッドを実装します。

このメソッドの返り値として、 UIViewControllerAnimatedTransitioning に準拠したオブジェクトを返すことで、画面遷移時にカスタムしたアニメーションが行われるようになります。

nil を返すと、横からスライドするデフォルトのアニメーションとなります。

UINavigationController

わざわざ書くまでもないかと思いますが、このクラスによって画面遷移を行います。 上記の UINavigationControllerDelegate に準拠したオブジェクトを UINavigationController オブジェクトの delegate プロパティにセットすることで、画面遷移時に UINavigationControllerDelegate の各メソッドが呼ばれるようになります。

具体的な実装

UIViewControllerAnimatedTransitioning

このプロトコルに準拠したクラスを作ります。
UIViewControllerAnimatedTransitioning が NSObjectProtocol を継承しているので、作成するクラスも NSObject を継承させる必要があります。

class CustomAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        // ...
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // ...
    }

}

transitionDuration

アニメーションにかける、良い感じの値を返すようにするだけです。

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.3
}

animateTransition

先にコードを書いてしまうと、プッシュの場合は以下のようになります。

// (1)
guard let sourceView = transitionContext.view(forKey: .from),
    let destinationView = transitionContext.view(forKey: .to) else {
        // アニメーション終了の通知
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        
        return
}

// (2)
let containerView = transitionContext.containerView
containerView.addSubview(sourceView)
containerView.insertSubview(destinationView, aboveSubview: sourceView)

// (3)
UIView.animate(
    withDuration: transitionDuration(using: transitionContext),
    animations: {
        // プッシュのカスタムアニメーション処理
    },
    completion: { _ in
        // アニメーション終了の通知
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    }
)

(1) はじめに、引数の transitionContext からアニメーションに必要なオブジェクトを取り出します。
transitionContext.view(forKey: .from) で遷移元の画面の View、 transitionContext.view(forKey: .to) で遷移元の画面の View が取得できます。
これらのメソッドの返り値は Optional なので、取得できなかった場合は return していますが、このとき transitionContext.completeTransition(!transitionContext.transitionWasCancelled) によって画面遷移が終了したことを通知します。
このメソッドの引数には画面遷移が正常に終了したかどうかを Bool で渡します。
transitionContext.transitionWasCancelled で画面遷移がキャンセルされたかどうかを知ることができるので、今回はこの結果を渡しています。

(2) 続いて、transitionContext.containerView により、 transitionContext からアニメーションを描画するためのキャンバスとなる View を取り出します。
そして、この View に先ほど取り出しておいた View 達を addSubviewinsertSubview で追加していきます。 これでアニメーションさせるための前準備は終わりです。

(3) 最後に、画面遷移時に行いたいアニメーション処理を実行します。
アニメーションが終えたら、画面遷移が終了となるので、これを通知するために transitionContext.completeTransition(!transitionContext.transitionWasCancelled) を実行します (試してはいませんが、このタイミングでは引数を true に固定してしまっても良いのかもしれません) 。

ポップの場合の処理も追加すると以下のようになるかと思います。

if プッシュなら {
    guard let sourceView = transitionContext.view(forKey: .from),
        let destinationView = transitionContext.view(forKey: .to) else {
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            
            return
    }
    
    let containerView = transitionContext.containerView
    containerView.addSubview(sourceView)
    containerView.insertSubview(destinationView, aboveSubview: sourceView)
        
    UIView.animate(
        withDuration: transitionDuration(using: transitionContext),
        animations: {
            // プッシュのカスタムアニメーション処理
        },
        completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    )
} else {
    guard let sourceView = transitionContext.view(forKey: .to),
        let destinationView = transitionContext.view(forKey: .from) else {
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            
            return
    }
    
    let containerView = transitionContext.containerView
    containerView.addSubview(destinationView)
    containerView.insertSubview(sourceView, aboveSubview: destinationView)
    
    UIView.animate(
        withDuration: transitionDuration(using: transitionContext),
        animations: {
            // ポップのカスタムアニメーション処理
        },
        completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    )
}

プッシュとポップの判別

後ほど説明しますが、 UIViewControllerAnimatedTransitioning に準拠したオブジェクトを生成するタイミングで画面遷移時の UINavigationControllerOperation の値を取得することができるので、これを保持しておいて参照できるようにしておくのが、個人的には良いのではないかと思っています。

let operation: UINavigationControllerOperation

init(operation: UINavigationControllerOperation) {
    self.operation = operation
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    switch operation {
    case .push:
        // プッシュのカスタムアニメーション処理
    case .pop:
        // ポップのカスタムアニメーション処理
    case .none:
        break
    }
}

UINavigationControllerDelegate

このプロトコルに準拠したクラスを作ります。
こちらも NSObjectProtocol を継承しているので、作成するクラスも NSObject を継承させる必要があります。 今回は UINavigationController を継承したクラスを作成し、このクラスに適用することにします。 実装すべきメソッドを実装すると以下のようになります。

class NavigationController: UINavigationController, UINavigationControllerDelegate {
    
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAnimatedTransitioning(operation: operation)
    }
    
}

このメソッドの返り値として先ほどつくったクラスのオブジェクトを返すことで、画面遷移時にカスタムアニメーションが行われるようになります。

UIViewControllerAnimatedTransitioning に準拠したオブジェクトを生成するタイミングで画面遷移時の UINavigationControllerOperation の値を取得することができるので

先ほど上記のように書きましたが、それがこのタイミングとなります。
このメソッドの引数として 画面遷移の種類を表す UINavigationControllerOperation が渡ってくるので、これを CustomAnimatedTransitioning の初期化時に渡してあげることで、 animateTransition が呼ばれたタイミングでプッシュとポップを判別することができるようになり、アニメーションを切り替えることができます。

引数の fromVCtoVC は、それぞれ遷移元、遷移先の ViewController です。そのため、画面によってデフォルトのアニメーションとカスタムアニメーションを切り替えたい場合には、以下のように分岐させることで可能となります。

if toVC is CustomViewController || fromVC is CustomViewController {
    return CustomAnimatedTransitioning(operation: operation)
} else {
    // nil を返すとデフォルトのアニメーション
    return nil
}

UINavigationController

画面遷移に使用するこのクラスのオブジェクトの delegate プロパティに、先ほどの UINavigationControllerDelegate に準拠したオブジェクトをセットします。そうすることで、画面遷移のメソッドが呼ばれた時に delegate で実装したメソッドが呼ばれ、カスタムアニメーションが実行されることになります。

なお、画面遷移のメソッドが呼ばれるタイミングより前に delegate をセットする必要があります。今回は、上記ですでに UINavigationController を継承したクラスをつくっているので、この init 内で delegate をセットするようしています。

class NavigationController: UINavigationController {
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
        
        delegate = self
    }
    
}

extension NavigationController: UINavigationControllerDelegate {
    
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if toVC is DestinationViewController || fromVC is DestinationViewController {
            return CustomAnimatedTransitioning(operation: operation)
        } else {
            return nil
        }
    }
    
}

以上で、アニメーションをカスタムするための実装は完了となります!

おわりに

この記事の頭に表示している GIF のアニメーションと同様のアニメーションを行うサンプルコードも用意してみたので、よかったらこちらも確認してみてください。

github.com

次は、インタラクティブなカスタムアニメーションを行うための実装について書いてみたいと思います。