Интересно, есть ли еще люди, как я, — любящие программирование за тот самый момент, когда ты осознаешь причину ошибки и понимаешь, что все с самого начала было довольно очевидно? Сразу после этого мистика улетучивается, и все расставляется по своим местам.
Сегодня я расскажу о как раз таком случае, постигшем меня на почти ровном месте. Подозреваю, что для многих развязка этого «триллера» будет довольно очевидной задолго до конца. Хотя я потратил какое-то время, пытаясь понять что происходит.
Появление проблемы
Большинство читающих эту статью знакомо с инициализацией переменных с помощью автоматически вызываемого замыкания. В свою очередь, большинство из этого большинства использовали его для создания того или иного элемента управления с последующей настройкой его полей. Такой подход позволяет обособить создание элементов управления, выделив их и визуально, и логически.
Но кто мог подумать, что привычный паттерн создания кнопки, помноженный на небольшую логическую ошибку, приведет к удивительным последствиям?
Ладно, думаю, хватит риторических вопросов, лучше покажу вам банальнейший фрагмент кода, который каждый из нас писал много раз:
class CustomCell: UITableViewCell { let actionButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("Do", for: .normal) button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside) return button }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(actionButton) // добавить констрейнты } @objc func buttonTapped(sender: UIButton) { print("tap !") } }
Я намеренно максимально утрировал пример, чтоб сконцентрироваться на основной сути вопроса. Такой код много раз писал почти любой Swift-разработчик, в том числе и я. Каково же было мое удивление, когда оказалось, что обработчик нажатия на кнопку не работает.
Ошибочные гипотезы
Так как в прошлом я несколько раз наступал на похожие грабли, то, потерев места, где были прошлые шишки, проверил возможные причины несрабатывания.
Проверка показала, что isUserInteractionEnabled
выставлен в true
по всей иерархии. Кроме того, я убедился, что кнопка добавлена в contentView
ячейки, а не просто во view
(поднимите руку те, кто делал такую ошибку во времена своего джунства). На всякий случай я даже временно отключил allowsSelection
для таблицы, но естественно дело было не в нем.
Следующей гипотезой стало то что self
в момент выполнения замыкания, создающего кнопку равен nil
. Эту теорию я почти сразу откинул, так как она противоречит знаниям про жизненный цикл класса. Действительно, инициализация полей класса, для которых заданы значения, происходит до вызова конструктора, но self
при этом должен быть недоступен, да и он вообще не опционал.
Разумеется, мне стало интересно, «что ты такое», и в код замыкания добавился print(type(of: self))
, а результат меня изрядно озадачил:
(CustomCell) -> () -> CustomCell
Крутящиеся в голове мысли — «какой-то тут self
странный» и «а откуда он вообще взялся» — натолкнули на правильный эксперимент.
Поменяв let
на lazy var
, я добился того, что код заработал, ведь ленивые переменные инициализируются уже после того как класс инициализирован, и в их замыканиях self
содержит ожидаемое значение, что и подтвердил отладочный вывод:
CustomCell
«Но что это было?»
В целом, на этом этапе ошибку уже можно было считать исправленной. Понятно, почему она возникла (вместо self
у нас какая-то ерунда, а так как первый параметр addTarget(_:action:for:)
имеет тип Any?
, метод радостно принимает почти что угодно). Очевидно, как ее избежать (добавлять обработчик там, где уже доступен «нормальный» self
), но мне, как, наверно, и большинству людей на моем месте, стало интересно, что такое self
в случае let
.
Зубры мобильной разработки, помнящие времена Objective-C уже поняли, откуда «растут ноги» этого неправильного self
: на самом деле, это instance-метод NSObject
, ожидаемо возвращающий сам класс, помноженный на каррирование при объявлении методов, просуществовавшее в Swift на протяжении двух первых версий. В ходе реализации SE-0002 его (к счастью) выпилили из синтаксиса декларации функций, но в семантике методов он остался.
В обычной жизни с этим рудиментарным self
столкнуться довольно сложно: его «закрывает» ключевое слово, поэтому, чтобы «достучаться» до метода NSObject
, нужно использовать обратные кавычки. Сравните:
print(type(of: NSObject.self)) print(type(of: NSObject.`self`)) print(type(of: NSObject().self)) print(type(of: NSObject().`self`))
Эти четыре вызова дают нам ожидаемые результаты:
NSObject.Type (NSObject) -> () -> NSObject NSObject () -> NSObject
Именно со вторым вариантом я и столкнулся изначально, только вместо NSObject
попался его дальний потомок CustomCell
.
Многолетняя проблема
Забавно, что Xcode одновременно помогал и мешал найти ошибку.
Помощь заключалась в попытках подставить скобки при автодополнении. IDE понимает, что это метод, и логично предполагает, что его надо вызывать. Я, к сожалению, изначально не обратил на это внимания. А вот то, что подсветка кода выделяла его тем же цветом, что и обычный self
, внесло небольшую лепту в запутывание ситуации.
Позже, порывшись немного в сети, я обнаружил, что подобные вопросы давно обсуждаются в сообществе Swift. Вот, например, тема на форуме самого Swift или схожая проблема на Stack Overflow (забавно, что все наталкиваются на эту проблему именно с UIButton
). Да и в багтрекере есть как минимум SR-4559 и SR-4865.
Впрочем, последние два имеют достаточно низкий приоритет, и не факт, что этот вопрос будет как-то решен в ближайшем будущем. Поэтому главная защита от подобных проблем — только расширение собственных знаний о работе «глубинных» механизмов языка и библиотек.
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: