Я заметил, что многие люди в Интернете (например, StackOverflow) переходят к нескольким свойствам UIViewController, когда их представления не отображаются должным образом, а затем манипулируют ими почти случайным образом, чтобы добиться желаемого поведения. Это неизменно приводит к тому, что «принятый» ответ на проблему с макетом «устанавливает автоматическиAdjutstsScrollViewInsets на [true / false]» или «устанавливает крайForExtendedLayout на UIRectEdgeZero», и один человек говорит «спасибо, это исправлено!» и еще пять человек сказали: «Это не помогло, пожалуйста, помогите». Даже в видеороликах Apple о WWDC нет супер четкого объяснения того, как эти API работают и какое влияние они оказывают друг на друга, хотя в WWDC 2013, когда они были представлены, есть общие обзоры. Я не смог найти хороших описаний того, как все это работает, поэтому я хотел опубликовать PSA о том, как именно работают эти API, чтобы мы могли начать исправлять вещи правильным способом и прекратить увековечивать неверную информацию.

Я считаю, что примеры приложений с интерактивными свойствами - лучший способ изучить все тонкости UIKit. Если вы поиграете с одним достаточно, вы сможете начать предсказывать поведение; Как только вы дойдете до того момента, когда сможете объяснить, что изменение данного свойства повлияет на результирующий макет, вы получите довольно приличное понимание. Итак, чтобы проиллюстрировать, как все это работает, я включу скриншоты из примера приложения, а затем предоставлю вам место, где вы можете остановиться и подумать о том, какой эффект будет иметь изменение данного свойства. Конечно, вы можете просто бегло просмотреть, но я думаю, что вам будет намного выгоднее попытаться заранее составить схему итогового макета.

Для этого мы начнем с iOS 10, которая позволяет нам игнорировать эффекты недавно представленной safeArea и дополнительных API (и соображений), которые она приносит. Мы рассмотрим их как-нибудь в ближайшем будущем.

Визуализация расширенного макета

Здесь все со значениями по умолчанию, которые имеют контроллеры представлений / представлений при создании экземпляра, и внешним видом по умолчанию, который имеет tableView при отображении внутри UINavigationController. Обратите внимание, что фон tableView зеленый, а фон ячеек красный. Мы можем использовать это, чтобы увидеть, где начинается tableView и где начинается его содержимое; когда эти два значения различаются, мы смотрим на ненулевой contentInset, который отображается в ячейке внизу.

Откуда этот contentInset? Если вы проверяете код, мы сами его не указываем. Введите automaticAdjustsScrollViewInsets .

Что такое automaticAdjustsScrollViewInsets UIViewController?

Свойство UIViewController automaticAdjustsScrollViewInsets было введено в iOS 7 (версия, которая переработала интерфейс) и по умолчанию имеет значение true. В iOS 7 панели навигации по умолчанию были полупрозрачными, и, чтобы продемонстрировать прозрачность, представления должны были начинаться под панелями навигации (вы не сможете оценить полупрозрачность, если там нет контента для просмотра!). Однако, если виды начинаются за навигационной панелью, вещи, очевидно, будут затемнены без некоторого Apple Magic ™, которое поможет разработчикам. Это волшебство появилось в форме automaticAdjustsScrollViewInsets, которая переходит в подпредставления UIViewController и запускает этот тест:

func magicView() -> UIView? {
    guard self.isKind(of: UIScrollView.self) else {
        return self.subviews.first?.shouldApplyMagic()
    }
    return self
}

и он автоматически устанавливает contentInset этого представления прокрутки, чтобы разместить панель навигации.

Что такое contentInset UIScrollView?

