Не так давно в Кошельке появился платежный фрагмент для оплаты в магазинах. При разработке его дизайна мы, конечно, ориентировались на Apple Pay. Он отображается в Bottom Sheet и имеет навигацию внутри себя. Если нажать на выбор карты, Bottom Sheet отобразит viewcontroller со списком карт, у которого еще и другая высота контента. Нам потребовалось повторить такое поведение и оказалось, что сделать это не так-то просто. Контент внутри UINavigationController не привязывается автолейаутом к верхней и нижней границам, а значит, высота Bottom Sheet’а не может быть рассчитана автоматически. Было два варианта: рассчитывать высоту вручную или написать свой NavigationController. Мы прикинули, что использовать Autolayout для нас важнее, чем использовать нативный UINavigationController, и выбрали второй вариант. Это вторая статья из цикла про bottom sheet. Весь материал можно найти вот здесь: Bottom sheet: Custom transitioning Bottom sheet: Navigation Bottom sheet: Scrolling and interactions Если вы ещё не пробовали отобразить Bottom Sheet в iOS до 15 версии, рекомендую сначала прочитать первую статью из цикла, там подробно описан именно этот вопрос. Скачайте стартовый проект. Можно запустить и ознакомиться с его функциями. Первая кнопка уже функциональная и умеет отображать различный контент как bottom sheet. Его высота вычисляется на основе размера контента при помощи autolayout. Наш навигационный контроллер(BSNavigationController), как и нативный, займётся хранением и отображением списка child контроллеров. Для удобства использования другими разработчиками, полностью повторим интерфейс UINavigationController. Реализуем анимации для транзишенов, схожие с нативными. Они будут учитывать отображения контента только на части экрана, а также его увеличение или уменьшение. Первым этапом опишем хранение child контроллеров в нашем navigation controller. Найдите в проекте класс BSNavigationController и добавьте в него код: Кастомный transitioning delegate мы реализовали ранее, чтобы отображать navigation controller в виде bottom sheet. Как это работает, описано в первой статье. Метод выглядит на этом этапе бесполезным, но чуть дальше мы будем в нём настраивать стартовое состояние контроллера. Пока просто сохраним rootViewController в массиве. На случай, если navigation controller был создан без rootController, то, независимо от animated, мы вызовем метод setRootViewController(:) Место для старта анимации транзишена между верхними контроллерами текущего стека и нового. Добавим rootViewController для отображения как child и закрепим к краям контейнера: С добавлением и хранением разобрались, перейдём к анимациям транзишенов между контроллерами. Как выглядит анимация перехода на новый контроллер? Новый контроллер появляется на верхнем слое из-за правого края экрана и перекрывает контент нижнего. В это время нижний контроллер смещается на треть своей ширины за правый край экрана. Анимация будет построена на изменениях констрейнтов и их попеременном включении и отключении. Реализуем эту анимацию, добавив следующие строки в BSNavigationController . Перед самой анимацией добавим расчёт максимальной высоты контента: Максимальной будем считать высоту экрана минус отступ safeArea сверху. Проверим, что контейнер, в котором располагается BSNavigationController’s view, существует. Старт обновления constraints производится на его уровне, чтобы обновить всю иерархию зависимостей. Если делать это выше, то обновление высоты верхнего контейнера произойдёт без анимации. Добавляем новый контроллер как child, а для старого стартуем процесс открепления. Удалим внешние констрейнты from контроллера, чтобы не завязываться в анимации на предыдущее состояние. Подготовим constraints для стартового состояния контроллеров. Активируем все констрейнты контроллеров кроме top для to контроллера. Здесь нам понадобится contentMaxHeight. Таким образом, если контент имеет высоту больше, чем может поместить на экране, то мы это учтём ещё до того, как в силу вступят ограничения контейнера. Так анимация перехода будет происходить между финальными состояниями контента внутри контроллеров. Сохраняем в переменные те констрейнты, которые будем менять в дальнейшем. Вызовем обновление layout, чтобы до старта анимации контроллеры обновились до заданного констрейнтами состояния. Изменение высоты контейнера будем анимировать от верхней точки старого контроллера к верхней точке нового. Отключаем top контстрейнт для from и включаем top для to. Обновляем константы leading констрейнтов обоих контроллеров для анимации смещения по горизонтали. Добавим возможность отключить анимации смены контроллеров. Для этого обернём вызов анимации в блок CATransaction. С помощью метода setDisableActions(:) можно пропускать анимацию обновления layout. Стартуем обновления layout. Заканчиваем добавление нового контроллера как child и открепление старого. Как выглядит анимация перехода на предыдущий контроллер? Верхний контроллер смещается полностью за правый край экрана. Нижний виден на две трети своей ширины на начало анимации и смещается на треть из-за левого края, заполняя всё пространство экрана по ширине. Реализация этого поведения находится в следующих строках: Принцип анимации pop аналогичен push. Разница заключается в направлении смещения по горизонтали и в том, что верхний экран теперь from, а нижний to. Добавим вызовы анимаций в три метода: Ещё одной неотъемлемой составляющей navigation controller является верхняя навигационная панель (navigation bar). Повторим её для нашего кастомного решения и будем использовать тот же класс, что и для нативного — UINavigationBar. Navigation bar хранит в себе navigation items подобно тому, как это делает navigation controller с children controllers. У каждого view controller есть свой navigation item, который navigation controller по аналогии с методом setViewControllers(_:animated:) добавляет в navigation bar. Так как мы используем UINavigationBar, а не пишем своё решение, то все анимации смены состояний мы получим из коробки. Добавим navigation bar в navigation controller. Перейдём в BSNavigationView.swift и добавим в него код: Кастомизируем немного отображение navigation bar, чтобы сохранить общий стиль приложения =) Настроим констрейнты так, что navigation bar располагается сверху и на всю ширину контейнера. Высоту navigation bar рассчитывает самостоятельно. Navigation bar всегда должен быть расположен поверх остального контента. Для этого переопределяем методы addSubview(:) и insertSubview(:at:) и поднимаем  bringSubviewToFront(navigationBar) Дадим возможность скрывать navigation bar при необходимости. Теперь настроим жизненный цикл navigation bar. Вернёмся в BSNavigationController и добавим логики: Дадим возможность узнать, включено ли отображение navigation bar. Реализуем доступ к самому navigation bar на чтение. Может пригодиться при кастомизации из child контроллеров. Переопределим view для bottom sheet и обновим верхний отступ. В зависимости от того, видим или нет navigation bar, будет меняться дополнительный отступ сверху. Привязав контент к safeArea, мы всегда будем иметь актуальный отступ. Актуализируем высоту отступа при изменении контента. Меняем состояние видимости navigation bar. Высота navigation bar рассчитывается UIKit автоматически в зависимости от типа презентации. Если это fullscreen, то 44, а если модально, то 56 (начиная с iOS 13). Таким образом можем положиться на высоту navigation bar почти всегда, кроме начала появления контроллера, когда она ещё 0. Для корректной отрисовки контента во время транзишена минимальную высоту сделаем 44. Добавим настройку navigation items для navigation bar в методы навигации: Для тестирования добавим следующий код в метод presentVCInBottomSheet() RootViewController: Для тестирования результата создадим BSNavigationController, в который поместим FirstViewController. У последнего есть переменная nextTapHandler, ожидающая замыкание, которое вызовется по нажатию на «Далее» в navigation bar. Откроем ViewController и добавим следующие строки в метод presentVCInBottomSheet(): Как видно из примера, по итогу работа с BSNavigationController ничем не отличается от UINavigationController. Запускаем, проверяем, радуемся! Финальный проект. Мы воссоздали полноценную навигацию в рамках bottom sheet отображения, основанного на autolayout, а не на неудобном ручном расчёте высоты. Повторили анимации навигационных переходов. Добавили navigation bar как нативный способ управления навигацией. У этого решения есть как плюсы, так и минусы. Основным минусом является то, что решение не нативное, а значит есть шанс, что на его поддержку в каждой версии SDK необходимо будет выделять дополнительные ресурсы разработки. С другой стороны, если не использовать autolayout, это тоже может привести к тому, что в новой версии iOS что-то отвалится. Мне неоднократно приходилось чинить кастомные хэдэры после обновления SDK, высота которых рассчитывалась вручную. Я же постарался сделать свой NavigationController, по максимуму используя нативные инструменты: Autolayout и UINavigationBar. Так я попробовал минимизировать риски того, что в модуль придется лезть, чтобы что-то починить, а не добавить. В конечном итоге плюсы такой реализации перевесили минусы, и мы построили свой платежный фрагмент в проде на таком модуле.   iOS разработчик в приложении «Кошелёк»