Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Global elegant way to add bar button items to any UIViewController of the project

Usually we have a predefined set of UIBarButtonItem in the project that can be used in the project and multiple times like a left menu button to open a side menu, it can be used in different UIViewControllers also a close button that dismiss the presented view controller.

The classic way is to add these buttons as needed, but this introduce a code duplication and we all want to avoid that.

My come up with an approach, but it's far from being perfect :

enum BarButtonItemType {
    case menu, close, notification
}

enum BarButtonItemPosition{
    case right, left
}

extension UIViewController {

    func add(barButtons:[BarButtonItemType], position: BarButtonItemPosition) {

        let barButtonItems = barButtons.map { rightBarButtonType -> UIBarButtonItem in
            switch rightBarButtonType {
            case .menu:
                return UIBarButtonItem(image: UIImage(named:"menu"),
                    style: .plain,
                    target: self,
                    action: #selector(presentLeftMenu(_:)))
            case .notification:
                return UIBarButtonItem(image: UIImage(named:"notification"),
                style: .plain,
                target: self,
                action: #selector(showNotification(_:)))
            case .close:
                return UIBarButtonItem(image: UIImage(named:"close"),
                    style: .plain,
                    target: self,
                    action: #selector(dismissController(_:)))
            }
        }

        switch position {
        case .right:
            self.navigationItem.rightBarButtonItems = barButtonItems
        case .left:
            self.navigationItem.leftBarButtonItems  = barButtonItems
        }
    }

    // MARK: Actions
    @objc fileprivate func presentLeftMenu(_ sender:AnyObject) {
        self.parent?.presentLeftMenuViewController(sender)
    }

    @objc fileprivate func dismissController(_ sender:AnyObject) {
        self.dismiss(animated: true, completion: nil)
    }

   @objc fileprivate func showNotification(_ sender:AnyObject) {
       let notificationViewController = UINavigationController(rootViewController:NotificationViewController())
       self.present(notificationViewController, animated: true, completion: nil)
   }
}

and then the usage:

override func viewDidLoad() {
    super.viewDidLoad()
    self.add(barButtons: [.close], position: .right)
    self.add(barButtons: [.menu], position: .left)
}

The limitations of my approach are:

  • The extension needs to know how to instantiate new view controller (case of notification for example) and what if viewController must be inited with parameters

  • It assumes that you only want to present a UIViewController

  • Not elegant.

I am sure that there is better way with Swift language and protocol oriented programming that can achieve the intended result with more flexibility, any thoughts ?

like image 473
iOSGeek Avatar asked Sep 14 '17 14:09

iOSGeek


People also ask

How do I add a button to the navigation bar on storyboard?

Drop a button on the right-hand side of the navigation bar, then switch to the assistant editor so we can connect it to some code. Ctrl-drag from your new bar button item into your source code, and when you release your mouse button change Connection from “Outlet” to “Action”.

How do I customize the navigation bar in Swift?

Go to the ViewController. swift file and add the ViewDidAppear method. a nav helper variable which saves typing. the Navigation Bar Style is set to black and the tint color is set to yellow, this will change the bar button items to yellow.


1 Answers

It seems that you're after having a default bar button configuration but specific (to subclass of UIViewController) bar button action implementations. You mentioned 1. "The extension needs to know how to instantiate new view controller" and your second point 2. "It assumes that you only want to present a UIViewController", thats a good sign that your extension should delegate that job to a subclass that knows what to do with those actions. Here I've done a sample implementation:

enum BarButtonItemPosition {
    case right, left
}

enum BarButtonItemType {
    case menu(BarButtonItemPosition)
    case close(BarButtonItemPosition)
    case notification(BarButtonItemPosition)
}

/// Has default implementation on UIViewControllers that conform to BarButtonActions.
protocol BarButtonItemConfiguration: class {

    func addBarButtonItem(ofType type: BarButtonItemType)
}

/// Hate that we're forced to expose button targets to objc runtime :(
/// but I don't know any other way for the time being, maybe in Swift 6 :)
@objc protocol BarButtonActions {
    @objc func presentLeftMenu(_ sender:AnyObject)
    @objc func dismissController(_ sender:AnyObject)
    @objc func showNotification(_ sender:AnyObject)
}

extension BarButtonItemConfiguration where Self: UIViewController, Self: BarButtonActions {

    func addBarButtonItem(ofType type: BarButtonItemType) {

        func newButton(imageName: String, position: BarButtonItemPosition, action: Selector?) {
            let button = UIBarButtonItem(image: UIImage(named: imageName), style: .plain, target: self, action: action)
            switch position {
            case .left: self.navigationItem.leftBarButtonItem = button
            case .right: self.navigationItem.rightBarButtonItem = button
            }
        }

        switch type {
        case .menu(let p): newButton(imageName: "", position: p, action: #selector(Self.presentLeftMenu(_:)))
        case .notification(let p): newButton(imageName: "", position: p, action: #selector(Self.showNotification(_:)))
        case .close(let p): newButton(imageName: "", position: p, action: #selector(Self.dismissController(_:)))
        }
    }
}

/// Conform to this in subclasses of UIViewController and implement BarButtonActions (its impl. differs from vc to vc).
protocol BarButtonConfigarable: BarButtonItemConfiguration, BarButtonActions {}

/// example
class SampleVC: UIViewController, BarButtonConfigarable {

    override func viewDidLoad() {
        super.viewDidLoad()
        addBarButtonItem(ofType: .menu(.right))
        addBarButtonItem(ofType: .menu(.left))
    }

    @objc func presentLeftMenu(_ sender:AnyObject) {
        // TODO:
    }
    @objc func dismissController(_ sender:AnyObject) {
        // TODO:
    }
    @objc func showNotification(_ sender:AnyObject) {
        // TODO:
    }
}
like image 199
Lukas Avatar answered Nov 15 '22 20:11

Lukas