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




Основы


Достаточно часто встречается следующий кейс: по нажатию на задний фон скрывать клавиатуру.


Базовое решение — имеем ссылку на UITextField, создаем UITapGestureRecognizer с методом, который снимает выделение с текстового поля и выглядит оно так:


В данной статье используется Swift 3, но можно реализовать и на других версиях и на Objective-C

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture))
        view.addGestureRecognizer(tapGesture)
    }

    func tapGesture() {
        textField.resignFirstResponder()
    }
}

Проблемы данного кода:


  • viewDidLoad грязный и нечитабельный
  • много кода в контроллере
  • не переиспользуемый

Делаем читабельным


Для решения первой проблемы мы можем вынести код создания и добавления жеста в отдельную функцию:


class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        addTapGestureToHideKeyboard()
    }

    func addTapGestureToHideKeyboard() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture))
        view.addGestureRecognizer(tapGesture)
    }

    func tapGesture() {
        textField.resignFirstResponder()
    }
}

Кода стало еще больше, но он стал чище, логичней и приятней глазу.


Уменьшение кода


Для решения второй проблемы у UIView есть метод:


func endEditing(_ force: Bool) -> Bool

Он как раз отвечает за снятие выделения с самой вьюхи или ее subview. Благодаря ему мы можем сильно упростить наш код:


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        addTapGestureToHideKeyboard()
    }

    func addTapGestureToHideKeyboard() {
        let tapGesture = UITapGestureRecognizer(target: view, action: #selector(view.endEditing))
        view.addGestureRecognizer(tapGesture)
    }
}

Если делаете по шагам, то не забудьте удалить textField свойство из IB.
Также поменяйте target с self на view.

Код стал радовать глаз! Но копировать это в каждый контроллер все еще придется.


Решение копирования


Для переиспользуемости вынесем наш метод добавления в extension контроллера:


extension UIViewController {
    func addTapGestureToHideKeyboard() {
        let tapGesture = UITapGestureRecognizer(target: view, action: #selector(view.endEditing))
        view.addGestureRecognizer(tapGesture)
    }
}

И код нашего контроллера будет выглядеть следующим образом:


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        addTapGestureToHideKeyboard()
    }
}

Чисто, одна строчка кода, и она переиспользуема! Идеально!


Несколько вьюх


Решение выше — очень хорошее, но в нем кроется один минус: мы не можем добавить жест на конкретную вьюху.


Для решения данного кейса воспользуемся расширением UIView:


extension UIView {
    func addTapGestureToHideKeyboard() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(endEditing))
        addGestureRecognizer(tapGesture)
    }
}

и соответственно код контроллера будет выглядеть так:


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addTapGestureToHideKeyboard()
    }
}

Тут возникает другая проблема: данное расширение решает проблему только для вьюхи контроллера. Если мы добавить someView на view и на нее повесим жест, то не сработает. Это все из-за того, что метод endEditing работает только для вьюхи, которая содержит активную вьюху или сама таковой является, а нашего текстового поля скорее всего не будет в нем. Решим данную проблему.


Т.к. view контроллера точно будет содержать активную вьюху, и наша добавленная вьюха всегда будет в ее иерархии, то мы можем дотянуться до view контроллера через superview и у нее вызвать endEditing.


Получаем view контроллера через расширение UIView:


var topSuperview: UIView? {
    var view = superview
    while view?.superview != nil {
        view = view!.superview
    }
    return view
}

Скажу сразу, изменив селектор на:


#selector(topSuperview?.endEditing)

работать все еще не будет. Нам необходимо добавить метод, который будет вызывать конструкцию выше:


func dismissKeyboard() {
    topSuperview?.endEditing(true)
}

Вот теперь заменяем селектор на:


#selector(dismissKeyboard)

Итак, расширение для UIView будет выглядеть следующим образом:


extension UIView {

    func addTapGestureToHideKeyboard() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        addGestureRecognizer(tapGesture)
    }

    var topSuperview: UIView? {
        var view = superview
        while view?.superview != nil {
            view = view!.superview
        }
        return view
    }

    func dismissKeyboard() {
        topSuperview?.endEditing(true)
    }
}