ContentInsets довольно просты, и я включаю этот раздел только в качестве быстрого напоминания для тех, кто не совсем понимает идею. Я считаю, что если вы думаете о UIScrollViews как о «окнах» в контент, вам будет легче понять, что они делают; сама оконная рама не меняет размер, но вы можете перемещаться, чтобы увидеть различное содержимое за рамкой окна. Вы не можете видеть больше, чем есть, поэтому, как только вы попадете в самую верхнюю / самую нижнюю часть вашего контента, вы не сможете двигаться дальше (игнорируйте отскакивающий UIKit). Используя ту же метафору, contentInset по существу добавляет «пустой» контент над предметом, на который вы смотрите, что означает, что вы можете видеть больше, чем в противном случае. Если я добавлю верхний contentInset из 100 пунктов, это означает, что я могу прокрутить до верха своего содержимого, а затем прокрутить дополнительные 100 пунктов над ним. Для базового случая UITableView это просто пустота, которая покажет, какой у вас backgroundColor, но UIScrollViews позволит вам сделать гораздо больше, чем просто это. Я не буду сейчас вдаваться в подробности; возможно, объяснение UIScrollViews - хорошая идея для другого дня.

Теперь, когда у нас есть хорошее представление о automaticAdjustsScrollViewInsets, начальное состояние нашего приложения должно иметь смысл. Наше представление начинается в верхней части UINavigationController (ну, его представление). По умолчанию Apple дает нам автоматическиAdjustsScrollViewInsets == true, поэтому, когда наше представление выложено, оно видит, что наша панель навигации имеет высоту 64 (она переходит в область строки состояния) и дает нам 64 точки пустого содержимого над tableView. . Фактически, это визуально подталкивает наш tableView вниз, что теперь позволяет нам прокручивать панель инструментов, если мы хотим, но нам не нужно иметь дело с состоянием по умолчанию, когда наш контент изначально застревает за нашей навигационной панелью.

Что такое расширенный макет и EdgeForExtendedLayout UIViewController?

Резкий ответ заключается в том, что edgeForExtendedLayout - это свойство, которое люди устанавливают в .zero (или UIRectEdgeNone в Objective-C), когда они не могут понять, почему их представления смещены неправильно, не понимая, что происходит.

Точнее говоря, «расширенный макет» - это название поведения, которое Apple представила в iOS 7 вместе с их полупрозрачными по умолчанию панелями навигации. Расширенный макет вида относится к частям вида, которые могут «растягиваться». По умолчанию edgeForExtendedLayout включает все ребра. Это должно иметь смысл, учитывая прозрачность по умолчанию в iOS 7. Apple по умолчанию делает все панели навигации полупрозрачными; он не сможет воспользоваться этим, если наши представления не «начинаются» за панелью навигации. Для tableViews это достигается путем закрепления представления в верхней части представления navigationController, а затем установки соответствующего contentInset в представлении таблицы. Время викторины!

Повторюсь, по умолчанию мы выглядим так:

Наша панель навигации полупрозрачна, наша edgeForExtendedLayout включает в себя все грани (здесь актуальна верхняя часть), и мы автоматически выполняем вставкиAdjustScrollView. В результате мы можем видеть за нашей панелью навигации, наше представление простирается до верхней части панели навигации, а для contentInset tableView устанавливается высота панели навигации.

Что будет, если мы отключим автоматическиAdjustsScrollViewInsets? Cue the Jeopardy! Музыка.

Наша панель навигации полупрозрачна, наш крайForExtendedLayout по-прежнему включает верхний край, но наш контроллер представления больше не ищет в своих подпредставлениях UIScrollView, чтобы установить для contentInset значение. В результате мы можем видеть за нашей панелью навигации, наше табличное представление начинается в верхней части контроллера навигации, но содержимое не вставлено, поэтому мы застряли в затемненном представлении.

Что, если мы повернем и не позволим нашему верхнему краю быть частью расширенного макета?

Несмотря на то, что наша панель навигации полупрозрачна, мы не позволяем нашему верхнему краю участвовать в расширенном макете. В результате наше представление теперь закреплено под панелью навигации. Вы можете видеть, что даже при прокрутке мы не видим ячейки за панелью навигации; «оконная рама», если использовать более раннюю метафору, больше не находится за панелью навигации. Наш contentInset установлен в ноль, так как нам больше не нужно приспосабливаться к запуску за панелью навигации.

Что происходит, когда мы не используем полупрозрачные панели навигации?

