Корзина в Додо Пицце на iOS 14: баг длиной в полгода

c0f6f76f7d21cd978032d169aac23a7d.jpg

Иногда даже самые заядлые геймеры подолгу не могут пройти уровни в видеоиграх. Помните дремлющего дракона Сина из Dark Souls, уровень Chamber 15 из Portal или миссию Demolition Man — она же легендарная «миссия с вертолётиком» — из GTA Vice City? Вы пытаетесь и пытаетесь их пройти, но у вас не получается. Вы не понимаете, что упускаете и упускаете ли вообще.

Тогда вы идёте проходить другие доступные вам миссии, исследовать открытый мир, выполнять сайд-квесты. А потом, может быть даже не через один год, возвращаетесь к игре и проходите заветный уровень с первой-второй попытки.

Такое бывает и когда вы боретесь с багами. Привет! Меня зовут Лёша Берёзка. Я iOS техлид в Додо Пицце. Сегодня я расскажу вам историю о том, как внимательность и упорство творят чудеса, и помогают решать задачи, на которые другие бы просто забили.

Раньше у нас в приложении был таббар. В нём было несколько элементов, в том числе и кнопка перехода в корзину:

Кнопка Корзина справа снизу

Кнопка Корзина справа снизу

А потом случился редизайн: мы убрали таббар, а для корзины сделали отдельную кнопку в меню. Если в корзине пусто, то кнопка не видна, но как только вы что-то добавляете — кнопка тут же анимированно появляется:

Когда мы заменили таббар на плавающую кнопку, к нам в поддержку стали поступать жалобы на неработающую корзину. Выяснилось, что клиенты просто не видели кнопку корзины. Жалоб было не очень много и вроде бы некритично, но регулярный фон был.

Со временем мы заметили, что все обратившиеся клиенты пользовались устройствами на iOS 14. У наших QA аппарат с такой версией ОС был. Они попробовали воспроизвести баг, но ни разу не смогли этого сделать. В итоге поступили просто и радикально: включили постоянное отображение кнопки корзины на всех устройствах с iOS 14, независимо от наполненности корзины.

Прошло 3–4 месяца и я случайно узнал, что тикет с проблемой всё ещё открыт и в него всё ещё добавляются новые кейсы. Некоторые клиенты присылали видео с багом: кнопка корзины видна, но стоимость продуктов на ней не отображается, и по нажатию на неё ничего не происходит:

Я внимательно изучил кейсы, пообщался с QA и выяснил, что проблема стреляет только на iOS 14.0–14.4.2. У наших же QA аппарат был на iOS 14.6. Это объяснило, почему мы не могли воспроизвести баг.

Установить симулятор iOS 14.0 мы не смогли — у всех уже самые последние версии макосей и икскодов, с них её скачать нельзя. Если ставить подходящие старые икскоды, то они не запускаются — ОС сильно свежая (не шучу):

3089519f495385ff3a30c6827d4a4427.png

Если вы знаете, как установить симулятор iOS 14.0 на macOS 14, напишите в коментах.

Ситуация: симулятора нет, подходящего айфона у меня и у моих знакомых нет, а неработающая корзина у пользователей есть. Значит, чтобы починить баг, надо купить подходящий айфон.

В итоге я пошёл на Авито. Там я неделю искал нужный мне айфон, убеждая продавцов в том, что аппарат мне нужен именно на iOS 14, а я ничего не перепутал.

Один из продавцов обновил iOS с 14 до 15 и убеждал меня, что так даже лучше

Один из продавцов обновил iOS с 14 до 15 и убеждал меня, что так даже лучше

В конце концов я вышел на объявление от забытого привокзального сервисного центра формата «киоск в углу ТЦ». Там я и купил iPhone 6S на iOS 14.4.2 за 5500 ₽.

Пришёл домой, подключил айфон к маку и позвал на совместный дебаг всю iOS-команду Додо Пиццы — ребятам тоже было интересно, в чём там дело. Спустя полчаса выяснили причину:

  1. Кнопку корзины мы сверстали на SwiftUI.

  2. Экран меню у нас ещё на UIKit.

  3. Кнопку корзины в меню мы встраиваем через UIHostingController.

  4. На этом UIHostingController не вызывается метод viewDidLoad(), в котором мы как раз подписываем кнопку на все нужные изменения и настраиваем на ней экшены. Все остальные методы ЖЦ вызываются.

Проверил другие подобные места с UIHostingController — проблема есть везде. Осталось понять, точно ли проблема только на iOS 14.0–14.4.2, чтобы в фиксе ифчик по версии ОС правильный написать.

Открыл аналитику, собрал цифры, на каких именно версиях iOS 14 нет событий открытия корзины, а они везде есть. Даже события оформления заказов прилетают. Пу-пу-пум.

Я решил посмотреть детальную аналитику по всем сессиям. Чекал только те, где события есть, но быть их там не должно.

Оказалось, что клиенты нашли воркэраунды. Всего их было два:

Воркэраунд 1:

  1. Положить товары в корзину.

  2. Открыть историю заказов.

  3. Повторить какой-то случайный заказ. Так корзина откроется автоматически.

  4. Удалить из корзины товары из повторенного заказа, оставив только те продукты, которые хотели заказать изначально.

  5. Оформить заказ.

Воркэраунд 2:

  1. Положить товары в корзину.

  2. Открыть коинстор. В нём можно купить продукты за додокоины.

  3. Купить продукт за додокоины — всплывёт другая кнопка перехода в корзину.

  4. Нажать на кнопку и открыть корзину.

  5. Удалить из корзины товары, купленные в коинсторе, и оставить только те продукты, которые хотели заказать изначально.

  6. Оформить заказ.

Отсекаю все такие кейсы и подтверждаю, что проблема только на iOS 14.0–14.4.2. Ура!

Пора чинить. Варианты такие:

  1. Вручную дёргать viewDidLoad() сразу после создания инстанса вьюхи.

  2. Перенести настройку кнопки во viewWillAppear(), который вызывается корректно. Ну и спрятать за флажок, чтобы не больше раза отрабатывало.

Мы решили пойти первым путём, сократив время рефакторинга и уменьшив вероятность что-то сломать. Для этого везде заюзали UIHostingController не напрямую, а через его сабкласс. В нём из конструктора вручную дёрнули viewDidLoad(). Сам фикс уместился в пару строк — кайф, люблю такое:

open class DHostingController: UIHostingController where Content: View {
     public override init(rootView: Content) {
         super.init(rootView: rootView)

         // Fix for iOS 14.0-14.4.2
         //
         // iOS doesn't call viewDidLoad despite view being loaded
         // Other lifecycle methods, like viewWillAppear/viewWillDisappear are called as expected
         if #unavailable(iOS 14.5) {
             viewDidLoad()
         }
     }

     var viewDidLoadAlready = false
     open override func viewDidLoad() {
         guard !viewDidLoadAlready else { return }
         defer { viewDidLoadAlready = true }

         super.viewDidLoad()
     }
  }

Когда выйдет версия с фиксом, мы отключим поддержку iOS 14. А пока расскажите нам вы, были ли у вас похожие кейсы? Покупали ли вы старые устройства, чтобы посмотреть, как появляется баг и как можно его поправить?

Предлагаю вам ещё поучаствовать в нашей голосовалке. Интересно, какую минимальную версию iOS поддерживаете вы!

© Habrahabr.ru