Вообще дебаг инструмент в нашем приложении был давно и выглядел так. Но: там был очень маленький набор функциональности; все было полностью закрыт #IF DEBUG’ом; фичи были разбросаны по разным проектам; вдобавок открывалось это всё только с главного экрана с шестерёнки. Зачем что-то менять? Ведь все «идеально». Шутка, конечно, надо всё менять. Ведь у нас появилась новая технология для SSL-Pinning. Её суть в том, что теперь публичные ключи для сертификатов стали приходить с сервера и надо было это как-то тестировать. А все прокси, по типу Сharlie или Proxyman не могли работать с этой технологией и просто отваливались — внутри защищенного соединения ключи не посмотреть. А где есть публичные ключи? Правильно — на клиенте. Значит нам достаточно куда-то сохранить ключи и вывести эти ключи на экран тестировщику через несложный интерфейс? Логично. Решение мы знаем, но «куда его»? Полезли мы в наше старое дебаг меню, и поняли, что туда совсем не удобно добавлять что-то новое, потому что это два разных репозитория. К тому же ещё всё дебагом закрыто. Надо как-то обновляться. Всплывает вопрос — «Стоит ли делать свой инструмент или мы можем воспользоваться существующими?» Готовых решений достаточно много, их можно найти по хэштегу #debugging-tool на GitHub. Здесь представлены некоторые из них. Но ни одно не подходило. Каждое было написано на своей архитектуре. Код незнакомый, архитектура чужая — придётся тратить много времени на «разбирательства», чтобы сделать новую функциональность. Чтобы дорабатывать решения необходимо их форкать. Соответственно, мы теряем возможность удобного обновления этой библиотеки от разработчиков. Пришлось бы постоянно мержить изменения стороннего разработчика с нашими.  Некоторые из них были написаны на Objective-C, а наш проект на 99% на Swift. Тащить себе в приложении кучу кода Objective-C что-то не хочется. Остаётся только сделать свое решение. Но как оно должно работать? Мы должны иметь возможность добавлять новый функционал, в принципе. Решение должно быть написано на нашей архитектуре. Мы не должны бояться за его расширение — что сборки в прод перестанут собираться, пока всё дебагом не закроешь.  Оно должно быть красивым удобно, иначе им не будут пользоваться. Также мы придумали допусловия, что каждый локальный модуль мог бы добавить свою функциональность при необходимости. Перейдём сразу к результату без промежуточных этапов.  Дебаг меню можно открывать из любой части приложения.  Это отдельный модуль в нашем приложении, который не линкуется в релейные сборки.  За счет того, что мы сделали отдельный модуль, мы максимально уменьшили количество #IF DEBUG. Мы использовали нашу дизайн-систему, чтобы оно было красивым и удобным. Для решения проблемы дополнительной функциональности для каждого модуля сделали протокол, под которым можно подписать любой контроллер, и он может добавить какую-то свою кастомную функциональность. Давайте скорее посмотрим как оно работает! Дебаг меню начинается с его открытия и есть несколько возможностей это сделать.  Первое — это двойное зажатие. Можно прямо на девайсе, можно эмулировать на симуляторе. Второе — потрясти телефон. Третье — интересное открытие в симуляторе — сочетанием клавиш Shift + <—.  У UIResponder есть свойство keyCommands, которое можно переопределить, назначить сочетание клавиш и экшен.  Вот мы на этот экшен и открываем дебаг меню.  Открыли — пользуемся.  Первая функциональность — создание тестового пользователя. Это один из рутинных процессов, который мы научились автоматизировать с помощью дебаг меню.  Раньше создание тестового пользователя происходило так: У тестировщика есть коллекция в Postman, он делает запрос, получает авторизационные данные и авторизовывается. Разработчик же пришел сюда «кнопки красить», а не запросы в Postman делать, верно? Поэтому он пишет тестеру, а тот — скидывает авторизационные данные. И всё это долго — пока один другому напишет, пока один другому ответит. Мы решили этот процесс упростить и теперь все эти запросы можно делать с клиента. Пожалуйста — вот вам шаблоны, вот вам простой UI, и теперь, чтобы авторизоваться в приложении, достаточно просто открыть дебаг меню, нажать пару кнопок и всё. После того, как прошли в авторизационную зону, в дебаг меню увидим наши авторизационные данные, которыми можем поделиться с коллегой (если это необходимо). Следующая функциональность — изменение значений feature toggles.  Все значения feature toggles у нас подгружаются в момент авторизации. Раньше, чтобы посмотреть, как будет вести себя наше приложение с одним или другим включенным feature toggle, тестировщику необходимо было сначала поменять значение где-то на сервере, потом заново пройти авторизацию, чтобы получить обновлённые значения feature флагов, и потом уже смотреть изменения. Теперь авторизация не нужна. Есть несложный UI, где можно посмотреть все наши feature toggles. Он открывается с любого экрана. Теперь, чтобы тестировщику посмотреть, что находится под другим значением feature toggle, что произойдёт, если поменять значение, достаточно просто открыть дебаг меню и перезайти на экран. Всё, он сразу увидит, как экран поменялся. Всё это реализовано за счёт того, что у нас есть локальное хранилище, куда сохраняется feature toggles и хранилище позволяет менять их значения. Ничего сложного. Как я уже говорил, Apple не даёт API к своему сетевому порту, а логи надо как-то смотреть. На самом деле, это немножко кликбейтный пункт, потому что можно смотреть логи и на реальным девайсе, как это делает, например, приложение Proxyman для iOS. Необходимо поставить VPN сертификат себе на устройство и все запросы начинают проходить через этот VPN, а Proxyman их отображает. Но так мы не сможем видеть запросы с прода, опять же из-за SSL Pinning.  В то же время наше решение позволяет смотреть все продовые запросы даже с прода, игнорируя SSL Pinning, если в приложении, собранном для теста, зайти в прод. Если вы с такой дебажной сборкой залогинетесь в прод, то увидите все запросы, что позволяет быстро отдебажить приложение. В каждый запрос можно провалиться и посмотреть детальную информацию: здесь видно все хедеры, урлы, боди.  У нас реализован аналог Cmd+F, можно искать детальную информацию. Также можно поделиться этим запросом, как сформированным curl'ом, так и всем логом. Тестировщики часто этим пользуются, когда работают на реальном девайсе — просто берут и в Telegram скидывают разработчику лог, а он уже разбирается, что не так. Под капотом для сетевого слоя мы используем Alamofire. Сохранение результатов всех сетевых запросов у нас реализовано с помощью протокола EventMonitor, который описывает сущность, способную обрабатывать внутренние события Alamofire. Ссылка на документацию — AdvancedUsage.md. Для обработчика выполненных запросов мы сделали класс, реализующий только один метод этого протокола. В теле метода происходит обращение к хранилищу для сохранения информации о выполненном запросе. Экземпляр созданного класс, реализующего протокол EventMonitor, принимает в себя объект Session при инициализации. Теперь для просмотра сетевых логов достаточно сделать несложный UI, в котором будет отображаться информация, полученная из хранилища сетевых логов.  Также здесь мы реализовали поиск детальной информации по сетевому логу. Скроллинг к какой-либо части текста и её выделение позволяет сделать UITextView. Для выделения цветом найденных частей текста мы сначала проходимся регулярным выражением по всему тексту для поиска NSRange совпадающих частей.  Теперь каждый найденный NSRange надо выделить цветом. Это умеет делать NSMutableAttributedString путём добавления нового атрибута к строке. А для скроллинга к найденному тексту достаточно вызвать метод scrollToVisible(_ range: NSRange) у UITextView. Собственно, с этого и началась новая эра дебаг меню.  Помимо того, что здесь можно посмотреть публичные ключи для всех запиненных доменов, можно отключить и выключить SSL-пиннинг. Это полезная фича, когда вы хотите запроксировать запросы. Дальше целый раздел — логирование. Это наши внутренние логи, которые логируют всякие события в коде, будь то вызов метода, или какие-то ошибки.  Инструмент АБ логов позволяет нам посмотреть, какие события в коде происходят, даже не подключая Xcode. Прям на реальном устройстве, можно посмотреть какой класс был вызван, на какой строчке произошло событие. Это позволяет дебажить приложение вообще не подключая Xcode. Здесь мы реализовали все виды логов за счёт глобальных функций вот такого вида. У нас реализована такая для каждого из типа внутреннего лога: Verbose, Debug, Info и т.д. Вызвать эти методы можно абсолютно откуда угодно примерно вот так. Данный вызов сохранит в хранилище логов результат вызова функции, что помогает в уже собранном приложении понять, какой метод был вызван. LoggerClientDefault реализует взаимодействие с неким провайдером, который кроме того, что сохраняет логи в хранилище, также выводит их в консоль. Здесь представлен небольшой список пушей, которые получало приложение.  Это все сделано за счет класса UNUserNotificationCenter, у его инстанса есть метод getDeliveredNontification. Он вернёт информацию о полученных пушах.  Раньше было необходимо ставить breakpoint’ы, смотреть, что в пуш приходит, а здесь мы сразу видим, как наше устройство обрабатывает пуш-уведомление. Следующая очень важная функциональность.  Рассказываю как это было раньше (как это было и у меня): тестировщик проходит весь флоу, собирает все события аналитики и только на следующий день смотрит результат уже в сервисе аналитики. Да, некоторые сервисы аналитики могут до суток обновлять свои данные, поэтому мы решили этот вопрос просто и элегантно. У нас есть общая точка входа для всех сервисов аналитики (так называемый Facade). Тут у нас есть возможность куда-то сохранить все события, которые отправляются. Все их дименшены, сервисы, в которые они уходят, чтобы потом вывести это на экран. Теперь тестировщику, чтобы посмотреть какую-то информацию в аналитике и протестировать, достаточно пройти флоу и открыть дебаг меню. Всё, он сразу всё видит, не надо ждать сутки. Как мы решали проблему с сохранением аналитики ? Для начала понадобится типичный сторадж, в котором будут храниться отправленные в сервисы аналитики события. В нашем случае мы обогащаем сохраняемое событие уникальным идентификатором, чтобы можно было удобнее искать лог. Когда у нас есть сторадж, в него надо сохранить само событие аналитики. Как я уже говорил ранее, у нас есть единая точка входа для всех событий аналитики. Собственно теперь достаточно заинжектить сущность нашего storage в сущность, которая отвечает за отправку всех событий аналитики и начать сохранять события.  Дальше новый подраздел — фичи для разработчиков. Первая функциональность в разделе для разработчиков. Можно посмотреть значения, которые хранятся в UserDefaults и Keychain, соответственно.  Это очень полезный функционал. Если раньше, чтобы посмотреть, что вообще находится в Keychain, опять же, надо ставить breakpoint, пытаться посмотреть, всё ли действительно записалось, то теперь мы можем с реального девайса посмотреть, что хранится в Keychain или UserDefaults. Также мы выводим детальную информацию по UserDefaults и Keychain. Для UserDefaults нам важны ключ, значение и запись у домена. А для Keychain нас интересуют атрибуты, и мы можем вывести их все и посмотреть информацию. Каким образом можно вытащить все значения UserDefaults и Keychain, которые используется в вашем проекте? С UserDefaults мы решали задачу так. Все значения в них сохраняются либо в синглтон UserDefaults.standart, либо в объект, который заинициализирован с помощью suitName. Все suitName, которые используются в проекте, можно найти поиском. Имея все возможные suitName можно собрать массив объектов UserDefaults, которые используются в вашем проекте. Теперь, когда вы имеете массив необходимых объектов, я напомню, что в UserDefaults все данные хранятся парами ключ-значение. Соотвественно, эти пары удобно было бы преобразовать в словарь, что можно сделать благодаря методу dictionaryRepresentation у объекта UserDefaults. Всё вместе это будет выглядеть примерно вот так. Теперь эти словари можно обработать каким-либо образом и отобразить на экране. В Keychain все значения разбиты по классам. Мы взяли те классы, которые используются в нашем приложении, чтобы вытаскивать только интересующие нас значения.  Теперь необходимо сделать запрос в Keychain. Его надо обогатить его атрибутами, чтобы указать те данные, которые мы хотим из каждого класса получить. Мы добавили их несколько. kSecReturnAttributes указывает необходимость вернуть словарь атрибутов для каждого найденного Keychain Item.  kSecMatchLimit определяет максимальное количество результатов, которые могут вернуться в ответ на запрос. Мы указали, что нам надо вернуть все результаты, которые подходят под запрос, то есть значение kSecMatchLimitAll.  kSecReturnData указывает надо ли возвращать объект CFData, содержащего значение,  для каждого найденного Keychain Item.  Для запросов в Keychain существует глобальная функция. В итоге для получения значений Keychain получился примерно вот такой метод. Здесь просто список всех подключенных к проекту подов.  Все мы знаем проблему, когда поды после pod install кэшируются, и мы не уверены на какой версии библиотеки мы находимся, и есть ли там наши доработки. Пожалуйста, открываем дебаг меню и смотрим, что в нём находится. Всё работает за счет того, что мы просто выводим на экран то, что парсим Podfile.lock — там есть все библиотеки, все версии. Реализовано это за счёт структуры Podfile.lock. Этот файл генерируемый, поэтому он всегда будет выглядеть одним образом.   Зная структуру этого файла можно парсить его и вытаскивать все необходимые нам данные. Здесь уже только вам решать, насколько подробно вы хотите выводить полученную информацию. Это такой индикатор, который появляется в статусбаре в виде UIWindow. Он красится в цвета, в зависимости от того, хороший FPS или нет.  Счётчик появился из-за того, что мы хотели посмотреть, насколько вообще наше приложение производительное. Насколько оно подходит для новых девайсов, у которых большая частота обновления экрана. Мы реализовали счётчик FPS с помощью класса CADisplayLink. Ему прямо при инициализации назначается селектор, который будет вызываться каждый раз, когда система обновляет экран. Мы использовали его вот так. А сам селектор считает кадры в секунду вот так. Полученное количество кадров в секунду теперь надо только отобразить на экране. Мы делаем это с помощью UIWindow, находящегося в Status Bar. А какие идеи будут для этого у вас? Последняя функциональность для разработчика.  Краш очень прост — под капотом мы просто вызываем fatalError :) С помощью этого мы можем посмотреть, работают ли у нас вообще логи крашей, уходят ли краши в сервис аналитики. Удобно. С функциональностью для разработчиков всё. Зачем это вообще сделано? Дело в том, что у нас единая тестовая среда и разработчики, например, даже в одной команде, могут одновременно дорабатывать какой-то эндпойнт. Когда они что-то разработали, то, чтобы проверить, как фронт на это отреагирует, надо раскатить. Представим ситуацию, когда один бэкэнд-разработчик что-то сделал, раскатил, и другой тоже что-то сделал и взял и перекатил изменения предыдущего. Проблема. Решили так, что, совместно с нашими бэкенд-разработчиками, сделали новый хидер в запросах — Х-FEATURE-NAME. Это аналог ветки из гита — разработчик просто раскатывает «ветку», и дальше фронт будет отдавать этот хидер, который тестировщик может назначить в дебаг меню. Мидл же будет знать, куда ему надо сходить, чтобы получить необходимую версию эндпойнта. Наверно, как и у всех, нашим тестировщикам для проверки верстки на разных экранах, надо было иметь либо несколько девайсов и запускать на них приложения, либо запускать несколько симуляторов. В общем неудобно, сами знаете. Чтобы упростить процесс, мы решили, что почему бы не тестировать все размеры на одном устройстве? Просто берём самое большое и меняем у него фрейм UIWindow.  Получаем примерно такое: здесь (по порядку) 12 iPhone, потом восьмой и SE, а на самом деле это iPhone 14 Pro.  Но не всё так гладко — мы как-то поймали артефакты. Дело в том, что у нас были вьюхи, которые рендерятся один раз и сохраняют свою ширину после рендера. Соответственно, вы меняете ширину экрана, а вьюха запомнила свою старую ширину…В общем, некрасиво выглядело, пришлось решать и эти проблемы. На самом деле разбирать реализацию это функционала можно в отдельной статье, потому что она достаточно большая, но думаю можно выделить основные моменты. Для каждой модели телефона есть заранее известные размеры экрана. После выбора необходимого экрана мы уже знаем его ширину и высоту. С этими данными происходит примерно вот такой алгоритм действий: Вычисляем новый размер экрана Устанавливаем значение additionalSafeAreaInsets у rootViewController  в 0 Вызываем setNeedsLayout() и layoutIfNeeded() у корневой UIWindow и у UIView корневого контроллера Вычисляем новое значение additionalSafeAreaInsets Обновляем additionalSafeAreaInsets у всех UINavigationController Обновляем layout у всех view, которые были загружены Обновляем подписку на scrollView для корректной работы NavigationBar Примечание. Подробное описание реализации изменения размера экрана — это прямо отдельная статья, поэтому будет отдельная статья (либо я добавлю блок попозже). Напишите в комменты, если статья нужна. Их несколько. Первая — тесты webview. Мы хотим просто вставить URL и посмотреть как он откроет WebView. Не хотим добавлять к нему лишних символов и как-то его обрабатывать. Мы решили автоматизировать этот процесс: вставляем обычный URL — получаем урл для webview на выходе. Функциональность звуки. Она плавно перекочевала из первой версии дебаг меню. Ей, как и в прошлом дебаг меню, никто не пользуется. Ну есть и есть — пусть лежит, кушать не просит.  Очистка кэша изображений. Во время разработки часто картинки на дизайнах меняются, а перекатывают их под одним и тем же именем. Соответственно, разные библиотеки умеют кэшировать эти картинки. И если получают картинку с сервера с таким же именем, то просто берут её из кэша и не обновляют, а картинка-то уже другая.  Поэтому мы добавили такую фичу, что по нажатию кнопки мы чистим кэши у SDWebImage и у Nuke, удобно. Версия приложения. Часто на симуляторе стоит какая-то сборка, а ты даже не знаешь, какой она версии. Открываешь дебаг меню и вся эта информация уже здесь. Реализовано это с помощью синглтона Bundle.main. Там есть вся необходимая информация Вся новая функциональность идёт в рамках техцелей. Время разработчиков в квартале у нас разделено на 80 и 20: 20% времени — технические задачи, 80% — продуктовые. В рамках 20% разработчик может улучшить жизнь тестировщиков и попробовать для себя что-то новое. В процессе мы разработчиков не обременяем, например, не заставляем писать тесты в дебаг меню, потому что этот код всё равно никогда не попадет в прод. Зачем, собственно, его покрывать тестами? У нас нет дизайн ревью (и тому подобного) — каждый разработчик сам придумывает дизайн для своей функциональности. Но некоторый контроль есть — ответственные люди в конце проверят дизайн на адекватность и работоспособность. Мы также просим разработчиков показать функциональность, например, гифкой. Важное замечание. Каждая функциональность тестируется — QA действительно зайдут, посмотрят, что всё работает как надо и этим удобно пользоваться. Для разработчика дебаг меню — это способ попробовать что-то новое. Например, недавно мы подняли минимальную версию таргета приложения до iOS 14, получили доступ к новым API от Apple для таблицы коллекций. И сейчас всё наше дебаг меню полностью переписано на DiffableDataSource коллекции, как это и рекомендует Apple. Или можно попробовать изменить фрейма UIWindow, чтобы из iPhone 14 Pro сделать iPhone SE. Вряд ли кто-то когда-то попробует это в продуктовом коде. Или реализовать краш. В продуктовом коде специально краши точно не хочется завозить. Хороший тон при разработке функциональности — такая кнопочка в правом верхнем углу.  Там находится описание функциональности, можно посмотреть зачем она сделана и как работает.  Такая фича позволяет минимизировать количество обращений тестировщиков к разработчикам с вопросом «А зачем вообще это сделано, как я могу этим пользоваться?» Когда квартал заканчивается мы собираем обратную связь от тестировщиков и разработчиков, создаём задачки в бэклоге по этой обратной связи. Здесь может возникнуть вопрос — а сколько людей надо, чтобы это все реализовать? Нам всего один — Влад …. Он начал разрабатывать SSL-Pinning с того, что просто создал бэклог задач, раскидал на разработчиков и в следующем квартале дебаг меню начало само себя делать. Решение дебажить приложения без Xcode имеет несколько последствий. Мы упростили жизнь нашим тестировщикам. Они теперь могут много вещей делать сами, не обращаясь к разработчикам. Мы получили дебаггинг без Xcode. Разработчик может просто открыть симулятор, когда ему лень собирать проект. Он может открыть приложение, запустить дебаг меню и посмотреть логи, которых может быть вполне достаточно, из-за этого даже не стоит подключать Xcode. У нас много интересных задач, например, изменение размера экрана. Наши разработчики всегда готовы браться за задачи для дебаг меню — ими просто интересно заниматься. Я недавно провел опрос на тему «Что тебе нравится в дебаг меню». Результаты радуют. А что вам понравилось в дебаг меню?  P.S: У нас есть видеоверсия данной статьи в виде доклада, рассказанного мной на Alfa Mobile Meetup  #2. С момента написания этой статьи наше дебаг меню не стояло на месте и в нём появилась новая крутая функциональность. К тому же, эта статья не резиновая и описывать подробно реализацию сложной функциональность кажется too much. Поэтому, если хотите подробного описания чего-то типо того, что на видео ниже или подробнее узнать про изменение размера экрана, то дайте знать об этом в комментах.  Подборку статей из блога, надеюсь что-то из этого вам будет интересно. Как катить фичи без релизов. Часть 1: про виджеты Как катить фичи без релизов. Часть 2: про низкоуровневый Server Driven UI Хочу в iOS-разработку: к чему готовиться на собеседовании в продуктовую команду 100 дней из жизни новичка: как устроен онбординг в мобильной разработке Раскладываю Swift Runtime на детали Быть или не быть тимлидом – вот в чём вопрос Также подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости о митапах и стажировках, видео с митапов, краткие выжимки из статей, иногда шутим.  iOS разработчик в Альфа-Банке