Всем привет!
Меня зовут Илья, я из Tinkoff.ru. Я перевел для вас статью от Geoff Hackworth про то, как изменился стиль модальной презентации в iOS 13, на что это повлияло и как работает обратная совместимость с предыдущими версиями iOS и Xcode.
Введение
На момент написания статьи WWDC 2019 подходит к концу. Как и многие разработчики для iOS, я медленно обрабатываю всю новую информацию, которую Apple нам дала, и в ближайшие недели (и месяцы!) Постараюсь посмотреть столько видео, сколько смогу.
У меня появилось три вопроса по поводу моих собственных приложений:
- Мои текущие приложения работают без проблем на iOS 13? У Apple долгая история обратной совместимости, основанная на версии Xcode, с которой было создано приложение. История показывает, что приложения, созданные в Xcode 10 под iOS 13, будут вести себя так, как если бы они работали на iOS 12. Но это не всегда так.
- Работают ли мои приложения при сборке с Xcode 11 / iOS 13? Сборка с использованием новейших инструментов позволяет приложению работать по-новому, минуя обратную совместимость с предыдущими версиями iOS. Что-нибудь сломалось?
- Какие изменения можно / нужно сделать, чтобы мои приложения работали лучше или использовали преимущества новых функций iOS 13? Это самая большая задача, и она займет больше времени для изучения и реализации. Это исследование для отдельной статьи.
Я еще не установил iOS 13 на реальном устройстве, но могу провести тестирование пункта №1, установив приложения, созданные в Xcode-10, на симулятор iOS 13.
Я все еще работаю над пунктом №2, но на основании моего начального тестирования и чтения твитов от других разработчиков, делающих подобные открытия, я обнаружил ряд поведенческих изменений в моих приложениях при сборке с Xcode 11. У меня есть много видео для просмотра и информация для усвоения, но в этом посте я хочу сосредоточиться на заметных сразу и потенциально разрушительных изменениях в презентации UIViewController в iOS 13.
Изменение стиля модальной презентации по-умолчанию
По умолчанию модальная презентация теперь представляет собой "страницу" (ориг. Page Sheet), а не полный экран (Full Screen). Документация для modalPresentationStyle
гласит:
По умолчанию используется UIModalPresentationAutomatic для iOS, начиная с iOS 13.0, и UIModalPresentationFullScreen в предыдущих версиях.
По умолчанию UIViewController, если в качествеmodalPresentationStyle
установлен UIModalPresentationAutomatic, использует UIModalPresentationPageSheet, но системные контроллеры могут использовать другие стили показа для UIModalPresentationAutomatic.
Последствия этого изменения различаются для iPhone и iPad.
Модальная презентация на iPhone
Стили презентации Page Sheet, Form Sheet и Popover на iPhone адаптированы к полноэкранному режиму, если только метод UIAdaptivePresentationControllerDelegate
не используется для предотвращения адаптации. Например, экран настроек может быть презентован в стиле Form Sheet, чтобы он отображался в полноэкранном режиме на iPhone и в уменьшенном виде на iPad. Технически внешний вид / адаптация зависят от ширины. Презентации в стиле Page Sheet / Form Sheet / Popover на устройствах Landscape iPhone Plus и XS Max не занимают весь экран, потому что они имеют обычную ширину. Внешний вид iPad зависит от размеров slide over и режима многозадачности.
На следующих снимках экрана показана презентация Form Sheet на iPhone XS для трех случаев: сборка в Xcode 10 для iOS 12, сборка в Xcode 10 для iOS 13, сборка в Xcode 11 для iOS 13.
Обратная совместимость iOS 12 с iOS 13 для сборки Xcode 10 приводит к полноэкранному представлению. Стиль сгруппированных UITableView изменился в iOS 13, чтобы скрыть пространство над первым разделом при отсутствии заголовка. Даже сборка Xcode 10 / iOS 12 ведет себя по-разному при запуске на iOS 13, а это не то, что я ожидал.
Самым большим изменением в iOS 13, конечно же, является карточное представление экранов (ориг. card-like appearance). UIViewController был уменьшен в размерах, и его верх все еще немного виден за вновь представленным UIViewController. UIWindow за корневым UIViewController также немного видимо. Черный фон UIWindow по умолчанию выглядит великолепно, особенно на устройствах с выемкой (notch). Некоторые из моих приложений устанавливали фон UIWindow белым (по причинам, которые я уже не помню), и это выглядело довольно уродливо. Я быстро исправил это!
Поведение UIViewController при новом модальном стиле показа
Если представленный UIViewController показывает еще один UIViewController, карточки накладываются друг на друга с приятной анимацией. Обратите внимание, что виден только последний показанный UIViewController и немного предыдущего:
Другое потенциально важное различие в поведении — это то, что происходит с показывающим (ориг. presenting) UIViewController. Полноэкранное представление (ориг. full screen presentation), которое полностью покрывает UIViewController, приведет к удалению UIViewController из иерархии. Но в случае с новой карточной презентацией UIViewController должен оставаться в иерархии, потому что он все еще видим. Однако, хотя пользователь может видеть только два UIViewController одновременно, многократный показ UIViewController не удаляет нижние UIViewController из иерархии.
Изменения размеров
Новый внешний вид в стиле карточки означает, что показанный UIViewController не такой высокий на iOS 13, как на iOS 12:
Я хочу full screen!
Явный запрос на показ UIViewController в режиме Full Screen предотвратит показ экрана в стиле карточки. Однако, это может нарушить поведение приложения на iPad. Пожалуйста, не поддавайтесь искушению проверить идиому устройства и использовать другой стиль презентации для iPhone и iPad. Если последние несколько лет нас чему-то и научили, так это тому, что мы не должны делать предположений на основе типов устройств или размеров экрана. Если вы хотите, чтобы на iPad отображался Page / Form Sheet, но на iPhone был Full Screen, вы можете использовать UIAdaptivePresentationControllerDelegate
для адаптации к полноэкранному режиму в условиях компактной ширины.
Модальная презентация на iPad
Form Sheets
Экраны, показанные в стиле Form Sheet, остаются неизменными в iOS 13:
Page Sheets
Как отмечалось выше, modalPresentationStyle
по умолчанию в iOS 13 теперь Page Sheet. На iPad размер UIViewController в этом стиле изменился как в книжной, так и в альбомной ориентации:
Как и в iOS 12, ограничение по "читаемому контенту" (ориг. readable content size) меняет размер при изменении категории размера контента (ориг. content size category). Реальный размер кажется разным на iOS 12 и 13 в некоторых категориях размера контента.
Сам UIViewController, презентованный в стиле Page Sheet, также увеличивается на iOS 13 с увеличением категории размера контента. Вот так выглядит категория «Extra Extra Extra Large» (максимальный размер, доступный без включения larger accessibility sizes):
Остальные виды презентации
Документация для modalPresentationStyle
гласит:
По умолчанию UIViewController определяет UIModalPresentationAutomatic как UIModalPresentationPageSheet, но остальные системные контроллеры могут определять UIModalPresentationAutomatic по-другому.
Я не уверен на 100% во всех правилах для «остальных системных контроллеров», но я обнаружил, что показ UIViewController с разделенным экраном (split screen) без установки modalPresentationStyle
дает карточный вид во всю ширину:
Swipe to dismiss
Еще одно важное изменение, которое затрагивает как iPhone, так и iPad, заключается в том, что модально презентованные экраны не в полноэкранном режиме (кроме всплывающих окон) можно интерактивно закрыть с помощью смахивания вниз. В это время экран позади переходит обратно в полноэкранный режим:
Обратите внимание, что в этом примере я поместил экран «О программе» в UINavigationController экрана настроек. Несмотря на то, что UINavigationController не отображал свой корневой контроллер, интерактивное закрытие было возможным.
Не смахивай меня, пожалуйста!
Если вы полагаетесь на то, что пользователь нажимает кнопку «Готово» (или аналогичную кнопку) или переходит обратно к вершине стека контроллера навигации, чтобы закрыть показанный модально UIViewController, новое поведение смахивания для закрытия может нарушить работу вашего приложения, поскольку ваш обработчик закрытия экрана не будет выполнен.
Например, в моем приложении Pomodoro Timer Pommie пользователь может перейти к подэкрану на экране «Настройки» и добавить или отредактировать профиль таймера (конфигурация для периодов работы / перерыва для конкретного вида задачи):
В случае с Pommie, я думаю, что это нормально (и безопасно), если пользователь закрывает весь экран настроек с помощью смахивания. Пользователи, вероятно, будут ожидать, что они могут закрыть экран одним движением, и я хочу, чтобы мои приложения работали правильно в iOS 13. Однако, я чувствую, что на экране «Добавить / изменить профиль таймера» нельзя давать пользователю закрывать экран смахиванием, так как существует риск потери изменений. Для пользователя может быть не совсем понятно что произойдет после такого закрытия.
Одна часть исправления этой проблемы — новое свойство UIViewController: isModalInPresentation
. Из документации:
modalInPresentation устанавливается, когда вы хотите заставить экран стать модальным. Когда этот параметр включен, презентация будет предотвращать интерактивное закрытие и игнорировать события за пределами границ UIViewController, пока для этого параметра не будет установлено значение NO.
Чтобы получить поведение, аналогичное iOS 12, для моего экрана «Настройки» на iOS 13, я мог бы просто установить true для свойства isModalInPresentation
у показанного модально UINavigationController. Если пользователь попытается смахнуть вниз, чтобы закрыть его, экран немного сместится, но будет сопротивляться действиям пользователя и не будет закрыт.
Свойство можно изменить в любое время, чтобы вы могли, например, разрешить закрытие, если пользователь еще не внес изменения, которые будут потеряны, если он явно не сохранил их. Но как только изменение было внесено, вы можете установить isModalInPresentation
, чтобы предотвратить закрытие с помощью смахивания. Это заставит пользователя нажать кнопку «Отмена» или «Сохранить».
Обнаружение закрытия
Как отмечалось ранее, некоторым приложениям может потребоваться выполнить некоторый код, когда показанный модально UIViewController закрывается с помощью кнопки «Отмена», «Готово» или «Сохранить» (кроме просто его закрытия). Например, вам может потребоваться перезапустить таймер в игре или действовать на основании некоторой информации, которую пользователь изменил. Этот код не будет выполнен, если пользователь закроет экран смахиванием. Ваша кнопка не нажата, поэтому обработчик ее действия вызываться не будет. Это может нарушить поведение вашего приложения.
Самый простой способ избежать этой проблемы — предотвратить интерактивное закрытие с помощью isModalInPresentation. Пользователь должен будет нажать кнопку, чтобы закрыть контроллер представления, так же, как это было до iOS 13. Но есть другой способ…
iOS 13 добавляет некоторые новые методы UIAdaptivePresentationControllerDelegate. Они позволяют другому объекту (как правило, экрану, который показал другой экран модально) управлять тем, следует ли разрешить интерактивное закрытие (альтернатива использованию isModalInPresentation
), и получать информацию о том, когда интерактивное закрытие начинается или завершается. Эти методы хорошо документированы и четко объяснены в WWDC 2019 224: Modernizing Your UI for IOS 13, начиная с 15й минуты. Обратите внимание, что presentationControllerWillDismiss
может вызываться несколько раз, если пользователь начинает смахивать, чтобы закрыть, передумывает, а затем снова смахивает. В методе presentationControllerDidDismiss
вам необходимо выполнить дополнительный код, который вызывается при нажатии кнопки «Отмена», «Готово» или «Сохранить» (разумеется, вам не нужно закрывать показанный экран). Эти методы не будут вызываться, если UIViewController закрыт программно. Поэтому вам все равно нужно будет выполнить свой код в обработчике кнопки (или в своем собственном делегате), который вызывает закрытие даже при работе на iOS 13.
Давайте рассмотрим метод делегата presentationControllerDidAttemptToDismiss
. Он будет вызван, если пользователь попытается провести пальцем, чтобы закрыть, но isModalInPresentation
привел к блокировке закрытия. В видео с WWDC предлагается показать список действий с вопросом, хочет ли пользователь отказаться от изменений или сохранить их. Это кажется очень хорошей идеей, если показанный UIViewController имеет кнопки «Отмена» и «Сохранить / Готово»: создание новой заметки, редактирование свойств объекта и т.д.
Я думаю, что для вложенного UIViewController в навигационном стеке с кнопками «Отмена» и «Сохранить» это более сложно. Код для выполнения сохранения, вероятно, находится в UIViewController, который на один уровень выше в стеке, а не в объекте, который реализует UIAdaptivePresentationControllerDelegate
. Попытка перенаправить выбор пользователя на объект, который может выполнить сохранение, может быть не совсем уместной. В моих собственных приложениях, я думаю, я просто заблокирую закрытие экранов, которые требуют явного действия отмены / сохранения, если они не находятся на вершине стека навигации.
Ресурсы
Видео WWDC 2019 станет лучшим местом для того, чтобы узнать, что изменилось в iOS 13, какие изменения нужно внести в свои приложения, чтобы они работали корректно при сборке в Xcode 11, и какие изменения вы можете внести, чтобы улучшить их, чтобы воспользоваться новыми функциями. Вот несколько видео для начала:
- WWDC 2019 214: Implementing Dark Mode on iOS
- WWDC 2019 224: Modernizing Your UI for IOS 13
- WWDC 2019 801: What’s New in iOS and macOS Design
Заключение
До сих пор я не обнаружил никаких проблем с моими приложениями, созданными в Xcode 10 под iOS 13. Обратная совместимость здесь действительно работает. Я был немного удивлен, увидев изменение внешнего вида сгруппированной таблицы.
Сборки Xcode 11 нуждались в некоторых небольших исправлениях, чтобы иметь дело с изменениями в модальных представлениях, обсужденных в этом посте. Вероятно, будут изменения, которые я еще не обнаружил.
Тщательно протестируйте свои модальные презентации (особенно с панелями поиска)! Решите, хотите ли вы разрешить пользователю закрывать модальные экраны смахиванием, и используйте isModalInPresentation
, чтобы получить поведение, необходимое для предотвращения случайной потери данных из-за ошибочного смахивания. Для большей гибкости и контроля используйте UIAdaptivePresentationControllerDelegate
.