Irbisa · Шпаргалка

Auto Layout и UIKit-вёрстка

Constraint-ы, intrinsic content size, UIStackView, frame-ы, safe area

Junior-разработчик · 25 вопросов

  1. 01

    Что такое Auto Layout и чем он отличается от вёрстки на frame?

    Auto Layout строит вёрстку на constraint-ах: вы описываете отношения между view («на 16 pt ниже заголовка, той же ширины, что карточка»), а решатель на каждом проходе layout превращает их во frame-ы.

  2. 02

    Какое уравнение выражает constraint в Auto Layout и что такое layout anchors?

    Любой constraint представляет собой линейное уравнение item1.attribute = multiplier × item2.attribute + constant, где вместо равенства может стоять ≤ или ≥.

    // The equation spelled out with the original initializerNSLayoutConstraint(item: avatar, attribute: .leading,                   relatedBy: .equal,                   toItem: card, attribute: .leading,                   multiplier: 1, constant: 16).isActive = true…
  3. 03

    Сколько constraint-ов нужно view, чтобы её layout был однозначным?

    Столько, чтобы зафиксировать четыре значения (позицию x и ширину, позицию y и высоту); обычно это четыре constraint-а, по два на ось.

  4. 04

    Вы задали constraint-ы для view, созданной в коде, но она не появляется там, где ожидалось, а в консоли список сломанных constraint-ов. Что здесь не так?

    override func viewDidLoad() {  super.viewDidLoad()  let card = UIView()  card.backgroundColor = .systemBlue  view.addSubview(card)  NSLayoutConstraint.activate([    card.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),    card.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),    card.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),    card.heightAnchor.constraint(equalToConstant: 120),  ])}

    У card по-прежнему translatesAutoresizingMaskIntoConstraints равно true, поэтому UIKit превращает её frame (.zero) и autoresizing mask в constraint-ы, которые противоречат вашим.

  5. 05

    Этот код настройки падает с «Unable to activate constraint ... because they have no common ancestor». Что здесь сделано не в том порядке?

    let badge = UILabel()badge.text = "NEW"badge.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([  badge.topAnchor.constraint(equalTo: card.topAnchor, constant: 8),  badge.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -8),]) card.addSubview(badge)

    Constraint-ы активируются до того, как badge попала в иерархию view.

  6. 06

    Что такое safe area и как в UIKit удержать контент внутри неё?

    Под safe area понимают часть view, которую не перекрывают статус-бар, navigation и tab bar, home indicator или Dynamic Island.

    // Full-bleed background: the view's edges, not the safe areaNSLayoutConstraint.activate([  background.topAnchor.constraint(equalTo: view.topAnchor),  background.bottomAnchor.constraint(equalTo: view.bottomAnchor),  background.leadingAnchor.constraint(equalTo: view.leadingAnchor),  background.trailingAnchor.constraint(equalTo: view.trailingAnchor),…
  7. 07

    Почему constraint-ы ставят к leadingAnchor и trailingAnchor, а не к leftAnchor и rightAnchor?

    Leading и trailing следуют направлению чтения, поэтому в арабском и иврите layout отзеркаливается автоматически; left и right обозначают физические стороны и не переворачиваются никогда.

  8. 08

    Что такое UIStackView и какие у него плюсы и минусы?

    UIStackView выстраивает свои subview в строку или столбец и сам создаёт constraint-ы между ними: вы задаёте axis, distribution, alignment и spacing, а constraint-ы ставите только самому стеку.

    let metadataRow = UIStackView(arrangedSubviews: [authorLabel, dateLabel])metadataRow.axis = .horizontalmetadataRow.spacing = 4 let content = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel, metadataRow])content.axis = .vertical…
  9. 09

    В горизонтальном UIStackView все элементы получились одной ширины, а иконка растянута на всю высоту строки. Какие два свойства заданы неверно?

    distribution задаёт размеры вдоль оси, а alignment отвечает за расположение поперёк неё: одинаковую ширину даёт distribution = .fillEqually, растянутую иконку даёт alignment = .fill по умолчанию.

    let row = UIStackView(arrangedSubviews: [icon, title, chevron])row.axis = .horizontalrow.spacing = 8 // wrong: equal widths, and the icon stretched to the row height// row.distribution = .fillEqually…
  10. 10

    Что такое intrinsic content size и как content hugging и compression resistance решают, какая view уступит?

    Intrinsic content size означает размер, который view хочет иметь по своему контенту: текст лейбла, заголовок кнопки с отступами.

    let title = UILabel()title.text = "A long article title that will not fit"let badge = UILabel()badge.text = "NEW" // The title stretches into free space and truncates first…
  11. 11

    Что делают @IBDesignable и @IBInspectable и чем команды заменяют live rendering сегодня?

    @IBInspectable выводит свойство кастомной view в Attributes inspector в Interface Builder, а @IBDesignable заставляет Interface Builder собрать view и отрисовать её на канвасе.

    @IBDesignablefinal class RoundedButton: UIButton {  @IBInspectable var cornerRadius: CGFloat = 0 {    didSet { layer.cornerRadius = cornerRadius }  }…
  12. 12

    Что такое приоритеты constraint-ов и почему для constraint-а, который «обязан» выполняться, ставят 999, а не 1000?

    Приоритет от 1 до 1000 говорит, насколько сильно решатель старается выполнить constraint: 1000 (.required) обязан выполниться, всё ниже считается необязательным: решатель выполняет его настолько близко, насколько может, и без записи в лог уступает при конфликте с более сильным.

    // Preferred width: 320 if it fits, but never past the marginslet preferred = card.widthAnchor.constraint(equalToConstant: 320)preferred.priority = .defaultHigh   // 750: yields to required constraintsNSLayoutConstraint.activate([  preferred,  card.centerXAnchor.constraint(equalTo: view.centerXAnchor),…
  13. 13

    Этот бейдж после изменения count рисует новое число, но сохраняет старую ширину. Чего не хватает?

    final class BadgeView: UIView {  var count = 0 {    didSet { setNeedsDisplay() }  }   override var intrinsicContentSize: CGSize {    let text = "\(count)" as NSString    let font = UIFont.preferredFont(forTextStyle: .caption1)    let size = text.size(withAttributes: [.font: font])    return CGSize(width: ceil(size.width) + 16, height: ceil(size.height) + 8)  }   override func draw(_ rect: CGRect) {    // draws the pill and the number  }}

    В didSet нет вызова invalidateIntrinsicContentSize().

  14. 14

    Чем отличается привязка к краю view, к её layoutMarginsGuide и к её readableContentGuide?

    Край совпадает с буквальными bounds view, layoutMarginsGuide даёт bounds минус margins самой view, а readableContentGuide дополнительно ограничивает ширину, чтобы строки текста на широких экранах оставались удобной длины.

    // Three edges with three meaningslabel.leadingAnchor.constraint(equalTo: view.leadingAnchor)                      // literal boundslabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor)   // plus marginslabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor) // readable width // Directional margins mirror in right-to-left languages…
  15. 15

    Чем отличаются setNeedsLayout(), layoutIfNeeded() и layoutSubviews()?

    setNeedsLayout() помечает view как требующую layout в следующем цикле обновления, layoutIfNeeded() выполняет отложенный layout прямо сейчас, а layoutSubviews() является методом, который UIKit вызывает во время этого layout: первые два вызываете вы, третий вы переопределяете.

    final class GradientView: UIView {  private let gradient = CAGradientLayer()   override init(frame: CGRect) {    super.init(frame: frame)    layer.addSublayer(gradient)…
  16. 16

    Layout должен меняться при переключении состояния. Вы меняете constant, переключаете активные constraint-ы или меняете приоритет?

    Меняйте constant, когда меняется только число (это чисто анимируется), переключайте наборы активных constraint-ов, когда меняются сами отношения, и меняйте приоритеты, когда два конкурирующих constraint-а должны побеждать по очереди.

    private var heightConstraint: NSLayoutConstraint!   // stored once, reused func setExpanded(_ expanded: Bool, animated: Bool) {  heightConstraint.constant = expanded ? 240 : 88  guard animated else { view.setNeedsLayout(); return }  UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }…
  17. 17

    Найдите баг: панель должна плавно выезжать снизу, но прыгает на новое место без анимации.

    final class PlayerViewController: UIViewController {  private let panel = UIView()  // activated in viewDidLoad with leading, trailing and height 300  private lazy var panelBottom = panel.bottomAnchor.constraint(    equalTo: view.bottomAnchor, constant: 300)  // starts off-screen   func showPanel() {    panelBottom.constant = 0    UIView.animate(withDuration: 0.3) {      self.panel.layoutIfNeeded()    }  }}

    layoutIfNeeded() вызывается у panel, но изменённый constraint задаёт положение panel внутри view, поэтому блок ничего не двигает.

  18. 18

    В консоли появляется «Unable to simultaneously satisfy constraints». Как найти виновника?

    Прочитайте лог: в нём перечислены constraint-ы, которые не могут выполниться вместе, и назван сломанный.

  19. 19

    Что должно выполняться, чтобы ячейка таблицы сама подстраивала высоту под контент?

    Нужна непрерывная цепочка вертикальных constraint-ов от верха до низа contentView, чтобы Auto Layout мог вычислить высоту; таблица при этом использует UITableView.automaticDimension, и это уже значение по умолчанию.

    final class MessageCell: UITableViewCell {  private let title = UILabel()  private let body = UILabel()   override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {    super.init(style: style, reuseIdentifier: reuseIdentifier)…
  20. 20

    Как разложить контент внутри UIScrollView на Auto Layout так, чтобы он действительно прокручивался?

    Scroll view вычисляет размер контента по его constraint-ам, поэтому контент нужно привязать ко всем четырём краям contentLayoutGuide, а ширину взять от frameLayoutGuide.

    let scrollView = UIScrollView()let content = UIStackView()            // one container for everythingcontent.axis = .verticalcontent.spacing = 16[scrollView, content].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }view.addSubview(scrollView)…
  21. 21

    Текстовое поле внизу формы оказывается под клавиатурой. Как его правильно поднять в современном UIKit и в чём ошибался старый код на нотификациях?

    Привяжите низ поля к view.keyboardLayoutGuide.topAnchor.

    // iOS 15+: one constraint, animated together with the keyboardNSLayoutConstraint.activate([  composer.leadingAnchor.constraint(equalTo: view.leadingAnchor),  composer.trailingAnchor.constraint(equalTo: view.trailingAnchor),  composer.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -8),  scrollView.bottomAnchor.constraint(equalTo: composer.topAnchor),   // content stays visible…
  22. 22

    Что такое size classes и как переключаться между двухколоночной вёрсткой на iPad и одноколоночной на iPhone?

    Size classes дают грубые трейты, .compact или .regular, для ширины и высоты, которые реально достались вашей view.

    // wrong: a hardware question; an iPad in Split View is still .pad but gets a compact width// if UIDevice.current.userInterfaceIdiom == .pad { showTwoColumns() } // right: the trait describes the space this view actually gotprivate func applyLayout() {  let regular = traitCollection.horizontalSizeClass == .regular…
  23. 23

    Как в UIKit-вёрстке обрабатывать поворот устройства?

    В основном никак: constraint-ы относительно родителя и safe area сами пересчитываются под новый размер.

    override func viewWillTransition(to size: CGSize,                                 with coordinator: UIViewControllerTransitionCoordinator) {  super.viewWillTransition(to: size, with: coordinator)  let wide = size.width > size.height  coordinator.animate(alongsideTransition: { _ in    self.contentStack.axis = wide ? .horizontal : .vertical…
  24. 24

    Один экран загружает CPU на 100% и зависает, а layoutSubviews() вызывается снова и снова. Что вызывает петлю обратной связи в Auto Layout?

    Что-то внутри прохода layout снова инвалидирует layout: constraint-ы меняются на основе геометрии, которую только что вычислил тот же проход, и каждый проход планирует следующий.

    // wrong: the constant is derived from the frame this pass produced; whenever it// changes this view's own size, the next pass gets new input and the cycle never// ends. layoutIfNeeded() starts that next pass inside this one.override func layoutSubviews() {  super.layoutSubviews()  insetConstraint.constant = bounds.width * 0.1…
  25. 25

    Прокручиваемый список роняет кадры, а Instruments показывает время в Auto Layout. Что вы проверите и что поменяете?

    Выясните, сколько работы layout делает каждая ячейка на шаге прокрутки, и сократите её: меньше view и constraint-ов в ячейке, никаких изменений constraint-ов при переиспользовании, а для немногих «горячих» ячеек, которые всё ещё слишком дороги, используйте frame-ы.