Универсальные попапы или UIKit против

27 декабря состоялся VK Tech Talks | iOS, на котором Антон Спивак рассказал небольшую историю про проектирование универсального интерфейсного компонента в приложении VK и исследование внутренних особенностей UIKit.

В самом начале работы Антона к нему подошел дизайнер Миша и сказал:

Универсальные попапы или UIKit против, image #1

Для начала стоит описать поведение всплывающего экрана, что мы вообще желаем от него получить:

Универсальные попапы или UIKit против, image #2

Чем необходимо пользоваться, чтобы реализовать то поведение, которое мы хотим видеть:

Универсальные попапы или UIKit против, image #3

Основная идея в следующем: красным обведен большой и главный PopupController, который имеет в себе, например, маленький popup, и мы должны сделать так, чтобы мы могли запихнуть туда любой контент. Что мы будем для этого делать?

  1. Нам нужно ловить жест UIPanGestureRecognizer, чтобы перемешать экран.
  2. Возьмём child view controller и просто вставим его вовнутрь.
  3. Будем менять наш child controller с помощью bounds.
  4. И анимировать с помощью UIKit Dynamic.
Универсальные попапы или UIKit против, image #4

Первая проблема, с которой мы сталкиваемся — это проброс нажатий и Accessibility.

Универсальные попапы или UIKit против, image #5

Эта проблема решается, но тут приходит Миша и говорит:

Универсальные попапы или UIKit против, image #6

Как придётся это всё встраивать и какие ограничения накладываются на эти вещи?

Универсальные попапы или UIKit против, image #7

Первая задача — поиск UiScrollView: есть приватный метод, UIKit позволяет сделать это, на самом деле. Есть такая штука как contentScrollView и они сами этим пользуются, например, для scroll to top. Они пользуются этим методом и пытаются определить у topmost controller'а какой сейчас находится контент: ScrollView или, например, это сделано для того, чтобы в iOS 11 можно было сделать большие заголовки. Придётся бегать по иерархии, искать это всё, запоминать и кэшировать.

Универсальные попапы или UIKit против, image #8

Допустим мы это сделали. Теперь нужно реагировать на изменения контента в UIScrollView?

Универсальные попапы или UIKit против, image #9

Что делать для перехватывания жеста и как вообще можно было бы это сделать? У UIScrollViewPanGestureRecognizer, который является наследником обычного PanGestureRecognizer, который лежит и управляет действиями на ScrollView, есть такая штука, которая позволяет переместить жест на родительское ScrollView. Чтобы это работало, необходимо сделать так, чтобы у нас родительское ScrollView поддерживало paging, но это опять же приватный API и мы не можем это использовать.

Универсальные попапы или UIKit против, image #10

Миша, привет! Миша пришёл и сказал:

Универсальные попапы или UIKit против, image #11

Добавим это просто в PopupController. Вроде ничего сложного. Достаточно определить скорость окончания жеста и просто довести до нужной позиции.

Универсальные попапы или UIKit против, image #12

Сделали, справились. Миша доволен, почти. Но появилась история, что один попап может появиться чуть выше другого попапа и это, само собой, выглядит не очень.

Миша предложил скрывать нижние и оставить только верхний.

Универсальные попапы или UIKit против, image #13

Достаточно просто найти parentPopup, и просто будем вместе с появлением анимации скрывать предыдущий попап.

Универсальные попапы или UIKit против, image #14

Но это не совсем так. Он круто работает с UINavigationController, со стеком, когда есть определенный контролер из которого идет транзишн или в который идёт транзишн.

Универсальные попапы или UIKit против, image #15

Миша доволен. Миша обрадовался. Но не обрадовались мы.

Универсальные попапы или UIKit против, image #16

Началась история, когда приходили ребята из раздела тестирования и приносили баги. У нас был скриншот одного единственного метода, который отвечал за жест, который мы сами обрабатывали для попапа.

Универсальные попапы или UIKit против, image #17

Настало время рефакторинга:

  1. Первоначально хотелось бы избавиться от поиска UIScrollView. Достаточно дорогая операция, учитывая, что часто нужно как-то избавляться от предыдущего найденного ScrollView.
  2. Также хочется инкапсулировать логику полностью отвечающую, например, за статичный попап, и как-то отделить его от попапа, который может скроллиться у себя внутри.
Универсальные попапы или UIKit против, image #18

ScrollView. Как мы будем избавляться от этих поисков и инвалидаций кешей, и прочих вещей? Для начала придумаем протокол, который будет отвечать за изменение поведения ScrollView. Нам нужен такой момент, когда ScrollView может спросить у своего слушателя о том, может ли сейчас он поменять контент. Также понадобятся небольшие обзерверы — протоколы, которые позволяют просто слушать изменение контента. И сделаем условного регулятора, который позволит для определенного ScrollView отдать либо обзервера, либо его регулятора.

Универсальные попапы или UIKit против, image #19

В нашем случае сам PopupController будет являться Listener'ом, который отдаст behaviour, чтобы тот определял поведение ScrollView и самого попапа.

Универсальные попапы или UIKit против, image #20

Вот такая история, в которой удалось на базе этих попапов сделать обширное количество экранов.

К чему же мы пришли за это всё время?

Универсальные попапы или UIKit против, image #21

А на этом доклад Антона заканчивается.

Посмотреть полную версию доклада можно в трансляции мероприятия:

Универсальные попапы или UIKit против, image #22
The Brown Room
независимое интернет-издание о социальных сетях и современных технологиях

Автор: Сергей Котов
Корректор: Арсений Метелев

425 views·2 shares