Урок по треку Swift и iOS 8 мин

UIKit: жизненный цикл, Auto Layout, списки

Жизненный цикл приложения и UIViewController, Auto Layout, UITableView и UICollectionView, diffable data source.

Большинство продакшен-приложений в РФ по-прежнему в значительной части на UIKit, и вопросы по нему звучат на каждом собесе.

Жизненный цикл приложения

С iOS 13 UI-жизненный цикл живёт в SceneDelegate: у каждой сцены (окна на iPad) свои состояния. AppDelegate отвечает за процесс: запуск, push-токены, конфигурацию сцен.

Состояния приложения: не запущено, inactive (переходное, например при входящем звонке), active, background (короткое время на завершение работы), suspended (в памяти, код не выполняется). Из suspended система может выгрузить приложение без предупреждения.

Жизненный цикл UIViewController

Порядок при первом показе:

  1. loadView(): создаёт корневую view. Переопределяют, если вёрстка в коде.
  2. viewDidLoad(): один раз. Настройка сабвью, констрейнтов, подписок.
  3. viewWillAppear(_:): перед каждым показом, в том числе при возврате назад.
  4. viewIsAppearing(_:) (iOS 17, доступен с iOS 13): размеры и trait collection уже известны.
  5. viewWillLayoutSubviews() / viewDidLayoutSubviews(): раскладка, может повторяться много раз.
  6. viewDidAppear(_:): переход завершён, аналитика, старт анимаций.

При уходе: viewWillDisappear, viewDidDisappear. Если контроллер закрыт и на него нет ссылок, вызывается deinit, и это хорошая проверка на утечки.

Ловушка: в viewDidLoad размеры view ещё не финальные. Ручные расчёты фреймов делают в viewDidLayoutSubviews.

frame и bounds

frame это положение и размер в координатах супервью, bounds собственная система координат. У UIScrollView bounds.origin это смещение прокрутки. При transform frame становится описанным прямоугольником.

Auto Layout

let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(title)
NSLayoutConstraint.activate([
    title.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
    title.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    title.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -16),
])

Ключевые понятия:

  • Intrinsic content size: естественный размер (у лейбла по тексту).
  • Content hugging: сопротивление растяжению больше естественного размера.
  • Compression resistance: сопротивление сжатию меньше естественного. Если два лейбла не помещаются, обрежется тот, у кого сопротивление сжатию ниже.
  • Приоритеты от 1 до 1000. Необязательный констрейнт (меньше 1000) можно нарушить, если иначе не решить систему.

Анимация констрейнта:

heightConstraint.constant = 200
UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }

setNeedsLayout только помечает раскладку на следующий цикл, layoutIfNeeded выполняет её сейчас.

Hit-testing и responder chain

Касание ищет view сверху вниз: окно вызывает hitTest(_:with:), view проверяет point(inside:) по своим bounds и опрашивает детей. View не получит касание, если она скрыта, alpha < 0.01, isUserInteractionEnabled = false или торчит за границы родителя. Событие, которое никто не обработал, поднимается по responder chain к супервью, контроллеру, окну, приложению.

Таблицы и коллекции

Ячейки переиспользуются: dequeueReusableCell(withIdentifier:for:) отдаёт ячейку, ушедшую за экран. Отсюда правила:

  • каждое переменное свойство ячейки задаётся в конфигурации в обе стороны (badge.isHidden = !post.isPromo);
  • в prepareForReuse сбрасывают картинку и отменяют загрузку;
  • асинхронный результат проверяют на актуальность (ячейка могла уже показывать другую строку).

Self-sizing: UITableView.automaticDimension, разумный estimatedRowHeight, непрерывная цепочка констрейнтов внутри contentView.

Diffable data source (iOS 13): данные описываются снимком с идентификаторами секций и элементов, а apply сам считает и анимирует разницу.

var snapshot = NSDiffableDataSourceSnapshot<Section, Post.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(posts.map(\.id))
dataSource.apply(snapshot, animatingDifferences: true)

Идентификаторы обязаны быть уникальными: дубликат (например, со стыка страниц пагинации) приводит к исключению. Для обновления содержимого без замены ячейки есть reconfigureItems (iOS 15).

Compositional layout (iOS 13) описывает сложные коллекции из item, group и section, включая горизонтальный скролл секции (orthogonalScrollingBehavior).

Плавность

Бюджет кадра 16,7 мс на 60 Гц и 8,3 мс на 120 Гц. Типичные причины рывков в списках: синхронная работа в cellForRowAt (сеть, диск, декодирование картинок, создание DateFormatter), тени без shadowPath, лишние маски и прозрачность, reloadData вместо точечных обновлений.

Ещё ловушка: таймер, созданный через scheduledTimer, замирает при скролле, потому что run loop переходит в режим tracking. Его добавляют в режим .common.

Частые ошибки

  • Создавать ячейку через инициализатор вместо dequeue.
  • Читать bounds в viewDidLoad.
  • Забыть translatesAutoresizingMaskIntoConstraints = false.
  • Менять UI из фонового потока.

Что спросят на собесе

  • Жизненный цикл UIViewController и что в каком методе делать.
  • Как работают переиспользование ячеек и prepareForReuse?
  • Content hugging и compression resistance на примере.
  • Почему кнопка не нажимается (hit-testing)?
  • Как найти и убрать рывки при скролле?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.