1

How do I achieve this effect with a navigation controller.

I have just one VC embedded in a UINavigationController and is it presented modally over another VC. So far so good but I want to achieve a look something like below where the nav bar is shorter has curved edges and the status bar portion is transparent exposing the bottom viewcontroller's status bar.

enter image description here

This is what I have so far in the storyboard so far.

enter image description here

So far I have been able to achieve rounded edges for the navbar by subclassing UINavigationController. However I have no idea how to make the navBar shorter from the top:

class ComposeNavigController: UINavigationController {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        self.definesPresentationContext = true
        self.providesPresentationContextTransitionStyle = true
        self.modalPresentationCapturesStatusBarAppearance = false
        self.modalPresentationStyle = .overFullScreen


    }

    override func viewDidLoad() {
        super.viewDidLoad()
            view.clipsToBounds = true
            view.layer.cornerRadius = 16.0
            view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
            view.layer.shadowOffset = .zero
            view.layer.shadowColor = UIColor.black.cgColor
            view.layer.shadowRadius = 16.0
            view.layer.shadowOpacity = 0.5
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        view.layer.shadowPath = UIBezierPath(rect: view.bounds).cgPath
    }

}

Which works fine and curves the edges.

enter image description here

Anjan Biswas
  • 7,746
  • 5
  • 47
  • 77

1 Answers1

2

Alright, so here's what I had to do in addition to the UINavigationController subclass in my question. I had to make use of UIViewControllerAnimatedTransitioning delegate in order to achieve what I wanted.

I created two class that subclassed NSObject and conformed to the above protocol.

First one is for present transition-

import UIKit

class ComposePresentTransitionController: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let fromViewController = transitionContext.viewController(forKey: .from)!
        let toViewController = transitionContext.viewController(forKey: .to)!
        let containerView = transitionContext.containerView

        let screenBounds = UIScreen.main.bounds
        let topOffset: CGFloat = UIApplication.shared.statusBarFrame.height

        var finalFrame = transitionContext.finalFrame(for: toViewController)
        finalFrame.origin.y += topOffset
        finalFrame.size.height -= topOffset

        toViewController.view.frame = CGRect(x: 0.0, y: screenBounds.size.height,
                                             width: finalFrame.size.width,
                                             height: finalFrame.size.height)
        containerView.addSubview(toViewController.view)

        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 1.0, options: .curveEaseOut, animations: {
            toViewController.view.frame = finalFrame
            fromViewController.view.alpha = 0.3
        }) { (finished) in
            transitionContext.completeTransition(finished)
        }

        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, options: .curveEaseOut,
                       animations: {

        }) { (finished) in

        }
    }
}

Second for dismiss transition-

import UIKit

class ComposeDismissTransitionController: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.3
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let fromViewController = transitionContext.viewController(forKey: .from)!
        let toViewController = transitionContext.viewController(forKey: .to)!

        let screenBounds = UIScreen.main.bounds
        var finalFrame = fromViewController.view.frame
        finalFrame.origin.y = screenBounds.size.height

        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, options: .curveEaseIn,
                       animations: {
                        fromViewController.view.frame = finalFrame
                        toViewController.view.alpha = 1.0
        }) { (finished) in
            fromViewController.view.removeFromSuperview()
            transitionContext.completeTransition(finished)
        }
    }
}

Then, from the view controller that's going to present this viewcontroller needed to conform to the UIViewControllerTransitioningDelegate protocol and implement the animationController:forPresented and animationController:forDismissed methods like so-

extension PresentingVC: UIViewControllerTransitioningDelegate {

    func animationController(forPresented presented: UIViewController,
                             presenting: UIViewController,
                             source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return ComposePresentTransitionController()
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return ComposeDismissTransitionController()
    }
}

Once that was done, the PresentingVC needed to set the UIViewControllerTransitioningDelegate to self which was done in the prepareForSegue-

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        super.prepare(for: segue, sender: sender)
        segue.destination.transitioningDelegate = self
    }

If you are a segue hater like me, then you can do this from an IBAction of a button all in the code too.

@IBAction func composeTap(_ sender: Any) {
        let composeVC = self.storyboard?.instantiateViewController(withIdentifier: "compose") as! ComposeNavigController //this should be the navigation controller itself not the embedded viewcontroller
        composeVC.transitioningDelegate = self
        self.present(composeVC, animated: true, completion: nil)
    }

Thats it... now whenever the button was tapped to present the Compose screen modally, it would modally animate with the effect I desired. I added a little bit of bouncing effect just for fun :)

enter image description here

Anjan Biswas
  • 7,746
  • 5
  • 47
  • 77