Теперь, используя addTapGestureToHideKeyboard() для любой вьюхи мы будем скрывать клавиатуру.


KeyboardHideManager


Icon


Решением выше я пользовался долгое время, но потом стал замечать, что даже одна строка загрязняет функцию установки вьюх. Так же, (редко, но все же бывает) не очень красиво выглядит, когда это единственный метод во viewDidLoad:


override func viewDidLoad() {
    super.viewDidLoad()
    addTapGestureToHideKeyboard()
}

Вместе с пробелом это занимает 5 строк, что сильно сказывается на чистоте контроллера! У меня появилась идея сделать все это без кода, чтобы не было в контроллере и одной лишней строчки. Я создал класс, который можно добавить в IB с помощью Object


object


И с помощью @IBOutlet привязать нужные нам вьюхи:


preview


Реализация данного класса наипростейшая — добавляем жест на каждую вьюху с помощью выше созданных функций:


final public class KeyboardHideManager: NSObject {

    @IBOutlet internal var targets: [UIView]! {
        didSet {
            for target in targets {
                addGesture(to: target)
            }
        }
    }

    internal func addGesture(to target: UIView) {
        let gesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        target.addGestureRecognizer(gesture)
    }

    @objc internal func dismissKeyboard() {
        targets.first?.topSuperview?.endEditing(true)
    }
}

extension UIView {
    internal var topSuperview: UIView? {
        var view = superview
        while view?.superview != nil {
            view = view!.superview
        }
        return view
    }
}

Чтобы воспользоваться данным классом, нужно три простых действия:


  • 1) Перетащить Object в контроллер

usage_1


  • 2) Установить KeyboardHideManager в качестве класса данного объекта

usage_2


  • 3) Соединить нужные вьюхи со свойством targets

usage_3


Да, больше действий, чем написать одну строку (или несколько строк, если несколько определенных вьюх), за то этой самой строки нету в контроллере.


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


Данный класс можете подключить через CocoaPods или просто скопировать в проект.


Ссылка на исходный код KeyboardHideManager с полным ReadMe о самой библиотеке и ее подключении.


Итог


Разобрали реализацию популярного кейса, рассмотрели несколько его решений, в одну строку и без кода вообще. Используйте тот способ, который больше нравится.

Поделиться с друзьями
-->

Комментарии (6)


  1. as_thunderbolt
    26.12.2016 12:54
    +1

    Позволил бы себе еще добавить false в cancelsTouchesInView у класса UIGestureRecognizer. Ведь если Вы, например, добавите в таргеты UITableView, то нажатия на ячейки работать не будут. Ну и прочие не очень приятные моменты. :)


  1. svistkovr
    26.12.2016 12:59
    +1

    В вашей статье вы едите «в Москву через Владивосток».

    Добавляете в базовый класс метод

    - (IBAction) closeKeyboardByGesture.....


    В любом xib/storyboard файле вы можете добавить Tap Gesture прямо на view/viewcontroller и вынести ваш селектор простым перетягиванием IBAction к базовому методу.



    1. PapaBubaDiop
      26.12.2016 13:54
      +1

      Именно, и

      viewDidLoad()
      будет девственно чистым, как и мечтал автор.


    1. NeverendingWinter
      26.12.2016 14:29

      Дело вкуса. Я бы сделал как автор — предпочитаю держать все в коде, так легче (мне) отслеживать происходящее


  1. alexyat
    26.12.2016 14:29

    Когда дочитал до места где автору не нравится одна строка кода в viewDidLoad, вспомнился анекдот про "неаккуратненько как то"(легко гуглится по этой фразе), способ с IB, для многих не актуален, т.к. строят интерфейс в loadView. И еще слово в защиту одной строки кода: чтобы выяснить привязан там селектор или нет нужно лезть в IB и искать там нужный VC. А так полезный туториал.Спасибо.


  1. MariyaSafonova
    26.12.2016 14:29
    +3

    Для совсем ленивых можно использовать библиотеку: IQKeyboardManager
    Не только клавиатуру скрывает, перемещает между textField, но и поднимает вью вверх, в случаях, если клавиатура скрывает textField
    — вообще одна строчка кода на весь проект, и забыть о проблеме со скрытием клавиатуры