Основы UIKit
Жизненный цикл view controller, `frame` против `bounds`, переиспользование ячеек, цепочка ответчиков и hit-testing, `CALayer` и распознаватели жестов.
12 вопросов
JuniorТеорияОчень частоЧто делает dequeueReusableCell и какой баг возникает, если пропустить prepareForReuse?
Что делает dequeueReusableCell и какой баг возникает, если пропустить prepareForReuse?
dequeueReusableCell возвращает переиспользованную ячейку из пула, а не создаёт новую, поэтому прокрутка дёшева. Ячейка хранит состояние прошлой строки, поэтому без prepareForReuse на чужой строке виден старый текст или картинка.
Типичные ошибки
- ✗Считают, что ячейка из пула приходит очищенной, а не с прошлым состоянием
- ✗Думают, что
dequeueReusableCellсоздаёт новую ячейку на каждом вызове - ✗Считают
prepareForReuseнеобязательной косметической формальностью
Уточняющие вопросы
- →Где ещё, кроме
prepareForReuse, можно сбросить переиспользованную ячейку? - →Почему незавершённая async-загрузка картинки всё ещё портит переиспользованную ячейку?
JuniorТеорияОчень частоВ каком порядке вызываются viewDidLoad, viewWillAppear и viewDidAppear и что уместно в каждом?
В каком порядке вызываются viewDidLoad, viewWillAppear и viewDidAppear и что уместно в каждом?
viewDidLoad вызывается один раз после загрузки view — сюда кладут разовую настройку. viewWillAppear — перед каждым появлением, обновляет данные. viewDidAppear — когда view на экране, запускает анимации.
Типичные ошибки
- ✗Кладут обновление данных в
viewDidLoad, и оно не обновляется при возврате - ✗Считают, что
viewWillAppearвызывается один раз, а не при каждом появлении - ✗Запускают анимации в
viewWillAppear, пока view ещё не на экране
Уточняющие вопросы
- →Почему
viewDidLoadможет выполниться до того, как view получит финальный размер? - →Когда
viewWillAppearвызовется снова после push с последующим pop?
JuniorКодЧастоНастройте UITableViewDiffableDataSource со снапшотом
Настройте UITableViewDiffableDataSource со снапшотом
Постройте data source с замыканием cell-provider под каждый Item. Создайте NSDiffableDataSourceSnapshot, вызовите appendSections([.main]) и appendItems(items), затем dataSource.apply(snapshot). В отличие от reloadData, он анимирует лишь изменившиеся строки.
Типичные ошибки
- ✗Зовут
reloadDataвместо применения снапшота - ✗Забывают
appendSectionsпередappendItems - ✗Думают, что diffable data source только для collection view
Уточняющие вопросы
- →Как
apply(_:animatingDifferences:)выбирает анимации строк? - →Почему
Itemдолжен бытьHashable, чтобы сравнение работало?
JuniorТеорияЧастоЧем отличаются frame и bounds у view и когда bounds.origin не равен (0,0)?
Чем отличаются frame и bounds у view и когда bounds.origin не равен (0,0)?
frame — прямоугольник view в координатах суперпредставления: позиция и размер. bounds — тот же прямоугольник в своих координатах, origin обычно (0,0). Scroll view сдвигает bounds.origin при прокрутке — тогда он отличается.
Типичные ошибки
- ✗Считают, что
frameиbounds— всегда один и тот же прямоугольник - ✗Думают, что
bounds.originне может быть ничем, кроме(0,0) - ✗Путают, какой из прямоугольников задан в координатах суперпредставления
Уточняющие вопросы
- →Как применение
CGAffineTransformвлияет наframeи наbounds? - →Почему внутри
layoutSubviewsчитаютbounds, а неframe?
JuniorТеорияЧастоКакие состояния у UIGestureRecognizer, почему два распознавателя конфликтуют и что делает require(toFail:)?
Какие состояния у UIGestureRecognizer, почему два распознавателя конфликтуют и что делает require(toFail:)?
Распознаватель проходит possible, активные состояния для непрерывных жестов или recognized для дискретных, плюс failed/cancelled. Два конфликтуют, когда касание подходит обоим, как тап внутри pan. require(toFail:) заставляет один ждать провала другого.
Типичные ошибки
- ✗Думают, что у распознавателей нет состояний
failed/cancelled - ✗Считают, что
require(toFail:)отключает другой распознаватель - ✗Полагают, что два распознавателя не могут получить одно касание
Уточняющие вопросы
- →Чем
shouldRecognizeSimultaneouslyWithотличается отrequire(toFail:)? - →Почему дискретный тап пропускает состояния
began/changed?
JuniorТеорияЧастоЧто такое цепочка ответчиков, как action доходит до обработчика и что такое first responder?
Что такое цепочка ответчиков, как action доходит до обработчика и что такое first responder?
Цепочка ответчиков — упорядоченный список UIResponder: view, суперпредставления, контроллер, окно, приложение. Action с целью nil идёт вверх, пока объект не реализует селектор. First responder — голова цепочки.
Типичные ошибки
- ✗Думают, что action рассылается всем view, а не идёт по цепочке
- ✗Считают, что цепочка идёт сверху вниз от приложения к листовой view
- ✗Путают first responder с app delegate
Уточняющие вопросы
- →Как
becomeFirstResponderменяет то, куда идёт ввод с клавиатуры? - →Куда цепочка продолжается за пределами собственной view контроллера?
JuniorТеорияЧастоЧто выигрываешь и теряешь со Storyboard, XIB и UI только в коде, особенно в большой команде?
Что выигрываешь и теряешь со Storyboard, XIB и UI только в коде, особенно в большой команде?
Storyboard показывают поток визуально и быстры на старте, но плохо мёржатся и конфликтуют в большой команде. XIB — файлы на одну view, конфликтуют реже. UI в коде избегает боли слияний и удобнее для ревью, но без превью.
Типичные ошибки
- ✗Называют Storyboard самым удобным для слияний вариантом
- ✗Думают, что UI в коде труднее сравнивать (diff), чем Storyboard
- ✗Считают, что три варианта не влияют на работу большой команды
Уточняющие вопросы
- →Как segue с container view уменьшают площадь слияний у Storyboard?
- →Почему раскладку в коде легче покрыть модульными тестами, чем XIB?
JuniorТеорияЧастоЧем владеют UIView и CALayer по отдельности и что значит «layer-backed view»?
Чем владеют UIView и CALayer по отдельности и что значит «layer-backed view»?
За каждой UIView стоит CALayer — он занимается отрисовкой, геометрией и анимацией, а view добавляет касания и цепочку ответчиков. «Layer-backed» значит, что отрисовка делегируется слою, где и живёт cornerRadius.
Типичные ошибки
- ✗Думают, что view рисует сама, а не делегирует отрисовку слою
- ✗Считают view и слой не связанными объектами, соединёнными вручную
- ✗Ставят
cornerRadius/shadowна view, ожидая поведения слоя
Уточняющие вопросы
- →Почему можно анимировать свойство слоя, у которого нет аналога на
UIView? - →В чём разница между frame у view и frame у её слоя?
MiddleДебаггингЧастоИсправьте таблицу, где при быстрой прокрутке строки на миг показывают чужую картинку
Исправьте таблицу, где при быстрой прокрутке строки на миг показывают чужую картинку
Замыкание захватывает переиспользуемую ячейку, отданную другой строке раньше прихода картинки, поэтому рисуется чужой аватар. Сбросьте картинку в prepareForReuse и в завершении сверьте URL ячейки, иначе отбросьте устаревшую загрузку.
Типичные ошибки
- ✗Винят нестабильность
IndexPathвместо переработки ячеек - ✗Чинят отключением переиспользования вместо защиты завершения
- ✗Забывают сбросить картинку до старта async-загрузки
Уточняющие вопросы
- →Как токен загрузки на ячейку позволяет игнорировать устаревшее завершение?
- →Почему сброс в
prepareForReuseбез отмены всё равно оставляет мигание?
MiddleТеорияЧастоЧто делают setNeedsLayout, layoutIfNeeded, layoutSubviews и setNeedsDisplay и когда каждый выполняется?
Что делают setNeedsLayout, layoutIfNeeded, layoutSubviews и setNeedsDisplay и когда каждый выполняется?
setNeedsLayout помечает view «грязной», чтобы layoutSubviews выполнился на следующем проходе; layoutIfNeeded форсирует его сейчас. layoutSubviews располагает подвиды, не зовётся напрямую. setNeedsDisplay планирует перерисовку через draw(_:) — отдельный проход.
Типичные ошибки
- ✗Зовут
layoutSubviewsнапрямую вместоsetNeedsLayout - ✗Думают, что
setNeedsLayoutраскладывает синхронно, какlayoutIfNeeded - ✗Путают проход перерисовки (
setNeedsDisplay) с проходом раскладки
Уточняющие вопросы
- →Зачем группировать несколько
setNeedsLayoutперед однимlayoutIfNeeded? - →Когда run loop на самом деле сбрасывает отложенный проход раскладки?
MiddleТеорияИногдаКак hitTest и point(inside:) выбирают view для касания и как сделать кнопку нажимаемой за пределами bounds родителя?
Как hitTest и point(inside:) выбирают view для касания и как сделать кнопку нажимаемой за пределами bounds родителя?
hitTest обходит дерево view сзади наперёд, вызывая point(inside:); выигрывает самая глубокая view, чей bounds держит точку. Касание вне bounds родителя отбрасывается, поэтому зону нажатия расширяют через point(inside:).
Типичные ошибки
- ✗Думают, что один
clipsToBounds = falseрасширяет зону нажатия - ✗Считают, что
hitTestиспользуетframe/z-порядок, а не геометриюbounds - ✗Полагают, что касание вне
boundsродителя всё равно дойдёт до ребёнка
Уточняющие вопросы
- →Почему касание ребёнка обрезается, даже когда он рисуется за пределами родителя?
- →Как возврат
nilизhitTestделает view прозрачной для касаний?
MiddleДизайнИногдаВы строите переиспользуемый компонент-пейджер, который держит несколько полноценных дочерних экранов — каждый самостоятельный UIViewController со своей загрузкой данных, сетевыми вызовами и требованиями к жизненному циклу. Можно затолкать view всех экранов в один гигантский контроллер или сделать каждый экран настоящим дочерним контроллером внутри своего контейнера. Объясните, когда контейнерный контроллер уместнее одного монолитного, какие вызовы контейнмента (addChild, didMove(toParent:), willMove(toParent:), removeFromParent) нужны и в каком порядке при добавлении и удалении ребёнка, и что ломается — колбэки появления, распространение traitCollection, поворот, память — если их пропустить и просто добавить view ребёнка как подвид.
Вы строите переиспользуемый компонент-пейджер, который держит несколько полноценных дочерних экранов — каждый самостоятельный UIViewController со своей загрузкой данных, сетевыми вызовами и требованиями к жизненному циклу. Можно затолкать view всех экранов в один гигантский контроллер или сделать каждый экран настоящим дочерним контроллером внутри своего контейнера. Объясните, когда контейнерный контроллер уместнее одного монолитного, какие вызовы контейнмента (addChild, didMove(toParent:), willMove(toParent:), removeFromParent) нужны и в каком порядке при добавлении и удалении ребёнка, и что ломается — колбэки появления, распространение traitCollection, поворот, память — если их пропустить и просто добавить view ребёнка как подвид.
Используйте контейнер, когда каждый экран — самостоятельный контроллер со своим циклом, а не монолит. Добавление ребёнка — addChild, вставить child.view, затем didMove(toParent:); удаление — willMove(toParent: nil), убрать view, removeFromParent. Пропустите — и колбэки появления и поворот перестанут доходить.
Типичные ошибки
- ✗Добавляют
child.viewкак подвид безaddChild/didMove - ✗Путают порядок вызовов добавления/удаления, и колбэки срабатывают не так
- ✗Считают, что события появления и трейтов доходят без контейнмента
Уточняющие вопросы
- →Почему
didMove(toParent:)должен идти после вставки view ребёнка? - →Как
shouldAutomaticallyForwardAppearanceMethodsменяет этот порядок?