Я не буду утруждать себя предоставлением изображений для этого, так как у нас есть весь контекст, который нам нужен, чтобы это понять. Предполагая, что мы снова начинаем с состояния по умолчанию: automaticAdjustsScrollViewInsets не имеет никакого эффекта, поскольку наше представление больше не будет запускаться за панелью навигации. Включение нашего верхнего края в edgeForExtendedLayout тоже не даст никакого эффекта, поскольку у нас нет макета, который можно было бы расширить, если у нас есть полупрозрачная панель навигации. Независимо от того, что вы измените с помощью этих двух свойств, если наша панель навигации непрозрачна / непрозрачна, наше представление будет начинаться под панелью навигации, и наш contentInset никогда не изменится.

Что такое extendedLayoutIncludesOpaqueBars?

Итак, мы установили, что extendedLayout - это способ Apple по умолчанию перемещать наши представления за полупрозрачными панелями навигации. В предыдущем разделе мы обнаружили, что после отключения полупрозрачной панели навигации расширенный макет больше не используется для верхнего края, поскольку отсутствует прозрачность. Однако extendedLayoutIncludesOpaqureBars UIViewController изменяет эту логику, и теперь имя должно быть интуитивно понятным: если для свойства установлено значение true, расширенная логика макета вступает в игру даже, если ваша навигация планка не полупрозрачная.

Начиная с состояния по умолчанию , но с непрозрачной / непрозрачной панелью навигации, что должно произойти, если мы теперь скажем UIKit, что мы хотим, чтобы он включал непрозрачные полосы при определении того, как компоновка нашего представления относительно расширенного макет? Хит музыку еще раз ...

Это был своего рода вопрос с подвохом. Помните, что для automaticAdjustsScrollViewInsets по умолчанию установлено значение true, поэтому независимо от того, что происходит с расширенным макетом, UIKit собирается изменить наш contentInset, чтобы визуально наш контент начинался под панелью навигации. Что же произойдет, если мы его выключим? Напомним, что мы, по сути, говорим UIKit притвориться, что наша панель навигации полупрозрачна ...

Да, наше представление теперь застряло за нашей непрозрачной панелью навигации, поскольку наш contentInset больше не изменяется для нас. Что произойдет, если мы больше не будем включать верхний край в расширенный макет?

Все возвращается в нормальное русло, и наше табличное представление расположено ниже, а не позади нашей панели навигации.

Что делать, если панели навигации нет вообще?

Чтобы охватить ситуацию, когда не вообще не включает панель навигации (полупрозрачную или нет), единственные свойства, которые имеют какой-либо эффект, - это автоматическиAdjustsScrollViewInsets, который определяет, смещено ли вы ниже строки состояния или нет. и EdgeforExtendedLayout, который определяет, простирается ли ваше представление за строку состояния или нет. Если вы находитесь в ситуации, когда ваше представление не касается строки состояния, ни одно свойство не имеет никакого эффекта.

Что делать, если я не использую UITableViewController / UIScrollView?

Мы должны быть относительно уверены в том, как наши UITableViews и UIScrollViews ведут себя с расширенными макетами в iOS 10. А как насчет не-UIScrollViews? Как стандартные UIViewControllers - конечно же, стандартные UIView - ведут себя в мире расширенного макета? Для этого я удаляю конфигурацию «automaticAdjustsScrollViewInsets». Мы уже знаем, что свойство будет использовать наш метод magicView () выше, чтобы найти представление прокрутки для управления, но у нас его здесь не будет.

В любом случае, в этом новом мире без UIScrollView может произойти несколько разных вещей. Если мы прикрепим наши subviews к нашему topAnchor, мы получим следующее:

Если мы закрепим на нашем контроллере представления topLayoutGuide, мы получим следующее:

Это должно несколько прояснить, что делает topLayoutGuide (или bottomLayoutGuide, если мы использовали панель инструментов), если до этого было не до конца ясно.

Что делают свойства на этом этапе?

Отображение / скрытие панели навигации изменится только там, где находится наш topLayoutGuide; он будет закрывать только строку состояния:

