Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multi View Controller Navigation in Swift

Side Menu Navigation in Swift

Swift 4.2, Xcode 10.0

My ultimate goal is to be able to easily (normally) transverse/navigate through my view controllers like as shown below.

Navigation

I want to be able to navigate from view controller to view controller with a common side menu. To this point, I have come up with an incredibly hacky way to accomplish this wherein when I select a view controller in the side menu, it goes dismisses the side menu, then it precedes to present the next view controller from that one, and assuming that is not the target view controller, a window temporarily covers its contents while I segue from that view controller to the target view controller.

Path through storyboard

Unique paths of view controllers to display from side menu.

Animation Paths Note: each colour in the above image is another path the app can take after something have being tapped in the side menu.

There are so many things wrong with this implementation I don't even know where to start. On rare occasions, you can see the contents of the intermediary view controller in between segues. Also the animation can be a bit choppy and distorted due to the amount of segues requires to actually get to the target view in some cases. Not to mention the immense difficulty and complexity now required to add another row to my side menu. And I know full well this is atrocious and am desperately trying to find a solution not my complex navigational problem. Of recent I have experimented with using container views and putting my side menu at the bottom of my stack rather than at the top as is right now, but it hasn't amounted to anything.


The past few weeks I have been driving myself insane trying to find out how to do this. I have found countless implementations of side menus, but all that I have found to this date will only show the side menu on only one view controller instead of showing up on all of the side menu's target and treating them if they are all on the same level so to say. In essence, the side menu needs to be able to come up on all of the 3 view controllers and take away the need to hackily segue over a view controller. As well, it would be quite ideal if this side menu is easily scalable so I can add multiple sections to the side menu with ease.


like image 968
Noah Wilder Avatar asked Oct 30 '18 06:10

Noah Wilder


2 Answers

enter image description here

I've created a sample project for this question. You can see the output in the above image. basically what I did is created a wrapper class around the Sidebar and then I used it whenever I want :)

Sidebar

import UIKit
protocol SidebarDelegate {
    func sidbarDidOpen()
    func sidebarDidClose(with item: Int?)
}
class SidebarLauncher: NSObject{

    var view: UIView?
    var delegate: SidebarDelegate?
    var vc: NavigationViewController?
    init(delegate: SidebarDelegate) {
        super.init()
        self.delegate = delegate
    }

    func show(){
        let bounds = UIScreen.main.bounds
        let v = UIView(frame: CGRect(x: -bounds.width, y: 0, width: bounds.width, height: bounds.height))
        v.backgroundColor = .clear
        let vc = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "NavigationController") as! NavigationViewController
        v.addSubview(vc.view)
        vc.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            vc.view.topAnchor.constraint(equalTo: v.topAnchor),
            vc.view.leadingAnchor.constraint(equalTo: v.leadingAnchor),
            vc.view.bottomAnchor.constraint(equalTo: v.bottomAnchor),
            vc.view.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -60)
            ])
        vc.delegate = self
        v.isUserInteractionEnabled = true
        v.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))))
        self.view = v
        self.vc = vc
        UIApplication.shared.keyWindow?.addSubview(v)

        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.curveEaseOut], animations: {
            self.view?.frame = CGRect(x: 0, y: 0, width: self.view!.frame.width, height: self.view!.frame.height)
            self.view?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        }, completion: {completed in
            self.delegate?.sidbarDidOpen()
        })

    }

    @objc func handleTapGesture(_ sender: UITapGestureRecognizer){
        closeSidebar(option: nil)
    }
    func closeSidebar(option: Int?){
        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.curveEaseOut], animations: {
            if let view = self.view{
                view.frame = CGRect(x: -view.frame.width, y: 0, width: view.frame.width, height: view.frame.height)
                self.view?.backgroundColor = .clear

            }
        }, completion: {completed in
            self.view?.removeFromSuperview()
            self.view = nil
            self.vc = nil
            self.delegate?.sidebarDidClose(with: option)
        })
    }

}
extension SidebarLauncher: NavigationDelegate{
    func navigation(didSelect: Int?) {
        closeSidebar(option: didSelect)
    }
}

NavigationController

import UIKit
protocol NavigationDelegate{
    func navigation(didSelect: Int?)
}

class NavigationViewController: UIViewController{

    @IBOutlet weak var buttonLaunchVC: UIButton!
    @IBOutlet weak var buttonSecondViewController: UIButton!
    @IBOutlet weak var buttonThirdViewController: UIButton!


    var delegate: NavigationDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        [buttonLaunchVC,buttonSecondViewController,buttonThirdViewController].forEach({
            $0?.addTarget(self, action: #selector(didSelect(_:)), for: .touchUpInside)
        })
    }

    @objc func didSelect(_ sender: UIButton){
        switch sender {
        case buttonLaunchVC:
            delegate?.navigation(didSelect: 0)
        case buttonSecondViewController:
            delegate?.navigation(didSelect: 1)
        case buttonThirdViewController:
            delegate?.navigation(didSelect: 2)
        default:
            break
        }
    }


    @IBAction func CloseMenu(_ sender: Any) {
        delegate?.navigation(didSelect: nil)
    }


}

ViewController

class ViewController: UIViewController {

    @IBAction func OpenMenu(_ sender: Any) {
        SidebarLauncher(delegate: self ).show()
    }

}
extension ViewController: SidebarDelegate{
    func sidbarDidOpen() {
        print("Sidebar opened")
    }

    func sidebarDidClose(with item: Int?) {
        guard let item = item else {return}
        print("Did select \(item)")
        switch item {
        case 0:
           break
        case 1:
            let v = UIStoryboard.main.SecondVC()
            present(v!, animated: true)
        case 2:
            let v = UIStoryboard.main.ThirdVC()
            present(v!, animated: true)
        default:
            break
        }
    }

The main area of interest is SidebarLauncher class what it does: when you call the show() method. it creates a UIView then it adds it to keywindow (i.e. your current View) and after that It adds the NavigationController.

To setup communication with the sidebar, I've created two Protocols

  1. SidebarDelegate:

Sidebar delegate is the main protocol through which you get to know if the user has selected any ViewController or not.

  1. NavigationDelegate: this protocol is used for communication between the wrapper and the navigation controller. When user tap any button. it informs the wrapper class about it.

The wrapper class has a method closeSidebar which then closes the sidebar and inform the Controller class that sidebar is closed with option.

In sidebarDidClose, you can decide what to do with the selection made by user.

I was in a bit of hurry which is why I used Int whereas you should consider using struct or class whichever suits your need to identify which ViewController to open.

https://github.com/sahilmanchanda2/SidebarTest

like image 177
Sahil Avatar answered Sep 21 '22 23:09

Sahil


You need use an Coordinator

raywenderlich Coordinator

townsend Coordinator

like image 33
YannSteph Avatar answered Sep 25 '22 23:09

YannSteph