How to add basic drawer menu with Swift 5

Using UIViewControllerTransitioningDelegate

@IBAction func didTapMenu(_ sender: Any) {        
}
// ViewController.swift@IBAction func didTapMenu(_ sender: Any) {        
let drawerController = DrawerMenuViewController()
present(drawerController, animated: true)
}
// DrawerTransitionManager.swiftimport UIKit class DrawerTransitionManager: NSObject, UIViewControllerTransitioningDelegate {
}
// DrawerMenuViewController.swift// store transition manager inside view controller class
let transitionManager = DrawerTransitionManager()
init() {
super.init(nibName: nil, bundle: nil)

// it's important to set presentation style to custom
// because it allows us to modify the presentation later on
modalPresentationStyle = .custom
transitioningDelegate = transitionManager
}
// since we provide custom initialiser
// we have to provide required init function
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// DrawerTransitionManager.swiftfunc presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?
// DrawerPresentationController.swiftimport UIKit class DrawerPresentationController: UIPresentationController {// define default size of child controller 
// inserted in this container controller
override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
// I want to have some space to the right of drawer
// to see underlying controller
return CGSize(width: parentSize.width * 0.8, height: parentSize.height)
}
// define final frame of presented controller
// will be set at the end of animation
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
guard let containerView = containerView else {
return frame
}
frame.size = size(forChildContentContainer: presentedViewController, withParentContainerSize: containerView.bounds.size)
return frame
}
// set final frame to controller at the begin of layouting
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
// DrawerTransitionManager.swiftfunc presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {        
return DrawerPresentationController(presentedViewController: presented, presenting: presenting)
}
// DrawerTransitionManager.swiftfunc animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
}
// DrawerSlideAnimation.swiftimport UIKit class DrawerSlideAnimation: NSObject, UIViewControllerAnimatedTransitioning {
}
// DrawerSlideAnimation.swiftfunc transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {        
// let's decide drawer will be sliding during half of a second
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// magic happens here
}
// DrawerSlideAnimation.swiftvar isPresenting: Bool = true
// DrawerSlideAnimation.swiftfunc animateTransition(using transitionContext: UIViewControllerContextTransitioning) {         
// if presenting drawer will be under key .to
// if dismissing drawer will be under key .from
let key: UITransitionContextViewControllerKey = isPresenting ? .to : .from
// check if our controller exist in transition
guard let presentedController = transitionContext.viewController(forKey: key) else {
return
}
// for convenience set containerView to new variable
let containerView = transitionContext.containerView
// define position and size of drawer at the end of presentation
let presentedFrame = transitionContext.finalFrame(for: presentedController)
// define position and size of drawer at the end of dismiss
// just offset presentedFrame by its width to left
let dismissedFrame = presentedFrame.offsetBy(dx: -presentedFrame.width, dy: 0)
// if we are going to present controller, we need to add its view to the container
if isPresenting {
containerView.addSubview(presentedController.view)
}
// get actual duration for animation, defined in the function above
let duration = transitionDuration(using: transitionContext)
// we need to notify that transition is finished with status from the context, not our custom animation
let wasCancelled = transitionContext.transitionWasCancelled
// define start and end frames for animation
let fromFrame = isPresenting ? dismissedFrame : presentedFrame
let toFrame = isPresenting ? presentedFrame : dismissedFrame
// set start frame for controller view before animation
presentedController.view.frame = fromFrame
UIView.animate(withDuration: duration) {
// change controller view frame to end frame during animation
presentedController.view.frame = toFrame
} completion: { (_) in
// notify animation complete with context status
transitionContext.completeTransition(!wasCancelled)
}
}
// DrawerTransitionManager.swiftlet slideAnimation = DrawerSlideAnimation()func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {        
slideAnimation.isPresenting = true
return slideAnimation }
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
slideAnimation.isPresenting = false
return slideAnimation
}
// DrawerPresentationController.swiftprivate lazy var dimmingView: UIView = {        
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0
return view
}()
// DrawerPresentationController.swiftoverride func presentationTransitionWillBegin() {      
guard let containerView = containerView else {
return
}
// before presentation
// add dimmingView to container view beneath presentedController
containerView.insertSubview(dimmingView, at: 0)
// set constraints of dimming view to fit container
// that's why we set translatesAutoresizingMaskIntoConstraints to false
NSLayoutConstraint.activate([
dimmingView.topAnchor.constraint(equalTo: containerView.topAnchor),
dimmingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
dimmingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
dimmingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor)
])
// check for animation coordinator
// if it's missing make dimming view visible immediately
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 1
return
}
// otherwise do it in animation
// duration of this animation same as duration of slide animation
coordinator.animate { (_) in
self.dimmingView.alpha = 1
}
}
override func dismissalTransitionWillBegin() { // do the same for making dimming view transparent on dismiss
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 0
return
}

coordinator.animate { (_) in
self.dimmingView.alpha = 0
}
}
// DrawerPresentationController.swiftprivate lazy var dimmingView: UIView = {        
let view = UIView()
/// ...
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissPresentedController))
view.addGestureRecognizer(tapRecognizer)

return view
}()
@objc private func dismissPresentedController() {
presentedViewController.dismiss(animated: true)
}
Final result

Where to go from here?

iOS developer since 2017