Как насчет EdgeForExtendedLayout и extendedLayoutIncludesOpaqueBars, если я не использую UIScrollView? Мы больше не манипулируем contentInset, потому что мы не используем режим прокрутки, но UIKit сделает следующее лучшее: он будет управлять нашим topLayoutGuide. Что произойдет, если наша панель навигации перестанет быть полупрозрачной? Ничего не движется, но цвет фона (на самом деле это само наше представление) больше не заходит за панель навигации, а наш topLayoutGuide сбрасывается на ноль, поскольку весь вид перемещается под панель навигации. Что делать, если у нас есть полупрозрачная панель навигации, но мы не включаем верхний край в расширенный макет? Наше представление больше не простирается за панель навигации, и наше представление перемещается под панелью навигации. Что, если у нас есть полупрозрачная панель навигации, верхний край которой включен в расширенный макет, но мы включаем непрозрачные полосы в расширенный макет? В этом случае наше представление будет расширяться за нашей непрозрачной панелью навигации (поэтому мы ничего не видим за ней), но наш topLayoutGuide настроен для размещения панели навигации / строки состояния. Визуально разницы нет, но за кулисами все свойства соблюдаются должным образом.

После объяснения всего вышесказанного, элементы automaticAdjustsScrollViewInsets, extendedLayoutIncludesOpaqueBars и EdgeForExtendedLayouts UIViewController, а также то, как они взаимодействуют с полупрозрачными и непрозрачными панелями навигации, теперь должны иметь больше смысла. Мы видим, что UIScrollViews используют contentInset для перемещения представлений вниз, а не-UIScrollViews будут использовать движущиеся направляющие макета для перемещения представлений вверх / вниз соответственно. UITableViewControllers будут ограничивать свои UITableView верхней частью своего руководства по макету, а не нижней частью, чтобы мы могли прокручивать за панелью навигации, но представления без прокрутки закрепляются в соответствии с различными свойствами, которые мы обсуждали.

Пока мы должным образом ограничиваем наши представления направляющими макетами, а не только верхними привязками, наши контроллеры представлений (и связанные с ними представления) должны вести себя так, как Apple изначально планировала еще в 2013 году, когда впервые была выпущена iOS 7.

Что делать, если я использую свой собственный контроллер представления / представление, но у меня есть представление с прокруткой?

Поведение здесь странное, но стоит отметить. Как упоминалось ранее, automaticAdjustsScrollViewInsets будет применяться к любому представлению, прошедшему тест magicView () выше. Если у вас есть подпредставление, представляющее собой прокручиваемое представление, на которое влияют, вы, вероятно, захотите ограничить свое представление прокрутки до self.view.topAnchor, а не self.topLayoutGuide.bottomAnchor, чтобы имитировать поведение Apple, что явно является тем, что они хотят, чтобы разработчики использовали. Насколько я могу судить, здесь нет никаких особых правил, которые следует учитывать помимо того, что я уже описал.

А как насчет iOS 11 и новее?

В iOS 11 Apple развернула концепцию safeAreas, автоматически отказавшись от поддержкиAdjustsScrollViewInsets в процессе. UIScrollView получил UIScrollViewContentInsetAdjustmentBehavior в процессе, что имеет некоторые важные последствия для того, как все вышеперечисленное работает. Однако, на мой взгляд, важно, чтобы мы понимали, с чего началась эта логика; как только это будет прочно закреплено в нашем сознании, мы сможем рассматривать новые API-интерфейсы как просто дельту, с которой они начались, а не изучать поведение iOS 10 и iOS 11 полностью независимо. У меня скоро будет сообщение, в котором я расскажу о поведении iOS 11 и обо всем, что мы увидим выше ™. Я добавлю сюда ссылку на новое сообщение, когда оно будет опубликовано.

В итоге

  • automaticAdjustsScrollViewInsets будет применяться, если self.view является UIScrollView, или если self.view.subviews является UIScrollView, или если self.view.subviews.first.subviews является UIScrollView, или если […]
  • automaticAdjustsScrollViewInsets добавит contentInset {navbarHeight} к любому виду прокрутки, на который влияет свойство, если расширенный макет действует для верхнего края
  • Если (представление имеет расширенный макет для верхней части) и (панель навигации полупрозрачна или extensionLayoutIncludesOpaqueBars имеет значение true), рамка представления будет расширяться под панелью навигации.
  • Расширенный макет (и его различные правила / свойства) на верхнем краю будет регулировать, где находится topLayoutGuide контроллера представления.

Не стесняйтесь обращаться ко мне в Twitter @ Wailord, если я допустил ошибку выше, или я могу помочь прояснить что-либо.