Slide in Menu as an UIViewController Extension

iOS Programmatic Slide in Menu Extension

 

We often need to add a slide in menu to a UIViewController. It’s no easy task in Interface Builder. Here’s how to make one in code…

Slide in Menu as an UIViewController Extension

 

First create a new file called MenuMgr.swift in your project. Declare the class… (link to the code at the bottom)

class MenuMgr : NSObject, UITableViewDelegate, UITableViewDataSource {
    var tvSections : [String]?
    var tvRows : [[String]]?
    var tvMenu : UITableView!
    var btnBG : UIButton!
    var delegate : MenuDelegate!

We have a few properties. We’re going to have an array of Strings for the section titles. We have an array of String arrays for the rows. So section 3 has a title of tvSections[3] and row values of tvRows[3].

We also have properties for the tableview to be created, a button (coming later) and a MenuDelegate that will handle the callback when the user taps on a menu item.

Next let’s look at creating the menu items. We return the number of sections as the count of the tvSections and similarly for the number of rows w/ the tvRows item.

For the section header, we return a view – this allows us to set the colors and such. You could use the call back for section title instead.

We also create the cell and set the text to the related tvRow entry – we get the right String array based on the section and then the element in that array with the row.

    public func numberOfSections(in tableView: UITableView) -> Int {
        guard tvSections != nil else { return 0 }
        return tvSections!.count
    }
    
    public func tableView(_ tableView: UITableView, 
           numberOfRowsInSection section: Int) -> Int {
        guard tvRows != nil else { return 0 }
        return tvRows![section].count
    }
    
    func tableView(_ tableView: UITableView, 
           viewForHeaderInSection section: Int) -> UIView?
    {
        let headerView = UIView(frame: CGRect(x: 0, y: 0, 
           width: tableView.bounds.size.width, height: 30))
        headerView.backgroundColor = UIColor.black
        
        let lbl = UILabel.init(frame: headerView.bounds.insetBy
           (dx: 10.0, dy: 0.08))
        lbl.text = tvSections![section]
        lbl.textColor = UIColor.white
        headerView.addSubview(lbl)
        
        return headerView
    }
    
    public func tableView(_ tableView: UITableView, 
           cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: 
          "cellMenu", for: indexPath)

        cell.textLabel?.text = tvRows![indexPath.section][indexPath.row]
        cell.textLabel?.textColor = UIColor.white
        cell.contentView.backgroundColor = UIColor.clear
        cell.backgroundColor = UIColor.clear
        return cell
    }

Next we need to handle when the user taps a menu item.

    func tableView(_ tableView: UITableView, 
      didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        delegate.didSelectMenuItem(atIndexPath: indexPath)
    }

This deselects the row so it doesn’t stay highlighted. And it calls the delegate method w/ the user selected indexPath.

That’s the only function on the delegate protocol defined at the top of the fine as:

protocol MenuDelegate {
    func didSelectMenuItem(atIndexPath ip : IndexPath)
}

To create our menu, we have a call to createMenu. This takes the arrays: section titles, row arrays, the view to display the menu on and the delegate:

    func createMenu(sections : [String], rows : [[String]],
                    forView view : UIView, menuDelegate : MenuDelegate) {
        delegate = menuDelegate
        
        tvSections = sections
        tvRows = rows
        let w = view.frame.size.width * 0.4
        tvMenu = UITableView.init(frame: CGRect.init(x: -(w+1),
                 y: 0, width: w, height: view.frame.size.height))
        tvMenu.delegate = self
        tvMenu.dataSource = self
        tvMenu.register(UITableViewCell.self,
                        forCellReuseIdentifier: "cellMenu")
        tvMenu.alpha = 0.0
        tvMenu.backgroundColor = UIColor.darkGray
        tvMenu.separatorStyle = .none
        view.addSubview(tvMenu)
        
        btnBG = UIButton.init(frame: view.bounds)
        btnBG.backgroundColor = UIColor.init(red: 0.5, green: 0.5,
                                             blue: 0.5, alpha: 0.5)
        btnBG.alpha = 0.0
        btnBG.addTarget(self, action: #selector(MenuMgr.doBtnBG), 
           for: .touchUpInside)
    }

This sets the properties, creates the table view and a button. The menu size is based on the view frame. For the table view, we set the delegate, datasource and register a cell.

The button will be behind the menu so if the user taps anywhere outside the menu the doBtnGB function will be called.

doBtnBG hides the menu:

    @objc func doBtnBG(sender : UIButton) {
        UIView.animate(withDuration: 0.3) {
            self.hideMenu()
        }
    }

Of course we need functions to show and hide the menu:

    func showMenu() {
        tvMenu.superview?.addSubview(btnBG)
        tvMenu.superview?.bringSubview(toFront: tvMenu)
        tvMenu.alpha = 1.0
        UIView.animate(withDuration: 0.3) {
            self.btnBG.alpha = 1.0
            var r = self.tvMenu.frame
            r.origin.x = 0
            self.tvMenu.frame = r
        }
    }
    
    func hideMenu() {
        UIView.animate(withDuration: 0.3, animations: {
            self.btnBG.alpha = 0.0
            var r = self.tvMenu.frame
            r.origin.x = -(r.size.width + 1)
            self.tvMenu.frame = r
        }) { (success) in
            self.tvMenu.alpha = 0.0
        }
    }

