Каждый разработчик, независимо от квалификации и типа текущей задачи, постоянно находится в знакомом всем цикле: мы пишем код, запускаем и исправляем. Количество итераций у каждого разное, но делаем мы это ежедневно множество раз.
По данным некоторых исследований, мы в среднем тратим до 60% на отладку — и это именно усредненное значение, для кого-то, особенно для начинающих разработчиков, оно может быть ещё больше. Эта статья призвана уменьшить это время и сделать процесс отладки эффективнее и приятнее.
Эта статья является условным пересказом одноименного доклада для любимой iOS команды Noveo. Факт того, что это пересказ накладывает определённый след на статью и стилистику изложения. Если что-то останется непонятным, welcome в комментарии, постараюсь ответить на все вопросы.
Пример
Давайте посмотрим на процесс изнутри. Я привёл немного странный пример, но он достаточно связан с ежедневной рутиной и отлично опишет большинство кейсов. Этот код производит вычисления высоты для ячейки таблицы: высота зависит от некоторых констант и других функций. Представим, что есть баг, где высота рассчитывается неверно и нашей задачей становится изучить эту функцию и исправить.
Первым под поздозрение попадает флага showTitle и мы хотим знать его значение в момент вычисления. Что первым приходит на ум? Правильно, поместить print для отладки.
Запускаем проект — он у нас большой, монолитный, да и Xcode не идеальный. Чаще всего происходит всем знакомая ситуация: добавили одну строчку и ждём несколько минут компиляцию. А тут и твиттер манит и тик-ток (у кого что, в зависимости знаете ли вы ObjC 😁 ) и вот уже пара минут компиляции превращается в 20 с выпадом из задачи. А ведь кроме сборки и запуска нужно ещё и восстановить условия, добраться до нужного экрана, воспроизвести проблему и только после этого посмотреть вывод свежедобавленного print’a.
И хорошо если догадка верна, но, как это обычно бывает, с первого раза расставить print’ы в полезных местах не получается. Само по себе знание о состояниии флага нам почти ни о чём не говорит, поэтому было решено добавить ещё один print с информацией об элементе, для которого производится расчёт. Затем мы пожелали переопределить некоторые значения констант и сделать исключение для одного элемента. Получается примерно такое:
И снова нас ждёт сборка, воспроизведение условий и анализ. Сразу оговорюсь — отладка через print’ы сама по себе не является чем-то плохим. Порой это единственный способ отладить что-то в сложных ситуациях, например когда баг наблюдается только в релизной сборке с включенными оптимизациями. Но сегодня разговор пойдёт о простых, рутинных сценариях, которые чаще всего происходят во время разработки.
Breakpoints
Мы уже выяснили, что отладка через принты не совсем эффективна и нам нужен какой-то инструмент, который облегчит жизнь. Таким инструментом являются брейкпоинты! Этот механизм представлен практически во всех средах разработки, на множестве платформ и языков. Где-то он реализован лучше, где-то хуже, но Apple нам предоставила мощный и гибкий механизм точек останова. Однако, работая с разными разработчиками, я заметил, что пользуются брейкпоинтами в большинстве случаев только для остановки программы — просто чтобы убедиться, что её выполнение пошло по запланированному сценарию. Иногда люди пользуются консолью отладки и командой po, но лишь в тех случаях, когда нужно разок выяснить состояние переменной. Я предлагаю рассмотреть дополнительные возможности отладчика, встроенного в нашу IDE, и привести примеры ситуаций, в которых они могут пригодиться.
Условные брейкпоинты
Начнём с очевидного: conditional breakpoints. Как ни странно, строка, позволяющая указать условия срабатывания брейкпоинта, всегда у нас под носом, но почему-то люди удивляются такой возможности. Для удивлённых самим наличием такого диалога — его можно увидеть, если дважды нажать на сам брейкпоинт. В поле Condition можно записать любое выражение, которое может вернуть булево значение, будь то сравнение переменной из текущей области видимости или вовсе значение какого-то синглтона. Но будьте внимательны — медленно вычисляемое выражение способно существенно снизить производительность вашей программы.
Skipping
Следующей возможностью, которая тоже обделена вниманием разработчиков, является игнорирование N-го количества срабатываний. Эта возможность может пригодиться, например, в рекурсивных функциях, чтобы посмотреть, что происходит на N-ой глубине, или же посмотреть результат функции для N-го элемента массива. В примере с массивом этот способ будет предпочтительнее установки условия, т.к. не требует вычисления выражения.
Actions
Но самое интересное дня нас кроется за кнопкой Add Action. Эта кнопка позволяет добавить дополнительное действие, которое будет вызвано в момент срабатывания брейкпоинта. Как вы видите, есть 6 типов действий, которыми можно дополнить брейкпоинт:
- Apple script. Позволяет запустить скрипт на одноименном языке.
- Capture GPU Frame. Для отладки приложений, использующих движок Metal, может потребоваться эта опция.
- Debugger command. Позволяет выполнить команду отладчика. О ней мы поговорим позже.
- Log-message. Позволяет вывести текстовое сообщение в лог.
- Shell command. Позволяет выполнить произвольную команду в среде, дефолтной для системы командной оболочки, sh/bash/zsh.
- Sound. Позволяет проиграть звук из динамиков компьютера, на котором запущен Xcode.
Я не буду рассказывать о первых двух типах — они слишком специфичны и вряд ли вам пригодятся. А ещё пропущу последний пункт, так как особо рассказывать там нечего. Но помнить о нём стоит — он может вам пригодиться, например, когда нужно быстро совершить некое действие в приложении вслед за триггером, которым и может являтся звук от брейкпоинта, поставленного в нужное место.
Log messages
Рассмотрим чуть более подробно тип дополнительного действия «Log message». Если мы его выберем, к нашим услугам окажется строка ввода формата сообщения. Обратите внимание, что в строке можно указывать полезные плейсхолдеры, два из которых позволяют подставить информацию о брейкпоинте и одно, самое полезное, позволяет подставить результат вычисления произвольного выражения. Таким выражением может быть переменная или любая другая конструкция используемого вами языка программирования. Но это не имеет никакого смысла, если не поставить галочку «Automatically continue after evaluating actions». Именно она в паре с любым из действий позволит нам экономить время на дебаге. Больше не нужно писать print(), пересобирать проект и ждать вечность. В любой момент времени, без перезапуска проекта вам доступен вывод в консоль отладки любой информации о ходе выполнения программы. А для знающих толк в извращениях дебаге Apple предусмотрела возможность воспроизвести выражения, используя встроенный синтезатор речи.
Shell command
Нетрудно догадаться, что этот экшен позволяет запустить произвольную команду в стандартной оболочке терминала ОС. Как и «Log message», она позволяет вычислить результат выражения в текущем контексте и дополнить им аргументы вызова команды. Для чего это может быть полезно? Примеров использования можно придумать массу. Из реальной жизни: запуск троттлинга через Charles. Необходимо было замедлять запросы из определённой точки, при этом в остальное время соединение должно было быть полноценным. Я не успевал включать-выключать троттлинг вручную и ещё совершать действия в симуляторе. Такой трюк с брейкпоинтом и «Shell command» отлично меня выручил. В другой раз мне понадобилось изменять информацию на сервере прямо параллельно с запросом, чтобы отловить довольно странный баг. Тут тоже был кстати этот вид брейкпоинта. Особые извращенцы могут собрать конструкцию на Arduino с электрошокером и бить себя током при каждом срабатывании нежелательного кода. Шучу. Не пытайтесь это воспроизвести в реальной жизни.
Debugger command
Одним из самых интересных видов экшенов я считаю «Debugger command». Этот экшен позволяет действительно безгранично влиять на отлаживаемую программу. Debugger command — это команды отладчика LLDB, а LLDB — это отладчик для проекта LLVM, который сейчас используется Apple и Xcode для сборки программ. Отладчик LLDB позволяет подключаться к процессу, прерывать выполнение программы и воздействовать на её память. Для этого отладчик имеет множество команд, некоторые из которых станут героями сегодняшнего повествования. Именно благодаря отладчику LLDB у нас в принципе есть такая замечательная возможность отлаживать программу, в частности устанавливать брейкпоинты.
Начнём мы с самой известной команды — po. Наверняка многие из вас уже не раз использовали эту команду при отладке, но для меня в своё время это стало открытием, хотя я уже имел некоторый опыт в разработке под iOS на тот момент. po — это сокращение от print object. Команда позволяет вычислить выражение из правой части от команды и распечатать в консоли результат выполнения. При этом у объекта запросится его debugDescription, если он определён, или просто description, если нет. У po существует команда-прародитель — print, или p, которая точно так же вычислит выражение и распечатает результат, но только в этом случае вам будет доступна сырая информация об объекте или скалярном типе. Обе эти команды будут компилировать введенное выражение в текущем контексте, что неминуемо замедлит выполнение кода при срабатывании брейкпоинта. К счастью, в Xcode 10.2 Apple добавили ещё одну команду отладчика — v, которая работает значительно быстрее. Она позволяет вывести в консоль значение переменной из текущей области видимости, но, в отличии от p и po, без компиляции выражения. Естественное ограничение, накладываемое этой особенностью, — вывод в консоль возможен только для хранимых свойств.
Влияние на ход программы
Такая комбинация (брейкпоинт + debugger command po + автоматическое продолжение) заменит нам описанную ранее Log message. Что же ещё мы можем сделать с помощью такой комбинации? Например, с помощью дебаггера мы можем пропустить выполнение нескольких строчек кода, будто они закомментированы. При этом вам не нужно пересобирать программу и заново воспроизводить условия. Для этого достаточно ввести
thread jump --by 1
для скачка вперёд на одну строчку или же
thread jump --line 44
для перехода, как вы уже могли догадаться, к 44 строчке.
Но будьте осторожны — вы не можете на 100% безопасно перепрыгивать по строчкам. Дело в том, что вы можете перепрыгнуть через инициализацию некоторой переменной, и это вызовет краш. Дело осложняется тем, что Swift «ленив» по своей природе, и инициализация может происходить не там, где вам кажется. Плюс компилятор при сборке вашей программы вставляет дополнительные инструкции, например для управления памятью, пропуская которые вы рискуете получить в лучшем случае утечку, в худшем — краш.
Влияние на дебаггер
Кроме влияния на вашу программу, с помощью отладчика вы можете влиять на сам отладчик. Например, мы можем поставить брейкпоинт из брейкпоинта. Вы спросите, зачем это нужно? Бывают методы общего назначения, которые срабатывают по ряду триггеров. Например функция по отправке сообщения в аналитику может вызываться сотню раз в секунду, а нам нужно отловить именно ту отправку, которую породит нажатие на кнопку. В этом случае мы можем поставить брейкпоинт на метод нажатия кнопки и добавить команду установки брейкпоинта на произвольной строке программы в произвольном файле. Команда bp s -o -f Calc.swift -l 44 расшифровывается как breakpoint set one-shot на файл Calc.swift на строку 44. Модификатор -o или --one-shot создаст специальный тип брейкпоинта, который «живёт» ровно до момента своего срабатывания, а после исчезает. Таким нехитрым способом мы можем создавать интересные алгоритмы установки брейкпоинтов для отладки нетривиальных багов.
Other breakpoints types
А есть ли ещё виды брейкпоинтов, о которых мы можем не знать? Конечно, есть. Xcode позволяет добавить некоторые виды брейкпоинтов, которые не относятся к какому-то конкретному файлу и строке. В Xcode есть вкладка Breakpoint Navigator, которая позволяет управлять уже созданными брейкпоинтами сквозь все файлы проекта, а также создавать новые. Внизу окна нашего IDE есть кнопка со значком плюса.
Это позволяет использовать 6 дополнительных типов брейкпоинтов:
- Swift Exception брейкпоинт — брейкпоинт, останавливающий программу при срабатывании не перехваченного throw для Swift кода.
- Exception брейкпоинт — то же самое, но для мира ObjC. Может показаться, что это не актуальный в современном мире брейкпоинт, но это не так. Стоит помнить, что нам пока всё ещё нужен UIKit, написанный на ObjC, ошибки которого мы можем отловить с помощью такого вот брейкпоинта.
- Symbolic breakpoint — позволяет останавливать процесс выполнения программы при выполнении кода, ассоциированного с некоторым идентификатором, который Apple называет символом. О символах я расскажу чуть позже.
- OpenGL ES Error брейкпоинт — брейкпоинт, останавливающий программу при возникновении ошибки OpenGL при разработке соответствующих приложений.
- Constraint Error breakpoint — очевидно, остановит вашу программу при возникновении ошибки автолейаута.
- Test Failure breakpoint может вам помочь при отладке тестов.
Так как уместить в этой сессии обзор всех типов точек останова не представляется возможным, я остановлюсь только на самых часто используемых. По своему опыту — я всегда использую Exception breakpoint. Довольно часто при разработке программ я сталкиваюсь с перехваченными системными исключениями, отладить которые порой проблематично из-за крайне неинформативного call stack’а. Думаю, вы сталкивались хоть раз с такой или подобной ошибкой:
Exception breakpoint
Для того, чтобы сделать стек вызова более информативным, мы можем добавить Exception breakpoint. Он позволит остановить программу прямо на моменте выброса исключения и отследить цепочку событий, которые привели к такому результату. По умолчанию неперехваченное исключение вызовет аварийную остановку приложения, и в стеке вызова мы ничего полезного не увидим, т.к. исключение будет пробрасываться вверх по стеку вызова и вся информация о месте выброса будет утеряна. Exception breakpoint позволяет остановить программу в момент выброса исключения и уже привычными нами методами получить гораздо больше информации о проблеме, пройдясь по стеку вызова и просмотрев значения переменных, если это необходимо. Я считаю этот тип брейкпоинта очень полезным и использую его на всех проектах по умолчанию. Для этого в Xcode есть удобный механизм, который позволяет указать брейкпоинту уровень и хранить его на трёх уровнях:
- Проект.
- Воркспейс.
- Пользователь.
Просто нажмите на брейкпоинт правой кнопкой мыши и выберите Move breakpoint. Перенесённый на уровень пользователя, брейкпоинт будет доступен на всех проектах, какой бы вы ни открыли в вашем Xcode.
Symbolic Breakpoint
Вторым часто используемым типом брейкпоинтов является Symbolic Breakpoint. Ранее я уже писал, что этот брейкпоинт позволяет останавливать программу при выполнении кода, ассоциированного с каким-то символом, и обещал рассказать подробнее про символы. Так вот, символы — это человекопонятные идентификаторы, которые ассоциируются с тем или иным адресом в памяти. LLDB умеет маппить известные ей символы в адреса функций и наоборот. При каждой сборке проекта система создаёт особый бандл из специальных файлов в формате dSYM, которые расшифровываются как Debug Symbols. Эти файлы хранят что-то вроде таблицы, содержащей в себе некоторые адреса методов и некоторые идентификаторы, среди которых сигнатуры методов, имена файлов, смещения и номера строк. Именно благодаря этим файлам мы можем поставить брейкпоинт на строку файла, получить читаемый стек вызова или расшифровать crashlog приложения из AppStore.
Благодаря этому механизму мы можем поставить брейкпоинт на любом методе класса, зная только его название. При этом нам не нужно достоверно знать, где этот метод объявлен и доступны ли вообще нам исходные файлы. Давайте рассмотрим реальный пример. Вас перевели на новый проект, и первая задача — исправить непонятное поведение на форме ввода данных кредитной карты, когда посреди набора фокус вдруг перепрыгивал на поле ввода имени. Сходу ничего не понятно, кода много, но симптомы ясны.
Для расследования необходимо понять, кто и почему инициирует смену фокуса. Можно долго читать код, искать логику в неочевидных расширениях классов, а как надоест — сделать наследника UITextField’a, переопределив там метод becomeFirstResponder(), поменять реализации и уже там поставить брейкпоинт. А можно за 10 секунд создать символьный брейкпоинт -[UITextField becomeFirstResponder], и программа остановится в момент смены фокуса. По цепочке бэктрейса мы сможем легко восстановить последовательность событий, которые приводят к нежелательным результатам.
У тех, кто пользуется таким видом брейкпоинта в первый раз, наверняка возник вопрос: а что это за символ
-[UITextField becomeFirstResponder]? Это ObjectiveC-сигнатура метода установки текста для лейбла. Использование ObjectiveC обусловлено тем, что UIKit написан именно на этом языке.
Пара слов для тех, кто имел мало опыта с ObjectiveC. Знак минуса обозначает, что нас интересует инстанс-метод, а не метод класса, далее в квадратных скобках записывается название класса и через пробел метод, двоеточие указывает на то, что этот метод принимает параметр.
Тут можно возразить, что пример притянут за уши. Я согласен — в хорошем коде не будет десятка мест с установкой текста лейбла, но моя цель — показать, как это может работать. Давайте рассмотрим более реальный пример. Допустим, для целей отладки нам может понадобиться распечатать последовательность показа вью контроллеров. Добавляем брейкпоинт с символом -[UIViewController viewDidAppear:], указываем дополнительное действие po NSStringFromClass([instance class]) и, конечно же, не забываем поставить галочку «Automatically continue after evaluating actions».
Мы снова вынуждены использовать ObjC, даже в дополнительной команде, так как находимся в его контексте. Что касается Swift, то символы записываются как название ClassName.methodName(param:). Прописывать параметры не обязательно, LLDB попытается разрешить неоднозначность, если есть методы с одинаковым названием, но разными параметрами.
Поиск символов
Рассказывая о символьных брейкпоинтах, я не могу не рассказать о возможности искать символы. Остановив программу любым способом, с помощью брейкпоинта или же просто нажав на пиктограмму паузы, мы можем воспользоваться командой
image lookup -r -n
и найти интересующие вас символы в вашей программе и во всех загруженных библиотеках. Это действительно делает вас чуть ли не богом дебага, потому как вы властны искать символы везде, скажем в UIKit’e, искать приватные методы, останавливать и изучать внутреннее устройство системных библиотек. Надеюсь, я убедил в вас в силе этого метода и он не раз поможет вам сэкономить время.
Watchpoints
Вотчпоинты позволяют останавливать программу, когда изменяется значение переменной. Корректнее будет сказать, что этот механизм позволяет следить за изменениями памяти по заданному адресу с заданным размером, но благодаря LLDB и Xcode разработчику достаточно сделать несколько кликов. Использование вотчпоинтов будет удобным, когда за изменением переменной не следует никакого сайд-эффекта прямо после изменения, но её состояние важно для отложенных вычислений. В ряде случаев может быть непонятно, что инициирует это изменение, и вотчпоинты позволят быстро узнать это. Достаточно приостановить выполнение программы в контексте нужного класса и воспользоваться окном Variables View.
Тут будут перечислены переменные в текущем фрейме, доступные к отлаживанию. В крупных проектах вычисление доступных переменных и их типов может занимать некоторое время, поэтому иногда нужно подождать несколько (десятков?) секунд перед тем, как переменные будут доступны к манипуляциям над ними. Приятным бонусом является возможность «заглянуть» внутрь объектов Objective-C: функциональность Variables View позволяет увидеть приватные переменные этих объектов. По клику правой кнопки мыши по переменной нам доступно не так много опций — мы можем изменять значение переменных скалярных типов и, собственно, добавлять вотчпоинты.
Конечно же, вотчпоинт можно установить и командой LLDB: watchpoint set variable variable_name, или, пользуясь функцией сокращения команд LLDB, просто: w s v variable_name, но помните, что переменная должна быть видна отладчику, то есть находиться в текущем фрейме. Помимо установки брейкпоинта на изменение переменной, нам доступна установка вотчпоинта на область памяти:
watchpoint set expression — 0x0d78ab5ea8
. В обоих случаях при изменении содержимого памяти по отслеживаемому адресу произойдет прерывание программы. Установленные точки останова можно посмотреть командой
watchpoint list
или в Debugger navigator. Так как любые вотчпоинты в итоге следят за адресом памяти, они становятся неактуальны после перезапуска и не сохраняются между перезапусками приложения. Даже если вы установили брейкпоинт на изменение переменной, под капотом механизм lldb вычислил её адрес и поставил вотчпоинт по этому адресу.
Влияем на состояние
Будем закругляться. Последнее, о чем я хотел поведать в рамках этой статьи, — влияние на состояние приложения из LLDB. До этого я говорил только об изменении состояния какого-либо объекта системы при остановке по брейкпоинту. Но что, если нам требуется приостановить программу в произвольный момент времени? Нажатие на значок паузы приводит к приостановке программы, но вот вместо привычного нам кода мы увидим код ассемблера. Так как же добраться до произвольного объекта и выполнить с ним хитрые манипуляции?
Memory graph
Большинство iOS-разработчиков уже с первых месяцев своей работы используют этот инструмент. Для тех, кто ни разу им не пользовался, поясню. Memory graph позволяет сделать дамп памяти программы и отобразить в виде списка и графа все экземпляры объектов, которые сейчас находятся в памяти. Зачастую этот инструмент используется для выявления утечек объектов и анализа связей, которые привели к такому результату. Но сегодня от этого инструмента нам нужна только возможность остановить программу в произвольное время, найти нужный объект и узнать его адрес. Но что мы можем сделать с этой, казалось бы, бесполезной информацией?
На самом деле — всё, что угодно. Тут нам на помощь приходит мощь ObjC. Мы можем написать
[0x7fafffa54a5 setValue:[UIColor redColor] forKey:@"switchedOffColor"]
— и мы уже поменяли значение цвета выключенной лампы на красный, используя стандартные методы NSObject, доступные нам из коробки. Но что, если нам недостаточно этих методов, а нужно «дёрнуть» за свои рычаги? Всё просто — мы можем использовать кастинг:
[(MyLamp *)0x7fafffa54a5 powerOff]
. Используя подобные техники можно воздействовать на любые сервисы, менеджеры и вью модели вашего приложения в любой момент времени.
Мы можем сохранить значение этого адреса в переменную для удобства:
(MyLamp *)$lamp = 0x7fafffa54a5
. Важно, что название переменной должно начинаться со знака доллара. Это переменная будет жить до полной остановки программы, то есть ей можно пользоваться не только в текущем сеансе отладки, но и при следующем прерывании программы в рамках одного запуска.
ObjectiveС предоставляет поистине широкие возможности для того, чтобы похакать текущее состояние и обойти многие ограничения, но что делать с классами, доступными только в Swift? Конечно же, при попытке кастинга Swift-класса в ObjC-контексте ничего не произойдёт. К счастью, в Swift есть подобный механизм. Точнее, функция, имя которой — unsafeBitCast(_:to:). Мы вправе использовать его с адресом:
unsafeBitCast(0x7fafffa54a5, to: MySwiftLamp.self)
и получить экземпляр класса MySwiftLamp по адресу. Помните, её использование небезопасно, о чём нам намекает её имя, и её крайне осторожно нужно применять в коде приложения. Хотя, когда вам осознанно нужно будет использовать эту функцию, вы будете достаточно опытны для таких предупреждений.
View Hierarchy
Рядом со инструментом Debug Memory Graph соседствует другой, не менее полезный инструмент, — View Hierarchy. Он позволяет быстро найти нужную View, посмотреть её параметры и лейаут, посмотреть активные и неактивные констрейнты. С iOS 11 этот инструмент ещё научился отображать ViewController’ы в иерархии, таким образом находить нужную View стало легче. Неочевидным тут является возможность фильтрации по имени и возможность отключить/включить отображение View, скрытых за экраном. Также я обратил внимание, что редко кто пользуется панелью управления внизу окна визуального отображения View.
Кроме того, что она может регулировать глубину просмотра иерархии, она позволяет указать «включить отображение обрезанного контента» и «включать отображение констрейнтов». Обязательно поиграйтесь со всеми инструментами, я уверен — вы найдете полезное для себя применение для некоторых из них.
Но в рамках этого рассказа нам нужна только возможность найти нужную View и узнать её адрес. Далее действуем по накатанной:
po unsafeBitCast(0x7fafffa54a5, to: UIView.self)
но в таком случае мы получим ошибку, т.к. сейчас находимся в контексте ObjectiveC и не можем использовать po со Swift-кодом. Мы вынуждены использовать команду expession, или просто e с указанием языка:
e -l Swift -- unsafeBitCast(0x7fafffa54a5, to: UIView.self)
Но и тут наши попытки не увенчаются успехом, мы получим ошибку error: :3:35: error: use of unresolved identifier ‘UIView’.
Это произойдет из-за модульной природы Swift’а. Для успешного выполнения операции нам потребуется сделать импорт модуля UiKit:
e -l Swift -- import UIKit
, и после этого мы наконец добьёмся результата:
e -l Swift -- unsafeBitCast(0x7fafffa54a5, to: UIView.self)
.
Ура! Мы получили описание в консоли. Теперь давайте попробуем поменять, скажем, цвет её бэкграунда. Для начала сохраним View в переменную, чтобы облегчить процесс доступа к ней. Как и в случае с ObjectiveC, при создании переменной в LLDB контексте её название должно начинаться со знака доллара:
e -l Swift -- let $view = unsafeBitCast(0x7fafffa54a5, to: UIView.self)
далее мы можем применить необходимые изменения:
e -l Swift -- $view.backgroundColor = .red
Чтобы увидеть изменения, необходимо продолжить выполнение программы. Но есть способ увидеть изменения и без этого, находясь в режиме «паузы».
Дело в том, что мы не видим изменения не потому, что приложение приостановлено, а потому, что все изменения UIView копятся в транзакцию CALayer и применяются только в конце «вращения» текущего RunLoop’а с помощью вызова CATrasaction.flush(). Когда приложение приостановлено для отладки, операционная система всё ещё живёт своей жизнью, вы можете свернуть это приложение и открыть другое. Операционная система всё ещё опрашивает состояние UI вашего приложения и отрисовывает ваше приложение несколько десятков раз в секунду, только RunLoop приостановлен, CATrasaction.flush не вызывается, изменения не применяются.
Так что, достаточно самостоятельно сделать вызов
e -l Swift -- CATrasaction.flush()
, и мы увидим изменения.
На этом пора завязывать. Надеюсь, приведённые примеры кому-то облегчат жизнь, сохранят время и нервы. Добавьте в закладки, и в следующий раз, когда на поиск и отладку очередного бага у вас будет уходить более 15 минут, загляните в эту статью — возможно, какой-нибудь приём вам пригодится.