Универсальный глобальный элегантный способ добавления элементов панели кнопок в любой UIViewController проекта

Обычно у нас есть предопределенный набор UIBarButtonItem в проекте, который можно использовать в проекте и несколько раз, как левая кнопка меню, чтобы открыть боковое меню, его можно использовать в разных UIViewController и кнопку закрытия которые отклоняют представленный контроллер представления.

Классический способ состоит в том, чтобы добавлять эти кнопки по мере необходимости, но это приводит к дублированию кода, и мы все хотим этого избежать.

Я придумал подход, но это далеко не идеальный:

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

а затем использование:

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

Ограничения моего подхода:

  • Расширение должно знать, как создать экземпляр нового контроллера представлений (например, случай уведомления), и что, если viewController должен быть включен с параметрами

  • Предполагается, что вы хотите только представить UIViewController

  • Не элегантный.

Я уверен, что есть лучший способ с языком программирования Swift и протоколом, который может достичь намеченного результата с большей гибкостью, любыми мыслями?

Ответ 1

Кажется, что у вас есть конфигурация бара по умолчанию, но специфические (для подкласса UIViewController) реализаций действий кнопки бара. Вы упомянули 1. "Расширение должно знать, как создать экземпляр нового контроллера представления" и второй пункт 2. "Предполагается, что вы хотите только представить UIViewController", это хороший признак того, что ваше расширение должно делегировать это задание подклассу который знает, что делать с этими действиями. Здесь я сделал пример реализации:

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