В данной статье разберем, как скрывать клавиатуру по нажатию на вьюху от самых основ до реализации в одну строчку или совсем без кода.
Основы
Достаточно часто встречается следующий кейс: по нажатию на задний фон скрывать клавиатуру.
Базовое решение — имеем ссылку на 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
Решением выше я пользовался долгое время, но потом стал замечать, что даже одна строка загрязняет функцию установки вьюх. Так же, (редко, но все же бывает) не очень красиво выглядит, когда это единственный метод во viewDidLoad
:
override func viewDidLoad() {
super.viewDidLoad()
addTapGestureToHideKeyboard()
}
Вместе с пробелом это занимает 5 строк, что сильно сказывается на чистоте контроллера! У меня появилась идея сделать все это без кода, чтобы не было в контроллере и одной лишней строчки. Я создал класс, который можно добавить в IB с помощью Object
И с помощью @IBOutlet
привязать нужные нам вьюхи:
Реализация данного класса наипростейшая — добавляем жест на каждую вьюху с помощью выше созданных функций:
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 в контроллер
- 2) Установить KeyboardHideManager в качестве класса данного объекта
- 3) Соединить нужные вьюхи со свойством targets
Да, больше действий, чем написать одну строку (или несколько строк, если несколько определенных вьюх), за то этой самой строки нету в контроллере.
Кто-то может сказать, что лучше написать строку в коде, так понятнее, и будут правы, с одной стороны. Я за то, чтобы вынести из контроллера, все что можно и тем самым его облегчить.
Данный класс можете подключить через CocoaPods или просто скопировать в проект.
Ссылка на исходный код KeyboardHideManager с полным ReadMe о самой библиотеке и ее подключении.
Итог
Разобрали реализацию популярного кейса, рассмотрели несколько его решений, в одну строку и без кода вообще. Используйте тот способ, который больше нравится.
Комментарии (6)
svistkovr
26.12.2016 12:59+1В вашей статье вы едите «в Москву через Владивосток».
Добавляете в базовый класс метод
- (IBAction) closeKeyboardByGesture.....
В любом xib/storyboard файле вы можете добавить Tap Gesture прямо на view/viewcontroller и вынести ваш селектор простым перетягиванием IBAction к базовому методу.
NeverendingWinter
26.12.2016 14:29Дело вкуса. Я бы сделал как автор — предпочитаю держать все в коде, так легче (мне) отслеживать происходящее
alexyat
26.12.2016 14:29Когда дочитал до места где автору не нравится одна строка кода в viewDidLoad, вспомнился анекдот про "неаккуратненько как то"(легко гуглится по этой фразе), способ с IB, для многих не актуален, т.к. строят интерфейс в loadView. И еще слово в защиту одной строки кода: чтобы выяснить привязан там селектор или нет нужно лезть в IB и искать там нужный VC. А так полезный туториал.Спасибо.
MariyaSafonova
26.12.2016 14:29+3Для совсем ленивых можно использовать библиотеку: IQKeyboardManager
Не только клавиатуру скрывает, перемещает между textField, но и поднимает вью вверх, в случаях, если клавиатура скрывает textField
— вообще одна строчка кода на весь проект, и забыть о проблеме со скрытием клавиатуры
as_thunderbolt
Позволил бы себе еще добавить false в cancelsTouchesInView у класса UIGestureRecognizer. Ведь если Вы, например, добавите в таргеты UITableView, то нажатия на ячейки работать не будут. Ну и прочие не очень приятные моменты. :)