Кастомный Navigation Bar

Новости, Туториалы

Кастомный Navigation Bar

Привет!

Часто бывает, что желание и дизайн клиента не всегда соответствует вашему мнению. И иногда это может приносить некий дискомфорт в работе. В этой статье речь пойдет о NavigationBar.

Вы когда-нибудь задумывались как работает NavigationBar? Наверняка нет. Просто подключаете, появляются кнопки навигации, можно добавлять другие кнопки, задавать им какие-то действия, менять заголовок, размер и прочее.

А что если у вас будет задача сделать NavigationBar с другим дизайном и с другим расположением картинок/заголовок/кнопок? Есть выход - сделать свой CustomNavigationBar при помощи XIB.  

Давайте начнем! Для начала нам нужно создать проект, и сразу создаем 2 файла. 

Первый - swift файл, который будет наследоваться от класса UIView

А второй - xib файл, который будет служить дизайном для нашего будущего NavigationBar. Нажимаем cmd + N -> User Interface -> View

Начнем с дизайна. При создании xib файла нужно задать ему размер. Нажимаем на View, переходим в Attributes inspector и меняем Size на Freeform и Top Bar на None

Устанавливаем высоту 140. И перетаскиваем в наше View еще один UIView, далее который и будет нашим NavigationBar. Закрепляем его констрейнтами: слева, снизу, справа и сверху к Safe Area. Это очень важно, так как нам нужен динамический NavigationBar, который будет приспосабливаться к модели телефона (с челкой/без) и устанавливать правильную высоту.

Для примера я пока покрашу наш NavigationBar в желтый цвет, а сам View в зеленый. Таким образом можно будет наблюдать изменения.

В этот раз вместо UILabel как заголовка мы будем использовать UIImage. Которая по дизайну может быть логотип компании. Устанавливаем по центру зеленого View, закрепляем высоту. Слева и справа добавляем кнопки. 

Примерно так должно получиться:

Далее нам нужно соединить все @IBOutlets, @IBActions с swift файлом. Но он пока не знает о xib файле. Для этого нужно нажать на File’s Owner -> Identity inspector установить swift файл NavigationBar.

Теперь соединяем все @IBOutlets и @IBActions. В целом должно получиться 4 @IBOutlets (сам главный желтый View, кнопки и картинка) и 2 @IBActions (для кнопок). 

И теперь немного кода.

    @IBOutlet var contentView: UIView!
    @IBOutlet weak var logoImageView: UIImageView!
    @IBOutlet weak var leftButton: UIButton!
    @IBOutlet weak var rightButton: UIButton!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() {

    }

Методы override init(frame: CGRect) и required init?(coder: NSCoder) нужны для инициализации нашего View, где устанавливаются все необходимые настройки для корректного отображения нашего NavigationBar.

В методе commonInit(), будет происходить вся косметическая настройка, где мы окончательно прикрутим xib (дизайн) и swift (код) файлы. 

