DEV Community

Wing CHAN
Wing CHAN

Posted on

Customize your navigation with Push using the present animation in iOS

Push using the present animation in iOS

Purpose

Implement a custom navigation animation that uses a "present-like" animation effect during push and pop operations across multiple view controllers.

  1. A pushes B (normal animation)
  2. B pushes C (custom present-like animation)
  3. C pops back to B (custom dismiss-like animation)
  4. Multiple view controllers may push to C, all using the same custom animations

Steps

Step 1: Create Custom Animation Controllers

  1. PresentAnimationController: Handles the animation effect when pushing to ViewControllerC.
  2. DismissAnimationController: Handles the animation effect when popping back from ViewControllerC.
// PresentAnimationController: Custom "present-like" animation
class PresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5 // Duration of the animation
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromVC = transitionContext.viewController(forKey: .from),
              let toVC = transitionContext.viewController(forKey: .to) else {
            return
        }

        let containerView = transitionContext.containerView
        let finalFrame = transitionContext.finalFrame(for: toVC)
        toVC.view.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.height) // Start position off-screen

        containerView.addSubview(toVC.view)

        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            toVC.view.frame = finalFrame // Animate to final position
        }, completion: { finished in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
}

// DismissAnimationController: Custom "dismiss-like" animation
class DismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5 // Duration of the animation
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromVC = transitionContext.viewController(forKey: .from),
              let toVC = transitionContext.viewController(forKey: .to) else {
            return
        }

        let containerView = transitionContext.containerView
        let initialFrame = transitionContext.initialFrame(for: fromVC)
        toVC.view.frame = initialFrame
        containerView.insertSubview(toVC.view, belowSubview: fromVC.view)

        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            fromVC.view.frame = initialFrame.offsetBy(dx: 0, dy: initialFrame.height) // Move off-screen
        }, completion: { finished in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Common UINavigationControllerDelegate Class

This delegate class handles all push and pop actions with custom animations.

class CustomNavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationController.Operation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if operation == .push, toVC is ViewControllerC {
            return PresentAnimationController() // Use present-like animation for push to C
        } else if operation == .pop, fromVC is ViewControllerC {
            return DismissAnimationController() // Use dismiss-like animation for pop from C
        }
        return nil
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Set the UINavigationController to Use This Delegate Class

In AppDelegate or the main view controller, set the delegate of UINavigationController to the common delegate class.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    let customNavDelegate = CustomNavigationControllerDelegate() // Create delegate instance

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let navigationController = UINavigationController(rootViewController: ViewControllerA())
        navigationController.delegate = customNavDelegate // Set the delegate
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
        return true
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Push to C from Any View Controller

Ensure that any view controllers pushing to ViewControllerC do so without needing additional configuration, as the common delegate handles all animation logic.

class ViewControllerA: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        title = "ViewController A"

        let pushButton = UIButton(type: .system)
        pushButton.setTitle("Push to B", for: .normal)
        pushButton.addTarget(self, action: #selector(pushToB), for: .touchUpInside)
        pushButton.frame = CGRect(x: 100, y: 200, width: 200, height: 50)
        view.addSubview(pushButton)
    }

    @objc func pushToB() {
        let viewControllerB = ViewControllerB()
        navigationController?.pushViewController(viewControllerB, animated: true)
    }
}

class ViewControllerB: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .lightGray
        title = "ViewController B"

        let pushButton = UIButton(type: .system)
        pushButton.setTitle("Push to C with Present Animation", for: .normal)
        pushButton.addTarget(self, action: #selector(pushToC), for: .touchUpInside)
        pushButton.frame = CGRect(x: 50, y: 200, width: 300, height: 50)
        view.addSubview(pushButton)
    }

    @objc func pushToC() {
        let viewControllerC = ViewControllerC()
        navigationController?.pushViewController(viewControllerC, animated: true)
    }
}

class ViewControllerC: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .cyan
        title = "ViewController C"

        let backButton = UIButton(type: .system)
        backButton.setTitle("Back to B", for: .normal)
        backButton.addTarget(self, action: #selector(popToB), for: .touchUpInside)
        backButton.frame = CGRect(x: 100, y: 200, width: 200, height: 50)
        view.addSubview(backButton)
    }

    @objc func popToB() {
        navigationController?.popViewController(animated: true)
    }
}

Enter fullscreen mode Exit fullscreen mode

Result

  • All pushes to ViewControllerC use the custom "present-like" animation.
  • All pops from ViewControllerC use the custom "dismiss-like" animation.
  • Simplifies management of navigation animations across multiple view controllers with a single, reusable delegate class.

Top comments (0)