Swift愛好会 Advent Calendar 2016 8日目の投稿です。なにか皆さん技術の話をあまりしていない気がしますが、気にせず技術の話をします。
iPhoneの写真アプリのように
iOS_UIトレース会という勉強会を主催しています。iOSエンジニアが世にあるアプリのUIを真似たアプリを作っていって開発能力をアップしていこうという目的の勉強会です。
11月と12月のお題がGohobeeというアプリのご褒美画面を真似しようということになっていました。
とてもリッチな画面です。特徴を幾つかあげると
- それぞれ異なるセルレイアウト
- シームレスに次の画像が遷移
- 遷移後画像がくるくるアニメーション
- パララックススクロール
とたくさんあります。
すべてを真似するのはなかなか難しいと思い、シームレスに次の画像が遷移だけを真似しようかと思いました。
今回はiOS7から導入されたカスタムトランジションを利用して、画像をシームレスに移動しながら画面遷移をする方法を見てみたいと思います。
カスタムトランジション
iOSの画面遷移はiOS7から開発者カスタムができるようになりました。
仕組みとしては
- UIViewControllerAnimatedTransitioningプロトコルを適応したクラスを作る。このクラスでアニメーションを指定する
- UINavigationControllerのサブクラスを作って、UINavigationControllerDelegateに適応させる。navigationController(_:animationControllerFor:from)メソッドを実装。1のインスタンスを戻り値として返す。
です。
実装
今回はUINavigationControllerを使ったトランジションを実装していきたいと思います。
まず、UIViewControllerAnimatedTransitioningプロトコルを適応したクラスを作ります。
class Transition: NSObject, UIViewControllerAnimatedTransitioning {
// pushなら forward == true
var forward = false
//アニメーションの時間
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
//アニメーションの定義
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if self.forward {
//push時のアニメーション
forwardTransition(transitionContext)
} else {
backwardTransition(transitionContext)
}
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)
がアニメーションの時間を返します。今回は0.4秒にしました。
そして実際のアニメーションの操作をfunc animateTransition(using transitionContext: UIViewControllerContextTransitioning)
で実装します。
今回はpushのときはforwardTransition
メソッド、backwardTransition
メソッドを実装しました。
forwardTransitionメソッド
//push時のアニメーション
fileprivate func forwardTransition (_ transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
return
}
guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
return
}
let containerView = transitionContext.containerView
//遷移先のviewをaddSubViewする(fromVC.viewは最初からcontainerViewがsubviewとして持っている
containerView.addSubview(toVC.view)
//アニメーション用のimageViewを新しく作成する
guard let sourceImageView = (fromVC as? ViewController)?.createImageView() else {
return
}
guard let destinationImageView = (toVC as? DetailViewController)?.createImageView() else {
return
}
//遷移先のimageViewをaddSubviewする
containerView.addSubview(sourceImageView)
toVC.view.alpha = 0.0
//addSubViewでレイアウトが崩れるため再レイアウトする
toVC.view.layoutIfNeeded()
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.05,
options: UIViewAnimationOptions.curveEaseIn,
animations: {
//アニメーション開始
// 遷移もとのimageViewのframeとconteModeを遷移先のimageViewに代入
sourceImageView.frame = destinationImageView.frame
sourceImageView.contentMode = destinationImageView.contentMode
// cellのimageViewを非表示にする
(fromVC as? SharedView)?.createImageView()?.isHidden = true
toVC.view.alpha = 1.0
}, completion: {
finished in
//アニメーション終了
transitionContext.completeTransition(true)
})
}
push時のソースです。
画面遷移中はtransitionContext.containerView
という特殊なコンテナビューの上で操作されます。
これは最初から遷移元のビュー(transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
)がaddSubViewされている状態です。
そこに、遷移中にシームレスに移動させる画像、sourceImageViewを取得して、それをアニメーションします。
最後にtransitionContext.completeTransition(true)
を実行します。これはシステムに画面遷移を知らせるメソッドです。
忘れると画面先のビューの状態がおかしくなるのでアニメーションが終わった段階で呼ぶようにします。
戻るときのアニメーションは以下のようにしました。
//pop時のアニメーション
fileprivate func backwardTransition(_ transitionContext: UIViewControllerContextTransitioning) {
// Pushと逆のアニメーション
guard let fromeVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
return
}
guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
return
}
let containerView = transitionContext.containerView
//前回遷移したさいのImageViewが残っているので一度全てを外す
containerView.subviews.forEach {
view in
view.removeFromSuperview()
}
// toView -> fromViewの順にaddSubView
containerView.addSubview(toVC.view)
containerView.addSubview(fromeVC.view)
guard let sourceImageView = (fromeVC as? DetailViewController)?.createImageView() else {
return
}
guard let destinationImageView = (toVC as? ViewController)?.createImageView() else {
return
}
//戻るときに一度ViewControllerのcellのimageViewを非表示にする
guard let viewController = toVC as? ViewController, let cell = viewController.collectionView.cellForItem(at: viewController.selectedIndexPath!) as? CollectionViewCell else {
return
}
cell.imageView.isHidden = true
containerView.addSubview(sourceImageView)
containerView.layoutIfNeeded()
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.05, options: UIViewAnimationOptions.curveEaseIn,
animations: {
sourceImageView.frame = destinationImageView.frame
fromeVC.view.alpha = 0.0
}, completion: {
finished in
sourceImageView.isHidden = true
(toVC as? ViewController)?.selectedImageView?.isHidden = false
cell.imageView.isHidden = false
sourceImageView.removeFromSuperview()
fromeVC.view.removeFromSuperview()
transitionContext.completeTransition(true)
})
}
基本的にpush時と逆のアニメーション同じですが、注意としてtransitionContext.containerView
のサブビューに対して一度全てビュー階層から外しています。
//前回遷移したさいのImageViewが残っているので一度全てを外す
containerView.subviews.forEach {
view in
view.removeFromSuperview()
}
transitionContext.containerView
はシングルトンのようで、遷移をするたびに同じオブジェクトが使われるようにです。
なので、遷移する際にaddSubviewをすると次回遷移するときにそのビューが残ってしまうようです。
UINavigationControllerにカスタムトランジションできるようにする
UINavigationControllerのサブクラスを作って作ったTransitionクラスのインスタンスのアニメーションになるように設定します。
UINavigationControllerDelegateを適応します。
そしてUINavigationControllerDelegateのnavigationController(_:animationControllerFor:from:to:)
にTransitionクラスのインスタンスを返すようにします。
class TransitionNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationControllerOperation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let transition = Transition()
switch operation {
case .push:
transition.forward = true
return transition
case .pop:
transition.forward = false
return transition
default:
return nil
}
}
}
これで画像をシームレスに移動しながら画面遷移ができます。
#完成品
#ソース
Githubにあげています。
https://github.com/SatoTakeshiX/SwiftSharedViewTransition
#参考