Customize your navigation with Push using the present animation in iOS

wingch

Wing CHAN

Posted on September 3, 2024

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.
💖 💪 🙅 🚩
wingch
Wing CHAN

Posted on September 3, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related