Edit
I have kind of achieved this by adding a custom UIViewControllerAnimatedTransitioning
to handle the push and pop events. It is an inspiration of Robert Chen's method and this stack overflow answer by Fattie. I also had to update SlidingNavigationController
(updated code below).
The main issues are:
I want the navbar visible for FromVC
when the user presses the hamburger menu icon. But this currently does not happen as in viewWillAppear
of the side nav I hide the nav bar.
interactivePopGestureRecognizer
does not follow the custom animation hence the entire VC gets swiped off-screen when the user does an edge swipe to right.
When hamburger icon is tapped:
When the user does an edge swipe:
New custom animation:
class RevealSideNav: NSObject, UIViewControllerAnimatedTransitioning {
var pushStyle: Bool = false
var previouslyHiddenVC: UIViewController.Type = MainVC.self
var oldSnapshot: UIView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else { return }
if pushStyle {
hideSidenav(using: transitionContext)
return
}
let initalScale = MenuHelper.initialMenuScale
let containerView = transitionContext.containerView
containerView.backgroundColor = MenuHelper.menuBGColor
toVC.view.transform = CGAffineTransform(scaleX: initalScale, y: initalScale)
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
// fromVC.navigationController?.navigationBar.isHidden = false
fromVC.view.isHidden = true
guard let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false) else { return }
snapshot.isUserInteractionEnabled = false
snapshot.tag = MenuHelper.snapshotNumber
snapshot.layer.shadowOpacity = MenuHelper.snapshotOpacity
containerView.insertSubview(snapshot, aboveSubview: toVC.view)
fromVC.view.isHidden = true
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
snapshot.center.x += UIScreen.main.bounds.width * MenuHelper.menuWidth
snapshot.layer.opacity = MenuHelper.snapshotOpacity
toVC.view.transform = CGAffineTransform(scaleX: 1, y: 1)
}, completion: { _ in
fromVC.view.isHidden = false
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.oldSnapshot = snapshot
}
)
}
func hideSidenav(using transitionContext: UIViewControllerContextTransitioning) {
let fz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let tz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let f = transitionContext.finalFrame(for: tz)
let fOff = f.offsetBy(dx: UIScreen.main.bounds.width * MenuHelper.menuWidth, dy: 0)
tz.view.frame = fOff
transitionContext.containerView.insertSubview(tz.view, aboveSubview: fz.view)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
self.oldSnapshot.removeFromSuperview()
tz.view.frame = f
}, completion: {_ in
transitionContext.completeTransition(true)
})
}
}
I want to add support for a hamburger menu in my app. Before I was using a really complex logic based on transition delegates and measuring how much the user has swiped to complete the slide-out animation for a child view (based on iOS Tutorial: How to make a customizable interactive slide-out menu in Swift by Robert Chen). Here's an old stackoverflow post with code for my previous hamburger menu.
This is how the old menu looked:
I since realized that the slide-out functionality could be implemented a lot better by adding my SideNav View controller in the built-in UINavigationController. And I think this approach would be less prone to memory leakages. So that's what I precisely did. Created a new copy of my old SideNav. Then I embedded it in a custom UINavigationController
that had methods pre-configured to handle a slide to go back. And finally updated my did select row actions to self.navigationController?.pushViewController
. The results look really nice when presenting and going back to the side nav.
But this approach has one major thing missing. A traditional hamburger menu has a part of the old view controller still visible at one edge of the screen so that the user can swipe on it to bring it back. I partially tried implementing this functionality by adding a pan gesture to my side nav and presenting the older view controller.
@objc func closeViewWithPan(sender: UIPanGestureRecognizer) {
guard let calledFromVC = calledFromVC else { return }
print("Presenting VC: ", navigationController?.presentingViewController)
if navigationController?.presentingViewController == nil {
navigationController?.pushViewController(calledFromVC, animated: true)
}
Analytics.logEvent(AnalyticsEvent.HideSideNav.rawValue, parameters: [StringAnalyticsProperties.VCDisplayed.rawValue : "(type(of: calledFromVC))".lowercased()])
}
But I immediately ran into this issue:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '<Speech_Drill.SlidingNavigationController: 0x10a810c00> is pushing the same view controller instance (<Speech_Drill.MainVC: 0x105848600>) more than once which is not supported and is most likely an error in the application : xxx'
terminating with uncaught exception of type NSException
So I was wondering if there was a way in which part of previously presented VC is still sticking to the right edge like a traditional hamburger menu which could be swiped on to bring to front. Also, if possible I would like the first view controller to be presented by default and not the side nav when the app launches.
App Delegate Setup:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let sideNav = SideNavigationController()
let sideNavigationController = SlidingNavigationController.init(rootViewController: sideNav)
self.window?.rootViewController = sideNavigationController
...
}
Custom UINavigationController:
class SlidingNavigationController: UINavigationController, UIGestureRecognizerDelegate, UINavigationControllerDelegate{
let revealSideNav = RevealSideNav()
override func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
delegate = self
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
super.pushViewController(viewController, animated: animated)
interactivePopGestureRecognizer?.isEnabled = false
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
interactivePopGestureRecognizer?.isEnabled = true
}
// IMPORTANT: without this if you attempt swipe on
// first view controller you may be unable to push the next one
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
func navigationController(
_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationControllerOperation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
revealSideNav.pushStyle = operation == .push
return revealSideNav
}
}
Side Nav ViewController:
class SideNavigationController: UIViewController {
private let noticesUrl = "https://github.com/parthv21/Speech-Drill/blob/master/Speech-Drill/Information/info.json"
private let sideNavMenuItemReuseIdentifier = "SideNavMenuItemIdentifier"
static let sideNav = SideNavVC()
var interactor: Interactor? = nil
var calledFromVC: UIViewController?
private let sideNavContainer: UIView
private let sideNavTableView: UITableView
private let sideNavNoticesTableViewCell: SideNavNoticesTableViewCell
private let sideNavAdsTableViewCell: SideNavAdsTableViewCell
private let versionInfoView: VersionInfoView
private var menuItems = [sideNavMenuItemStruct]()
var selectedIndex = 1
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
sideNavContainer = UIView()
sideNavTableView = UITableView()
sideNavNoticesTableViewCell = SideNavNoticesTableViewCell()
sideNavAdsTableViewCell = SideNavAdsTableViewCell()
versionInfoView = VersionInfoView()
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
sideNavTableView.delegate = self
sideNavTableView.dataSource = self
sideNavTableView.register(SideNavMenuItemCell.self, forCellReuseIdentifier: sideNavMenuItemReuseIdentifier)
sideNavTableView.separatorStyle = .none
sideNavNoticesTableViewCell.fetchNotices()
sideNavAdsTableViewCell.fetchAds()
configureSideNav()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(closeViewWithPan(sender:)))
view.addGestureRecognizer(panGesture)
view.backgroundColor = MenuHelpe