Skip to content

Instantly share code, notes, and snippets.

@SergLam
Last active February 6, 2024 09:23
Show Gist options
  • Save SergLam/a8a8a0ce54e913a7ceac7d852b7e216c to your computer and use it in GitHub Desktop.
Save SergLam/a8a8a0ce54e913a7ceac7d852b7e216c to your computer and use it in GitHub Desktop.
Custom screen transition with pan gesture from the center of screen
import UIKit
// Fade animation: https://medium.com/@ludvigeriksson/custom-interactive-uinavigationcontroller-transition-animations-in-swift-4-a4b5e0cefb1e
// Slide animation: https://medium.com/swift2go/simple-custom-uinavigationcontroller-transitions-fdb56a217dd8
class CustomNavigationController: UINavigationController {
private var interactionController: UIPercentDrivenInteractiveTransition?
private var edgeSwipeGestureRecognizer: UIScreenEdgePanGestureRecognizer?
private var panGesturerecognizer: PanDirectionGestureRecognizer?
var shouldDismissRootScreen: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
addSwipeGesture()
delegate = self
}
private func addSwipeGesture() {
let edgeGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
edgeSwipeGestureRecognizer = edgeGesture
edgeSwipeGestureRecognizer?.edges = .left
view.addGestureRecognizer(edgeGesture)
let panDirection = PanDirection.horizontal(direction: .LeftToRight)
let panGesture = PanDirectionGestureRecognizer(direction: panDirection, target: self, action: #selector(handleSwipe(_:)))
panGesturerecognizer = panGesture
view.addGestureRecognizer(panGesture)
}
@objc
func handleSwipe(_ gesture: UIPanGestureRecognizer) {
guard let gestureView = gesture.view else {
interactionController = nil
return
}
let percent = gesture.translation(in: gestureView).x / gestureView.bounds.size.width
if gesture.state == .began {
interactionController = UIPercentDrivenInteractiveTransition()
let shouldDismiss = shouldDismissRootScreen && viewControllers.count == 1
if shouldDismiss {
dismiss(animated: true, completion: nil)
} else {
popViewController(animated: true)
}
} else if gesture.state == .changed {
interactionController?.update(percent)
} else if gesture.state == .ended {
if percent > 0.5 && gesture.state != .cancelled {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
}
}
}
// MARK: - UINavigationControllerDelegate
extension CustomNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return PopNavigationGestureAnimator(presenting: true)
case .pop:
return PopNavigationGestureAnimator(presenting: false)
case .none:
return nil
@unknown default:
return nil
}
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
import UIKit
enum HorizontalDirection {
case LeftToRight
case RightToLeft
}
enum VerticalDirection {
case TopToBottom
case BottomToTop
}
enum PanDirection {
case vertical(direction: VerticalDirection)
case horizontal(direction: HorizontalDirection)
}
class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
let direction: PanDirection
init(direction: PanDirection, target: AnyObject, action: Selector) {
self.direction = direction
super.init(target: target, action: action)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard state == .began else {
return
}
let vel = velocity(in: view)
switch direction {
case .horizontal(let direction):
guard abs(vel.y) > abs(vel.x) else {
switch direction {
case .LeftToRight:
guard vel.x > 0 else {
state = .cancelled
return
}
case .RightToLeft:
guard vel.x < 0 else {
state = .cancelled
return
}
}
return
}
state = .cancelled
case .vertical(let direction):
guard abs(vel.x) > abs(vel.y) else {
switch direction {
case .TopToBottom:
guard vel.y < 0 else {
state = .cancelled
return
}
case .BottomToTop:
guard vel.y > 0 else {
state = .cancelled
return
}
}
return
}
state = .cancelled
}
}
}
import UIKit
class PopNavigationGestureAnimator: NSObject {
var transitionTime: TimeInterval = 0.3
private let presenting: Bool
init(presenting: Bool) {
self.presenting = presenting
}
}
// MARK: - UIViewControllerAnimatedTransitioning
extension PopNavigationGestureAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return transitionTime
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from) else { return }
guard let toView = transitionContext.view(forKey: .to) else { return }
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
if presenting {
container.addSubview(toView)
} else {
container.insertSubview(toView, belowSubview: fromView)
}
let toViewFrame = toView.frame
let xCoordinate = presenting ? toView.frame.width : -toView.frame.width
toView.frame = CGRect(x: xCoordinate, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
let animations = {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1) {
toView.frame = toViewFrame
let xCoordinate = self.presenting ? -fromView.frame.width : fromView.frame.width
fromView.frame = CGRect(x: xCoordinate, y: fromView.frame.origin.y, width: fromView.frame.width, height: fromView.frame.height)
}
}
UIView.animateKeyframes(withDuration: duration,
delay: 0,
options: .calculationModeCubic,
animations: animations,
completion: { _ in
container.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment