修改导航控制器以使其更像 swift 中的汉堡菜单

问题描述

编辑

我通过添加自定义 UIViewControllerAnimatedTransitioning 来处理推送和弹出事件来实现这一点。它是 Robert Chen's 方法和 Fattie 的堆栈溢出答案的灵感。我还必须更新 SlidingNavigationController(更新代码如下)。

主要问题是:

  1. 我希望当用户按下汉堡菜单图标时 FromVC 的导航栏可见。但这目前不会发生,因为在侧面导航的 viewWillAppear 中我隐藏了导航栏。

  2. interactivePopGestureRecognizer 不遵循自定义动画,因此当用户向右滑动边缘时,整个 VC 会从屏幕上滑动。

当汉堡图标被点击时:

on tap

当用户进行边缘滑动时:

on edge swipe

新的自定义动画:

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)
            })

    }
}

我想在我的应用中添加对汉堡菜单的支持。在我使用基于转换委托的非常复杂的逻辑并测量用户为完成子视图的滑出动画而滑动的次数之前(基于 Robert Chen 的 iOS Tutorial: How to make a customizable interactive slide-out menu in Swift)。 Here's 旧的 stackoverflow 帖子,其中包含我之前的汉堡菜单代码。

这是旧菜单的样子:

old side nav

我意识到通过在内置 UINavigationController 中添加我的 SideNav View 控制器可以更好地实现滑出功能。而且我认为这种方法不太容易发生内存泄漏。所以这就是我所做的。创建了我的旧 SideNav 的新副本。然后我将它嵌入到自定义 UINavigationController 中,该自定义 self.navigationController?.pushViewController 具有预先配置的方法来处理返回的幻灯片。最后将我确实选择的行操作更新为 @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()]) } 。呈现和返回侧面导航时,结果看起来非常好。

但是这种方法缺少一件重要的事情。传统的汉堡菜单在屏幕的一侧仍然可见旧视图控制器的一部分,以便用户可以在其上滑动以将其带回来。我部分尝试通过向侧面导航添加平移手势并呈现旧视图控制器来实现此功能。

*** 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

但我立即遇到了这个问题:

func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

  ...

  let sideNav = SideNavigationController()
  let sideNavigationController = SlidingNavigationController.init(rootViewController: sideNav)
  self.window?.rootViewController = sideNavigationController
        
  ...

}

所以我想知道是否有一种方法可以让之前展示的 VC 的一部分仍然像传统的汉堡菜单一样粘在右边,可以滑动到前面。此外,如果可能的话,我希望在应用启动时默认显示第一个视图控制器,而不是侧面导航。

应用委托设置:

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
    }
}