These could be abstracted out to one function to be more efficient.

Below I’l put the whole file and below that we’ll get into how to create and show the menu:

Whole File:

//
//  MenuMgr.swift
//  MenuProject
//
//  Created by Bear  Cahill on 10/6/17.
//  Copyright © 2017 Bear  Cahill. All rights reserved.
//

import UIKit

protocol MenuDelegate {
    func didSelectMenuItem(atIndexPath ip : IndexPath)
}

class MenuMgr : NSObject, UITableViewDelegate, UITableViewDataSource {
    var tvSections : [String]?
    var tvRows : [[String]]?
    var tvMenu : UITableView!
    var btnBG : UIButton!
    var delegate : MenuDelegate!
    
    public func numberOfSections(in tableView: UITableView) -> Int {
        guard tvSections != nil else { return 0 }
        return tvSections!.count
    }
    
    public func tableView(_ tableView: UITableView,
                          numberOfRowsInSection section: Int) -> Int {
        guard tvRows != nil else { return 0 }
        return tvRows![section].count
    }
    
    func tableView(_ tableView: UITableView,
                   viewForHeaderInSection section: Int) -> UIView?
    {
        let headerView = UIView(frame: CGRect(x: 0, y: 0,
              width: tableView.bounds.size.width, height: 30))
        headerView.backgroundColor = UIColor.black
        
        let lbl = UILabel.init(frame: headerView.bounds.insetBy(dx: 10.0, dy: 0.08))
        lbl.text = tvSections![section]
        lbl.textColor = UIColor.white
        headerView.addSubview(lbl)
        
        return headerView
    }
    
    public func tableView(_ tableView: UITableView,
                          cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cellMenu", for: indexPath)
        cell.textLabel?.text = tvRows![indexPath.section][indexPath.row]
        cell.textLabel?.textColor = UIColor.white
        cell.contentView.backgroundColor = UIColor.clear
        cell.backgroundColor = UIColor.clear
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        delegate.didSelectMenuItem(atIndexPath: indexPath)
    }
    
    func createMenu(sections : [String], rows : [[String]],
                    forView view : UIView, menuDelegate : MenuDelegate) {
        delegate = menuDelegate
        
        tvSections = sections
        tvRows = rows
        let w = view.frame.size.width * 0.4
        tvMenu = UITableView.init(frame: CGRect.init(x: -(w+1),
                 y: 0, width: w, height: view.frame.size.height))
        tvMenu.delegate = self
        tvMenu.dataSource = self
        tvMenu.register(UITableViewCell.self,
                        forCellReuseIdentifier: "cellMenu")
        tvMenu.alpha = 0.0
        tvMenu.backgroundColor = UIColor.darkGray
        tvMenu.separatorStyle = .none
        view.addSubview(tvMenu)
        
        btnBG = UIButton.init(frame: view.bounds)
        btnBG.backgroundColor = UIColor.init(red: 0.5, green: 0.5,
                                             blue: 0.5, alpha: 0.5)
        btnBG.alpha = 0.0
        btnBG.addTarget(self, action: #selector(MenuMgr.doBtnBG), for: .touchUpInside)
    }
    
    @objc func doBtnBG(sender : UIButton) {
        UIView.animate(withDuration: 0.3) {
            self.hideMenu()
        }
    }
    
    func showMenu() {
        tvMenu.superview?.addSubview(btnBG)
        tvMenu.superview?.bringSubview(toFront: tvMenu)
        tvMenu.alpha = 1.0
        UIView.animate(withDuration: 0.3) {
            self.btnBG.alpha = 1.0
            var r = self.tvMenu.frame
            r.origin.x = 0
            self.tvMenu.frame = r
        }
    }
    
    func hideMenu() {
        UIView.animate(withDuration: 0.3, animations: {
            self.btnBG.alpha = 0.0
            var r = self.tvMenu.frame
            r.origin.x = -(r.size.width + 1)
            self.tvMenu.frame = r
        }) { (success) in
            self.tvMenu.alpha = 0.0
        }
    }
    
}

Creating a Menu

In your code, add a property for the MenuMgr:

let menuMgr = MenuMgr()

Whenever you want to create the menu (e.g., viewDidLoad), call createMenu with the sections, rows and delegate:

 menuMgr.createMenu(sections: ["Settings", "More"], 
   rows: [["Logout","Profile","Other"],["Things","About"]], 
   forView: self.view, 
   menuDelegate: self)

Or menu sections will be “Settings” and “More”. Settings will have 3 options: Logout, Profile, Other and the More section will have 2: Things and About.

iOS slide in menu

To show the menu we call showMenu on our menuMgr:

menuMgr.showMenu()

And to hide… you guessed it:

menuMgr.hideMenu()

Download the code: MenuProject zip file