はじめに
iOS Second Stage Advent Calendar19日目の記事です。
個人的には6記事目になります。
本題
iPhoneの画面が大きくなったことでNavigationBarにある戻るボタンが押しづらいという事案をよく聞きます。
確かにホームボタンをダブルタップすれば画面半分を犠牲に届くようになりますが、思いやりを持ちたいところです。
ということで 横にスワイプ したら簡単に前の画面に戻れるサンプル作りました。
以下のような感じです。
サンプルは以下にあるので良かったら参考にしてください。
AdventCalendar2015/SampleSwipeNavigationController at master · ryokosuge/AdventCalendar2015
ソースコード
以下のようなクラスを作ってUINavigationController
を継承したクラスに保持させるだけです。
import UIKit
protocol NavigationAnimatorDelegate: class {
func popViewController()
func shouldBeginGesture(gesture: UIGestureRecognizer) -> Bool
}
class NavigationAnimator: UIPercentDrivenInteractiveTransition {
private var isPop: Bool = false
private var percentageDriven: Bool = false
private let Scale: CGFloat = 0.95
weak var delegate: NavigationAnimatorDelegate? = nil
init(view: UIView) {
super.init()
setupView(view)
}
}
/// MARK: - UIPanGestureReg
extension NavigationAnimator {
func handlePanGesture(gesture: UIPanGestureRecognizer) {
guard let view = gesture.view else {
return
}
var percent = gesture.locationInView(view).x / view.bounds.width / 2.0
percent = percent < 1 ? percent : 0.99
percentageDriven = true
switch gesture.state {
case .Began:
delegate?.popViewController()
case .Changed:
updateInteractiveTransition(percent)
case .Ended, .Cancelled:
gesture.velocityInView(view).x < 0 ? cancelInteractiveTransition() : finishInteractiveTransition()
percentageDriven = false
default:
break
}
}
}
extension NavigationAnimator: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
return delegate?.shouldBeginGesture(gestureRecognizer) ?? true
}
}
/// MARK: - UINavigationControllerDelegate
extension NavigationAnimator: UINavigationControllerDelegate {
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPop = operation == UINavigationControllerOperation.Pop
return self
}
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return percentageDriven ? self : nil
}
}
/// MARK: UIViewControllerAnimatedTransitioning
extension NavigationAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.3
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
containerView = transitionContext.containerView() else {
return
}
let toView = isPop ? fromViewController.view : toViewController.view
let fromView = isPop ? toViewController.view : fromViewController.view
let offset = containerView.frame.width
containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)
if isPop {
containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
}
toView.frame = containerView.frame
toView.transform = isPop ? CGAffineTransformIdentity : CGAffineTransformMakeTranslation(offset, 0)
fromView.frame = containerView.frame
fromView.transform = isPop ? CGAffineTransformMakeScale(Scale, Scale) : CGAffineTransformIdentity
let aniDuration = transitionDuration(transitionContext)
UIView.animateWithDuration(aniDuration, animations: {[weak self] () -> Void in
if let weakSelf = self {
toView.transform = weakSelf.isPop ? CGAffineTransformMakeTranslation(offset, 0) : CGAffineTransformIdentity
fromView.transform = weakSelf.isPop ? CGAffineTransformIdentity : CGAffineTransformMakeScale(weakSelf.Scale, weakSelf.Scale)
}
}) { (finished) -> Void in
toView.transform = CGAffineTransformIdentity
fromView.transform = CGAffineTransformIdentity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
}
/// MARK: - private methods.
extension NavigationAnimator {
private func setupView(view: UIView) {
let gesture = UIPanGestureRecognizer(target: self, action: "handlePanGesture:")
gesture.delegate = self
view.addGestureRecognizer(gesture)
}
}
import UIKit
class NavigationViewController: UINavigationController {
var animator: NavigationAnimator!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
animator = NavigationAnimator(view: self.view)
animator.delegate = self
delegate = animator
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension NavigationViewController: NavigationAnimatorDelegate {
func popViewController() {
popViewControllerAnimated(true)
}
func shouldBeginGesture(gesture: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
UIPercentDrivenInteractiveTransition
とUIViewControllerAnimatedTransitioning
を継承したクラスを作って、アニメーションの設定をするだけです。
あとはユーザーの指の動きを検知するためにUINavigationController
のview
にUIPanGestureRecognizer
を登録して、指のswipeの量で適度に動かしています。
ここら辺は知らなかった時は めんどくさかったですが 難しかったですが、いざ慣れると色々なことに応用できるので、ぜひ触って欲しい機能でもあります。
サンプルは以下にあるので、確認してみてください。
AdventCalendar2015/SampleSwipeNavigationController at master · ryokosuge/AdventCalendar2015
終わりに
これで6日目の記事を書き終わりました。
明日も自分が担当します。
以上です。
ありがとうございました。