Настройка будет состоять из 5 этапов:

  • получаем bundle файла (let bundle = Bundle(for: NavigationBar.self)
  • разархивируем содержимое файла, расположенного в bundle (bundle.loadNibNamed(“NavigationBar”, owner: self, options: nil))
  • добавляем главный View (желтый) в качестве subView (addSubview(contentView)
  • для желтого View устанавливаем рамки, которые описывают расположение и размер (contentView.frame = self.bounds)
  • и последнее нужно установить возможность растягиваться по ширине и высоте для адаптации (contentView.auterizingMask = [.flexibleWidth, .flexibleHeight])
    private func commonInit() {
        let bundle = Bundle(for: NavigationBar.self)
        bundle.loadNibNamed("NavigationBar", owner: self, options: nil)
        addSubview(contentView)
        contentView.frame = self.bounds
        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }

Теперь займемся ViewController(ом). Добавляем UIView в ViewController и закрепляем его слева, справа по 0 к Safe Area, а верх к SuperView на 0. Так как у нас уже задан Safe Area в нашем NavigationBar. И устанавливаем высоту UIView на 140. Теперь в Identity inspector устанавливаем класс NavigationBar для UIView

В storyboard вы никаких изменений не увидите. Для исправления этой ситуации class NavigationBar нужно сделать @IBDesignable

Добавляем @IBOutlet weak var customNavigationBar: NavigationBar! в ViewController.

После таких нехитрых манипуляций у вас должно получиться как-то так:

Таким же образом создаем и настраиваем SecondViewController.

Далее поработаем с навигацией. В этом плане нам поможет тот самый NavigationController, от которого мы отказались, но не полностью. Он может быть нам полезен. Связываем ViewController с NavigationController. От ViewController перетаскиваем segue на SecondViewController. Убираем галочку с Shows Navigation Bar.

Отлично! Теперь если запустите приложение на этом этапе, то увидите, что все отображается именно так, как мы хотели. Но есть один нюанс - как достучаться до кнопок? Ответ прост - нам поможет Protocol!

Возвращаемся в наш class NavigationBar и выше его создадим протокол NavigationBarDelegate, который как раз и будет отвечать за нажатие кнопок. Создаем 2 метода - func leftAction(), func rightAction(). А чтобы было вообще  красиво, мы сделаем эти методы опциональными (необязательными) так как не всегда нам понадобится сразу все методы, плюс так будет выглядеть аккуратнее код. Чтобы методы были опциональными нужно добавить @objc.

@objc protocol NavigationBarDelegate: class {
    @objc optional func leftAction()
    @objc optional func rightAction()
}

Далее создаем слабую ссылку на делегата в NavigationBar (weak var delegate: NavigationBarDelegate?) и в @IBAction каждом кнопки выполняем методы нашего делегата (delegate?.leftAction?() - для левой кнопки / delegate?.rightAction?() - для правой кнопки) 

 

Теперь нам осталось только обработать нажатия в ViewController и SecondViewController

Благодаря тому, что мы создали @IBOutlet customNavigationBar, в методе viewDidLoad() подписываемся под делегат (customNavigationBar.delegate = self

И ViewController и SecondViewController подписываем на NavigationBarDelegate, чтобы можно было проработать логику при нажатии: 

  • для ViewController логика будет такая: func rightAction() { self.performSegue(withIdentifier: "showNext", sender: nil) }.
  • для SecondViewController такая: func leftAction() { self.navigationController?.popViewController(animated: true) }.

 

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

 

И не забываем прятать кнопки в зависимости от контроллеров в viewDidLoad():

  • для ViewController: customNavigationBar.leftButton.isHidden = true
  • для SecondViewController: customNavigationBar.rightButton.isHidden = true

 

Супер! Мои поздравления! Теперь если у вас возникнет подобная ситуация, вы смело будете знать что делать!

Полный код класса ViewController:

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var customNavigationBar: NavigationBar!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.navigationController?.interactivePopGestureRecognizer?.delegate = self
        
        customNavigationBar.leftButton.isHidden = true
        customNavigationBar.delegate = self
    }
}

extension ViewController: NavigationBarDelegate {
    func rightAction() {
        self.performSegue(withIdentifier: "showNext", sender: nil)
    }
}

Полный код класса SecondViewController:

import UIKit

class SecondViewController: UIViewController {

    @IBOutlet weak var customNavigationBar: NavigationBar!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        customNavigationBar.rightButton.isHidden = true
        customNavigationBar.delegate = self
    }
}

extension SecondViewController: NavigationBarDelegate {
    func leftAction() {
        self.navigationController?.popViewController(animated: true)
    }
}

Бонус.

При отключении стандартного UINavigationBar пропадает реализация возврата на предыдущий экран свайпом влево. К счастью это мелкое недоразумение можно исправить несколькими строками кода.

В методе viewDidLoad() пропишите self.navigationController?.interactivePopGestureRecognizer?.delegate = self

И подписав ViewController на UIGestureRecognizerDelegate выполнить метод делегата: func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true }

extension ViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

Теперь все должно работать как надо! 

Статью подготовил: Михаил Цейтлин
Конечный проект урока.

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: