Default UIBarButtonItems with protocols

:  ~ 1 min read

Having to create the same set of buttons over and over can become cumbersome. We'll try to make use of protocols and implement some default ones. Let's start with that:

@objc protocol CanGoBack { // Sounds better than Backable :)

  func back()
  
}

extension CanGoBack where Self: UIViewController {
  
  func back() {
    if presentingViewController != nil, navigationController?.childViewControllers.first == self {
      navigationController?.dismiss(animated: true)
    }
    else {
	  navigationController?.popViewController(animated: true)
    }
  }
  
  func setupBackButton() {
    guard navigationController?.childViewControllers.first != self else { return }
	
    let backButton = UIButton.myBackButton()
    backButton.addTarget(self, action: #selector(back), for: .touchUpInside) // ??
    navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
  }
  
}

But here we stumble upon the problem: back has to be marked as @objc, to be able to create the #selector, but that's not possible; @objc is to be used for members of classes, @objc protocols, and concrete extensions of classes.

Sadly, there's no way to work around this without some subclassing, but the good news is it's rather straightforward:

class Button: UIButton {
  // `action` is the internal variable of type `Selector`.
  private let buttonAction: () -> Void
    
  @objc private func performButtonAction() {
    buttonAction()
  }
	
  init(action: () -> Void) {
    buttonAction = action
    let image = UIImage(named: "back")
    super.init(frame: CGRect(origin: .zero, size: image?.size ?? .zero))
    setImage(image, forState: .Normal)
    addTarget(self, action: #selector(performButtonAction), forControlEvents: .TouchUpInside)
  }
}

We then modify our protocol, by replacing the UIButton creation:

func setupBackButton() {
  guard
    navigationController?.childViewControllers.first != self
    else { return }

  let backButton = Button(action: { [weak self] in self?.back() } )
  navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
}

And from now on, all we have to do for an UIViewController to have a back button and action is:

class SecondMainViewController: UIViewController, CanGoBack {
  override viewDidLoad() {
    super.viewDidLoad()
    setupBackButton()
  }
}

We could go a step further and subclass UIBarButtonItem, but since the logic is exactly the same, there's no need to include it here.