Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
106 views
in Technique[技术] by (71.8m points)

ios - Modifying navigation controller to act more like a hamburger menu in swift

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:

  1. 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.

  2. 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:

on tap

When the user does an edge swipe:

on 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:

old side nav

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)
Waitting for answers

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...