自定义 UINavigationController:

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 = MenuHelper.menuBGColor
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
        navigationController?.setNavigationBarHidden(true,animated: animated)
        
        guard let calledFromVC = calledFromVC else { return }
        for (index,item) in menuItems.enumerated() {
            if item.presentedVC.isKind(of: type(of: calledFromVC)) {
                let indexPath = IndexPath(item: index + 1,section: 0)
                sideNavTableView.selectRow(at: indexPath,animated: false,scrollPosition: .none)
                selectedIndex = index
                break
            }
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        navigationController?.setNavigationBarHidden(false,animated: animated)
    }
    
    func configureSideNav() {
        
        
        view.addSubview(sideNavContainer)
        sideNavContainer.translatesAutoresizingMaskIntoConstraints = false
        
        sideNavContainer.addSubview(versionInfoView)
        versionInfoView.translatesAutoresizingMaskIntoConstraints = false
        
        //Storyboard Based VC
        let storyboard = UIStoryboard(name: "Main",bundle: nil)
        let mainVC = storyboard.instantiateViewController(withIdentifier: "MainVC") as! MainVC
        let infoVC = storyboard.instantiateViewController(withIdentifier: "InfoVC") as! InfoVC
        //Fully programatic VC - There is a reference to this in the VC too which I think will cause memory leaks
        let DiscussionsVC = DiscussionsViewController()
        
        //        calledFromVC = mainVC
        
        let mainVCMenuItem = sideNavMenuItemStruct(itemName: "Recordings",itemImg: recordIcon,itemImgClr: accentColor,presentedVC: mainVC)
        let infoVCMenuItem = sideNavMenuItemStruct(itemName: "About",itemImg: infoIcon,presentedVC: infoVC)
        let discussionsVCMenuItem = sideNavMenuItemStruct(itemName: "Discussions",itemImg: discussionIcon,presentedVC: DiscussionsVC) //Look into using SF Symbols with UIImage(systemName: T##String)
        
        menuItems.append(mainVCMenuItem)
        menuItems.append(discussionsVCMenuItem)
        menuItems.append(infoVCMenuItem)
        
        sideNavTableView.allowsMultipleSelection = false
        
        sideNavContainer.addSubview(sideNavTableView)
        sideNavTableView.translatesAutoresizingMaskIntoConstraints = false
        sideNavTableView.backgroundColor = .clear
        
        NSLayoutConstraint.activate([
            sideNavContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),sideNavContainer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor,constant: 30),sideNavContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor,constant: -70),sideNavContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),versionInfoView.bottomAnchor.constraint(equalTo: sideNavContainer.bottomAnchor),versionInfoView.leadingAnchor.constraint(equalTo: sideNavContainer.leadingAnchor,constant: -8),versionInfoView.trailingAnchor.constraint(equalTo: sideNavContainer.trailingAnchor,constant: 8),sideNavTableView.topAnchor.constraint(equalTo: sideNavContainer.topAnchor),sideNavTableView.leadingAnchor.constraint(equalTo: sideNavContainer.leadingAnchor),sideNavTableView.trailingAnchor.constraint(equalTo: sideNavContainer.trailingAnchor),sideNavTableView.bottomAnchor.constraint(equalTo: versionInfoView.topAnchor)
        ])
    }
    
    @objc func closeViewWithPan(sender: UIPanGestureRecognizer) {
        
        guard let calledFromVC = calledFromVC else { return }
        
        print("Presenting VC: ",parameters: [StringAnalyticsProperties.VCDisplayed.rawValue : "\(type(of: calledFromVC))".lowercased()])
        
    }
}

extension SideNavigationController: UITableViewDelegate,UITableViewDataSource  {
    
    func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
        return menuItems.count + 2
    }
    
    func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        if indexPath.row == 0 {
            return sideNavNoticesTableViewCell
        }
        
        if indexPath.row == menuItems.count + 1 {
            return sideNavAdsTableViewCell
        }
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: sideNavMenuItemReuseIdentifier) as? SideNavMenuItemCell else { return UITableViewCell() }
        cell.configureCell(with: menuItems[indexPath.row - 1])
        return cell
        
    }
    
    func tableView(_ tableView: UITableView,heightForRowAt indexPath: IndexPath) -> CGFloat {
        if indexPath.row == 0 {
            return 190
        } else if indexPath.row == menuItems.count + 1 {
            return 300
        }
        return 40
    }
    
    func tableView(_ tableView: UITableView,didSelectRowAt indexPath: IndexPath) {
        
        if indexPath.row == 0 || indexPath.row == menuItems.count + 1 { return }
        
        let vcToPresent = menuItems[indexPath.row - 1].presentedVC
        
        calledFromVC = vcToPresent
        vcToPresent.modalPresentationStyle = .fullScreen
        self.navigationController?.pushViewController(vcToPresent,animated: true)
        
    }
}

侧面导航视图控制器:

var timer = false
var autoClicker = 1
    setInterval(function() {
if (Game.hasBuff("Clot") && timer === false) {
autoClicker = setInterval(Game.ClickCookie,1);
timer = true;
} else if (Game.hasBuff("Clot") && timer === true) {
return "test"
}else {
clearInterval(autoClicker);
timer = false}
    },1000);

当前结果:

root view conroller

这就是我的想法:(希望可以配置可见的侧导航vc的宽度。)

expected side nav menu result

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...