<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:atom="http://www.w3.org/2005/Atom">

<channel>

<title>// by kei_sidorov</title>
<link>https://sidorov.tech/</link>
<description>Личный блог для статеек</description>
<author>Кирилл Сидоров</author>
<language>en</language>
<generator>E2 (v3576; Aegea)</generator>

<itunes:owner>
<itunes:name>Кирилл Сидоров</itunes:name>
<itunes:email></itunes:email>
</itunes:owner>
<itunes:subtitle>Личный блог для статеек</itunes:subtitle>
<itunes:image href="" />
<itunes:explicit></itunes:explicit>

<item>
<title>Почему Task под MainActor ухудшает производительность</title>
<guid isPermaLink="false">10</guid>
<link>https://sidorov.tech/all/pochemu-perenos-vsego-task-pod-mainactor-uhudshaet-proizvoditeln/</link>
<pubDate>Tue, 18 Nov 2025 11:02:38 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/pochemu-perenos-vsego-task-pod-mainactor-uhudshaet-proizvoditeln/</comments>
<description>
&lt;p&gt;Переход на Swift 6 и новую модель concurrency заставляет постепенно адаптировать старый код. Некоторые функции становятся асинхронными, начинают обновлять интерфейс, и легаси код начинает обрастать вставками Task. Часто в этих задачах нужно обновить интерфейс и возникает простое решение: пометить весь таск как @MainActor. Ошибки исчезают, UI обновляется, задача закрыта.&lt;/p&gt;
&lt;p&gt;Однако это решение может снизить производительность приложения. Рассмотрим пример и почему так может происходить.&lt;/p&gt;
&lt;p&gt;Допустим, была кнопка, по нажатию на которую запрашивались некоторые данные из пары источников и показывались в UI.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
let workers = dataProvider.getAllWorkers()
var result: [Schedule] = []
for worker in workers {
    let schedule = dataProvider.getSchedule(for: worker)
    result.append(schedule)
}
display(schedule: result)
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Когда-то это были очень простые операции из маленького локального файла, но потом стали сетевыми и очевидно нужно делать их async. Придется оборачивать в Task и чтобы там точно ничего не крешнулось, помечать &lt;samp&gt;@MainActor&lt;/samp&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
Task { @MainActor in
    setActivityIndicator(visible: true)
    let workers = await dataProvider.getAllWorkers()
    var result: [Schedule] = []
    for worker in workers {
        let schedule = await  dataProvider.getSchedule(for: worker)
        result.append(schedule)
    }
    setActivityIndicator(visible: false)
    display(schedule: result)
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Такой код действительно работает и работает корректно. Задачи получения из провайдера выполняются в фоне и не блокируют главный поток. Но внутри этой конструкции скрыт механизм, который начинает перегружать главный поток.&lt;/p&gt;
&lt;h3&gt;Что происходит под капотом&lt;/h3&gt;
&lt;p&gt;Каждый оператор await создаёт точку приостановки. После выполнения асинхронной функции Swift обязан вернуть выполнение туда же, где оно было запущено, а в данном случае — в MainActor.&lt;/p&gt;
&lt;p&gt;В примере выше четыре N await функций дают N возвратов на главный поток.&lt;/p&gt;
&lt;p&gt;Упрощённо, MainActor при каждом таком возврате:&lt;br /&gt;
— ставит continuation в свою очередь;&lt;br /&gt;
— планирует выполнение на главном потоке;&lt;br /&gt;
— когда главный поток свободен — продолжает выполнение следующего шага.&lt;/p&gt;
&lt;p&gt;И так 5-10-15 лишних возвратов на MainActor только чтобы загрузить расписание для следующего работника! Swift компилятор не умеет «слепить» это в один hop — акторная модель запрещает такую оптимизацию.&lt;/p&gt;
&lt;p&gt;Когда таких участков в приложении становится много, и каждый делает несколько await на MainActor, суммарная нагрузка на главный поток растёт. На горячих путях (например, старт приложения) это может проявляться в подлагивании интерфейса, задержках анимаций и снижении отзывчивости.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;🤔&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Кажется пример сильно выдуманным?&lt;/p&gt;
&lt;p&gt;Но нет, в гибридном коде, который только стремится к new concurrency, это может быть частым паттерном. К тому-же, тут велик соблазн обратиться к LLM чтобы фиксить такие мелочи автоматом, и он без должного промтинга наравит просто раставить @MainActor повсюду чтобы обезопасится от крешей. Будьте внимательны!&lt;/p&gt;
&lt;/div&gt;&lt;h3&gt;Как писать правильно&lt;/h3&gt;
&lt;p&gt;Асинхронную работу нужно выполнять вне MainActor. Обновление интерфейса — только в одном коротком блоке.&lt;/p&gt;
&lt;p&gt;Правильный подход использовать detached задачу и не ленится проставлять &lt;samp&gt;MainActor.run&lt;/samp&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
Task.detached {
    await MainActor.run { setActivityIndicator(visible: true) }
    let workers = await dataProvider.getAllWorkers()
    var result: [Schedule] = []
    for worker in workers {
        let schedule = await  dataProvider.getSchedule(for: worker)
        result.append(schedule)
    }
    await MainActor.run {
        setActivityIndicator(visible: false)
        display(schedule: result)
    }
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;В этом варианте тяжёлая работа остаётся на рабочем executor’е, а главный поток получает только необходимые небольшие UI-вызовы.&lt;/p&gt;
&lt;p&gt;При использовании агентов для рефакторинга, попробуйте добавить этот абзац в ваши правила:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Всегда анализируй async/await-код с точки зрения нагрузки на MainActor: не размечай весь Task как @MainActor, если внутри есть тяжёлая работа. Помни, что каждый await в MainActor-контексте создаёт hop на главный поток. Тяжёлую часть выполняй в фоне (Task или structured concurrency), а UI-обновления выноси в короткие блоки через MainActor.run. Используй Task.detached только осознанно — он рвёт structured concurrency. Главная цель — минимум возвратов на главный поток и максимум отзывчивости интерфейса.&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;h3&gt;Итог&lt;/h3&gt;
&lt;p&gt;Пометить весь Task как MainActor — простое, но зачастую ошибочное решение в условии отсутствия хорошей архитектуры. При наличии нескольких await внутри оно приводит к множественным возвращениям на главный поток, снижает его пропускную способность и вызывает лаги.&lt;/p&gt;
&lt;p&gt;Главный поток должен обновлять только интерфейс.&lt;br /&gt;
Асинхронная работа должна выполняться вне MainActor.&lt;br /&gt;
Один короткий вызов UI в конце — оптимальная стратегия, которая масштабируется и сохраняет отзывчивость приложения.&lt;/p&gt;
</description>
</item>

<item>
<title>Определяем триал правильно</title>
<guid isPermaLink="false">9</guid>
<link>https://sidorov.tech/all/opredelyaem-trial-pravilno/</link>
<pubDate>Thu, 26 Jun 2025 22:46:02 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/opredelyaem-trial-pravilno/</comments>
<description>
&lt;p&gt;Недавно в нашем приложении случился интересный кейс с in-app покупками — настолько неожиданный, что мы сначала решили, что это баг в StoreKit. А потом оказалось — всё работает ровно так, как должно. Просто знать об этом негде.&lt;/p&gt;
&lt;p&gt;Мы делаем приложение, которое помогает пользователям в США подготовиться к сдаче на права — на автомобиль, мотоцикл, коммерческий транспорт. В процессе они проходят серию экзаменов и марафонов вопросов, и после успешного завершения получают сертификат, который нужен для подачи документов в DMV (аналог ГАИ в Штатах). Мы предлагаем доступ к приложению по подписке — и в ней недавно начали эксперимент с триалом на 3 дня, чтобы улучшить конверсию.&lt;/p&gt;
&lt;p&gt;На эти три дня мы открываем доступ ко всему приложению, позволяем проходить все тесты, марафоны и экзамены, но выпуск сертификата доступен только после полной оплаты. Это осознанное решение: он имеет юридическую ценность, и давать его бесплатно мы не можем.&lt;/p&gt;
&lt;p&gt;И вот через неделю дней после запуска триала нам начали писать в поддержку: «Я оплатил подписку, а сертификат не доступен». Сначала мы долго копали в сторону синхронизации прогресса, потом и восе потерялись в догадках. Как всегда «у меня точно такая же рука и не болит». Мы смотрим — действительно, в чеке Apple оплата прошла, но в приложении числится триал, как так может быть? На этом месте мы начали думать, что StoreKit барахлит, или где-то подвис кеш. Но таких пользователей стало больше. И все они присылали реальные чеки с оплатой. Удивительно!&lt;/p&gt;
&lt;p&gt;Неделю копали и раскопали:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Пользователи действительно проходили все тесты за 1–2 дня — до окончания trial.&lt;/li&gt;
&lt;li&gt;Видели, что сертификат недоступен, и не хотели ждать.&lt;/li&gt;
&lt;li&gt;Шли в настройки приложения → Управление подписками → выбирали другую подписку из той же группы, но без триала — и оформляли её.&lt;/li&gt;
&lt;li&gt;У обеих подписок был один и тот же &lt;samp&gt;groupID&lt;/samp&gt;, и обе находились в &lt;samp&gt;currentEntitlements&lt;/samp&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Вот только одна — с trial, а вторая — платная. Мы не предполгагали что в один момент может существовать две транзакции и считали что при оформлении подписки в той же группе транзакция с триалом протузнет и будет только одна. Да чего уже там, мы вообще не предполагали что такая комбинация возможна — мы перестали предлагать опцию без триала на всех пейволах.&lt;/p&gt;
&lt;p&gt;То есть у нас в логике было примерно так (очень упрощенно):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
extension Transaction {
    /// Триальная ли транзакция
    var isTrial: Bool {
        guard 
                let expirationDate = expirationDate, 
                Date() &lt; expirationDate 
        else { return false }
        return offer?.paymentMode == .freeTrial
    }

    /// Есть ли триальная транзакция у пользователя
    static var hasTrialSubscription: Bool {
        get async {
            for await result in Transaction.currentEntitlements {
                do {
                    if try result.payloadValue.isTrial {
                        return true
                    }
                } catch { continue }
            }
            return false
        }
    }
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝🏼&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;При смене одной подписки через настройки на другую (в той же группе) во время действия триала, появляется две активные транзакции: первая на время действия триала и вторая с оплаченной подпиской, с одинаковым &lt;samp&gt;originalTransactionId&lt;/samp&gt;.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
extension Transaction {
    /// Триальная ли транзакция
    var isTrial: Bool {
        guard let expirationDate = expirationDate, Date() &lt; expirationDate else { return false }
        return offer?.paymentMode == .freeTrial
    }

    /// Есть ли триальная транзакция у пользователя
    static var hasTrialSubscription: Bool {
        get async {
            for await result in Transaction.currentEntitlements {
                do {
                    if try result.payloadValue.isTrial {
                        return true
                    }
                } catch { continue }
            }
            return false
        }
    }
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;h3&gt;Решение&lt;/h3&gt;
&lt;p&gt;Решение очень простое — нужно искать последнюю транзакцию с одним &lt;samp&gt;originalID&lt;/samp&gt; и уже у неё проверять статус оффера.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
extension Transaction {
    /// Есть ли триальная транзакция у пользователя
    static var hasTrialSubscription: Bool {
        get async {
            var latestTransactions: [UInt64: StoreKit.Transaction] = [:]
            for await result in Transaction.currentEntitlements {
                do {
                    let transaction = try result.payloadValue
                    if
                        let latestTransaction = latestTransactions[transaction.originalID],
                        transaction.purchaseDate &lt; latestTransaction.purchaseDate
                    {  continue }
                    latestTransactions[transaction.originalID] = transaction
                } catch { continue }
            }
            return latestTransactions.values.contains(where: { $0.isTrial })
        }
    }
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;h3&gt;Вывод еще раз&lt;/h3&gt;
&lt;p&gt;StoreKit не инвалидирует первую триальную транзакцию из entitlements, даже если пользователь сменил подписку внутри группы. И это не баг, а особенность. Просто её надо учитывать, особенно если вы ограничиваете функциональность на trial-период.&lt;/p&gt;
&lt;p&gt;Заметка в духе «запиши, чтобы потом не искать». Надеюсь, кому-то сэкономит пару часов и несколько тикетов в поддержку.&lt;/p&gt;
&lt;p&gt;Буду рад вашей подписке на мой &lt;a href="https://t.me/sidorovtech"&gt;мой телеграм канал&lt;/a&gt;!&lt;/p&gt;
</description>
</item>

<item>
<title>Ловим скриншоты</title>
<guid isPermaLink="false">8</guid>
<link>https://sidorov.tech/all/lovim-skrinshoty/</link>
<pubDate>Mon, 21 Oct 2024 09:18:01 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/lovim-skrinshoty/</comments>
<description>
&lt;p&gt;На днях прилетел мне баг от реального пользователя, в тикете всё как положено — и описание и запись экрана. Только вот непонятно что за версия приложения. Решил я добавить оверлей с номером версии и билда в момент записи экрана или скриншота. Задача вроде бы простая — я помню там есть какие-то уведомления, подписался, скрыл/показал и дело в шляпе! Минут 30 должно хватить! Но всё оказалось куда сложнее чем я думал!&lt;/p&gt;
&lt;p&gt;&lt;cut/&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;🚨&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Если не терпится попробовать — по ссылке либа и проекты на SwiftUI и UIKit. Помните что всё использование на ваш страх и риск. &lt;a href="https://github.com/kei-sidorov/CaptureGuard"&gt;📦 Библиотека и демо-проект&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/iphone-x-and-later-take-screenshot@2x.png" width="350" height="136.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Apple позволяет трекать запись экрана — тут всё без сюрпризов. Вот тебе &lt;a href="https://developer.apple.com/documentation/uikit/uiscreen/2921652-captureddidchangenotification"&gt;ивент в Notification Center&lt;/a&gt;, вот тебе &lt;a href="https://developer.apple.com/documentation/uikit/uitraitcollection/4292628-scenecapturestate"&gt;флажок у UIScreen&lt;/a&gt;. Но со скриншотами всё не так радужно: есть только &lt;a href="https://developer.apple.com/documentation/uikit/uiapplication/1622966-userdidtakescreenshotnotificatio"&gt;уведомление о том, что скриншот сделан&lt;/a&gt;, то есть приходит только после того, как он сделан. То есть заранее подготовиться никак. Какого чёрта, подумал я, ведь точно видел приложения, которые в момент делают подмену вьюх когда делается скриншот! (спойлер: не видел я таких, ложные воспоминания)&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
struct ContentView: View {
    @State private var isScreenCaptured = false

    private let center = NotificationCenter.default
    
    var body: some View {
        ZStack {
            Text("Main Content")
            
            if isScreenCaptured {
                Text("Screen Recording in Progress")
                            .foregroundColor(.white)
            }
        }
        .onReceive(center.publisher(for: UIScreen.capturedDidChangeNotification)) { _ in
            // ⚠️ Deprecated в iOS 18+, лучше использовать `sceneCaptureState`
            isScreenCaptured = UIScreen.main.isCaptured 
        }
        /// Отловить скриншот можно подписавшись на `UIApplication.userDidTakeScreenshotNotification`
        /// но уведомление приходит после скриншота, подготовить интерфейс к скриншоту не получиться!
    }
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;h3&gt;Хорошие художники копируют, великие — крадут&lt;/h3&gt;
&lt;p&gt;Ну что далеко ходить? В телеграмме я точно помню скрываются сообщения на скриншотах. Достаточно посмотреть какие события они ловят и что делают чтобы скрыть. Погнали! Git clone, grep ‘screenshot’ Минут через 20 раскапываю, что есть такой метод &lt;samp&gt;setLayerDisableScreenshots&lt;/samp&gt; в &lt;a href="https://github.com/TelegramMessenger/Telegram-iOS/blob/1d1ea447ad3fdbf9483ce337c532cf0793f3ab3a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m#L243"&gt;UIKitUtils.m&lt;/a&gt; который используя внутреннюю вьюху &lt;samp&gt;UITextField&lt;/samp&gt; делает любой слой скрываемым на любом захвате экрана, скриншот это или запись видео.&lt;/p&gt;
&lt;p&gt;Грубо говоря, он «бафает» любой слой и позволяет ему скрываться когда идет запись. Но как это работает, по-любому какое-то событие прилетает этому слою и он скрывается. Что-б долго не экспериментировать, я решил открыть старый добрый Hopper Disassembler и покопаться в &lt;samp&gt;UIKitCore&lt;/samp&gt;.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝🏻&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Чтобы быстро найти путь до нужного фреймворка достаточно поставить брейкпоинт где-нибудь в свифтовом коде и выполнить &lt;samp&gt;po Bundle(for: UIView.self)&lt;/samp&gt;. Вместо UIView подставить нужный класс.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/hopper@2x.png" width="1122" height="783" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Чтож, раскопки приводят к следующему выводу: никаких событий действительно нет, при установке &lt;samp&gt;isSecureTextEntry&lt;/samp&gt; текст филд устанавливает слою внутреннего контейнера специальные атрибуты (&lt;samp&gt;disableUpdateMask&lt;/samp&gt;) которые отправляются на рендер сервер, а тот уже решает рисовать это вью или нет. Чтож, логично, секьюрно. Это позволит не рисовать секьюрные поля даже если приложение зависло и главный поток не отвечает.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;⚠️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Несмотря на то, что тут в явном виде мы видим значения флага 0x12, завязаться на это не стоит. У меня на iOS 18 это работает через раз, поэтому я оставил решение как в телеграмме, оно работает как часы!&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Вот такой код получается:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
import UIKit

private let uiKitTextField = UITextField()
private var captureSecuredView: UIView?

public extension CALayer {
	func makeHiddenOnCapture() {
		let captureSecuredView: UIView? = captureSecuredView
			?? uiKitTextField.subviews
				.first(where: { NSStringFromClass(type(of: $0)).contains("LayoutCanvasView") })
		
		let originalLayer = captureSecuredView?.layer
		captureSecuredView?.setValue(self, forKey: "layer")
		uiKitTextField.isSecureTextEntry = false
		uiKitTextField.isSecureTextEntry = true
		captureSecuredView?.setValue(originalLayer, forKey: "layer")
	}
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;h3&gt;Реализуем в SwiftUI&lt;/h3&gt;
&lt;p&gt;Поскольку изначально мне нужно было решить задачу в SwiftUI проекте, умения скрывать UIKit’овые слои было недостаточно. Первой идеей было использовать маски. Типа берем ZStack, там рисуем белую и черную вью, где черная будет &lt;samp&gt;UIViewRepresentable&lt;/samp&gt; со скрываемым слоем. Давайте закодим:  &lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
struct HiddenOnCaptureColorView: UIViewRepresentable {
	
	let color: UIColor
	
	func makeUIView(context: Context) -&gt; UIView {
		let view = UIView()
		view.layer.makeHiddenOnCapture()
		updateViewColor(view: view)
		return view
	}
	
	func updateUIView(_ uiView: UIView, context: Context) {
		updateViewColor(view: uiView)
	}
	
	func updateViewColor(view: UIView) {
		view.backgroundColor = color
	}
}

struct VisibleOnlyOnCaptureModifier: ViewModifier {	
	func body(content: Content) -&gt; some View {
		content.mask {
			ZStack {
				Color.black
				HiddenOnCaptureColorView(color: .white)
			}
		}
	}
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Но вот незадача — я забыл что &lt;b&gt;mask работает не с белым/черным цветом, а с прозрачными/непрозрачными пикселями&lt;/b&gt;. Что же делать? Беглый гуглёж не дал результатов, зато ChatGPT подсказал — есть такой метод на View, называется &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/swiftui/view/luminancetoalpha()"&gt;luminanceToAlpha()&lt;/a&gt;&lt;/samp&gt; который смешает пиксели и сделает черные прозрачным, а белые непрозрачным. Не верилось что это заработает, но это оказалось оно!&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;

struct VisibleOnlyOnCaptureModifier: ViewModifier {	
	func body(content: Content) -&gt; some View {
		content.mask {
			ZStack {
				Color.black
				HiddenOnCaptureColorView(color: .white)
			}
			.compositingGroup()
			.luminanceToAlpha()
		}
	}
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Юхууу! Всё оказалось проще чем я думал! Меняем цвета местами и получаем вью которые видны только на скриншот или скрываются.   &lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;⚠️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Работает это всё только на реальном девайсе, в симуляторе такое проделать не получится, как и сделать UI тесты. Учитывайте это.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;⚠️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Т.к. это манипуляции с масками, они не оказывают никакого эффекта на лейаут, учитывайте это! Работают они подобно модификатору alpha, то есть учитываются при расчете верстки, но просто скрыты/показаны при записи.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3&gt;Приключение еще на полдня&lt;/h3&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/uikit-adventure2@2x.jpg" width="336" height="189" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;К этому времени я уже написал пост в свой телеграмм канал, и решил написать эту статью. Не хватало только полной поддержки UIKit. Скрывать вью там легко, а вот показывать…&lt;/p&gt;
&lt;p&gt;Опять же, казалось всё просто — сабклассимся от вью, вешаем маску на слой и дело в шляпе. Собираю рабочий эксперимент — и ничего не работает. &lt;b&gt;Я снова забыл, что вью должны быть не черно-белыми, а прозрачными/непрозрачными.&lt;/a&gt; Но как мне сделать эффект &lt;samp&gt;luminanceToAlpha&lt;/samp&gt;? Такого в UIKit нет.&lt;/p&gt;
&lt;h3&gt;Спускаемся ниже&lt;/h3&gt;
&lt;p&gt;Я уже было хотел забить на это дело, но спортивный интерес не давал покоя. Ну как так то? В SwiftUI сделал, а в родном UIKit нет!&lt;br /&gt;
Решил посмотреть что там во View Hierarchy. Я запустил минимальное SwiftUI приложение и решил посмотреть какие «китовые» вью она нарисует, ведь весь SwiftUI рано или поздно становится слоями которые будут отправлены на рендер-сервер.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/xcode@2x.png.jpg" width="2560" height="1684" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Не буду расписывать весь мой траверсс по дереву вью, но довольно быстро я нахожу то что мне надо. Вот моя вью, с маской, в которой два слоя и ФИЛЬТР с одноименным названием luminanceToAlpha! Каеф!&lt;/p&gt;
&lt;p&gt;Осталось немного приватной ObjC магии и мы это заведем. Итак, нам нужен слой, у которого внутри будут два слоя (черный и белый), один из которых будет прокачан на скрытие на скриншотах, а родитель будет их смешивать с помощью фильтра. Это специальный приватный &lt;samp&gt;CAFilter&lt;/samp&gt; (не путать с &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/coreimage/cifilter"&gt;CIFilter&lt;/a&gt;&lt;/samp&gt;) который так просто не получить, но так как ObjectiveC рантайм позволяет творить магию вне хогвардса, это нам под силам!&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="swift"&gt;
func makeFilter() -&gt; NSObject? {
	guard let filterClass: AnyClass = NSClassFromString("CAFilter") else { return nil }
	let selector = Selector("filterWithName:")
	typealias Function = @convention(c) (AnyClass, Selector, String) -&gt; AnyObject?
	
	guard let filterWithName = class_getClassMethod(filterClass, selector) else { return nil }

	let implementation = method_getImplementation(filterWithName)
	let function = unsafeBitCast(implementation, to: Function.self)
	
	guard let filter = function(filterClass, selector, "luminanceToAlpha") as? NSObject else { return nil }
	return filter
}
&lt;/code&gt;
&lt;/pre&gt;
&lt;h3&gt;Победа!&lt;/h3&gt;
&lt;p&gt;Отлично, я добился чего хотел и даже больше. Я оформил всё это добро в &lt;a href="https://github.com/kei-sidorov/CaptureGuard"&gt;📦 SPM-пакет&lt;/a&gt;, который можно использовать в проекте. В нём два таргета — для SwiftUI и UIKit. Первую из них можно использовать вполне безопасно, т.к. никакие приватные символы там не светятся, а вот с версий для UIKit нужно быть предельно осторожным из-за CAFilters. Вроде бы ничего такого, но лучше не использовать её напрямую, а просто подсмотреть решение и как-то обфурсцировать символы перед релизом в AppStore.&lt;/p&gt;
</description>
</item>

<item>
<title>Почему не устанавливаются AdHoc приложения в iOS 15</title>
<guid isPermaLink="false">7</guid>
<link>https://sidorov.tech/all/ios-15-adhoc-enterprise-installation-issue/</link>
<pubDate>Thu, 30 Sep 2021 11:07:30 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/ios-15-adhoc-enterprise-installation-issue/</comments>
<description>
&lt;h2&gt;История&lt;/h2&gt;
&lt;p&gt;Не так давно Apple зарелизила iOS 15 и, для большинства приложений которые мы делаем в Noveo, процесс миграции прошел довольно гладко. Нативные разрабочики предпочитают как можно скорее переехать на новый Xcode, и занимаются процессом адаптации приложений начиная с первых более-менее стабильных бета-версий iOS. Но разработчики кроссплатформенных решений, в силу сильной зависимости от стороннего фреймворка, не спешат с обновлением Xcode и пытаются оставаться на предыдущей стабильной версии.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/ios15_warn@2x.png" width="410" height="271" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Специально для них у нас есть билд машины со старым Xcode (например, 12.1) и так получилось, что выпущенные этой машиной &lt;b&gt;AdHoc&lt;/b&gt; и &lt;b&gt;Enterprise&lt;/b&gt; билды перестали устанавливать или запускаться (если уже были установлены) после обновления девайса до iOS 15. При попытке установить, пользователи получали уведомление «Разработчику необходимо обновить данное приложение, чтобы оно работало с этой версией iOS.» Что не так и как именно нужно обзвонить проблему решать пришлось мне.&lt;/p&gt;
&lt;h2&gt;Ищем источник проблемы&lt;/h2&gt;
&lt;p&gt;Как видно из скриншота, попап не содержит ни какой дополнительной информации об источнике проблемы и быстрый гуглеж не даёт четкого понимания почему такое происходит. Люди пишут разные причины или предположения о них, начиная от использования UIWebView (deprecated), заканчивая форматом подписи, но никак не сходятся во мнении. Предполагаю, что относительно небольшой процент пользователей использует AdHoc и Enterprise распространение, поэтому кейсов не так много и определенного ответа нет. Перебирать разные варианты не по мне, так что нужно искать точную причину.&lt;/p&gt;
&lt;p&gt;Когда нам не пишут причины проблемы на на экране, мы лезем в логи! Открываем приложение Console (не путать с терминалом) и подключаем свой девайс. Находим его в списке слева, стартуем снятие логов, попутно отфильтровав ошибки и включив вывод только от &lt;b&gt;installd&lt;/b&gt; (сервис установки приложений в iOS). Запускаем установку приложения на девайсе и ждем.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/console_ios15_install-1.png" width="1274" height="629" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;О! Источник проблемы найден уже через минуту. Пользователи на StackOverflow были правы насчет подписи. Осталось понять как это лечить и почему такое привходит на относительно свежей, не скинутой со счетов компанией Apple, Xcode 12.x.&lt;/p&gt;
&lt;h2&gt;Причины&lt;/h2&gt;
&lt;p&gt;После небольшого поиска информации в необъятном интернете, я наконец-то попадаю на &lt;a href="https://developer.apple.com/documentation/xcode/using-the-latest-code-signature-format"&gt;страницу из документации Apple&lt;/a&gt;, где все четко и последовательно описано. Для тех, кому лень читать текст на английском, я тут кратко опишу проблему и решение.&lt;/p&gt;
&lt;p&gt;Итак, начиная с iOS 15, операционная система требует новой версии цифровой подписи (версии 20400 и выше) и указания каких-то дополнительных entitlements в метаданных цифровой подписи. Давайте убедимся, что у нас действительно такая проблема. Для этого, нужно скачать IPA файл, разарихивировать его и воспользоваться утилитой &lt;samp&gt;codesign&lt;/samp&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="bash"&gt;
$ &lt;b&gt;unzip my app.ipa -d myapp&lt;/b&gt;
$ &lt;b&gt;codesign -dv myapp/Payload/MyApp.app&lt;/b&gt;

Executable=/Users/ksidorov/Downloads/myapp/Payload/MyApp.app/myapp
Identifier=CertificateID
Format=app bundle with Mach-O universal (armv7 arm64)
&lt;i&gt;CodeDirectory v=20400 size=61615 flags=0x0(none) hashes=1917+5 location=embedded&lt;/i&gt;
Signature size=4793
Signed Time=18 Jul 2019, 16:14:53
Info.plist entries=45
TeamIdentifier=XXXXXXXXXX
Sealed Resources version=2 rules=10 files=663
Internal requirements count=1 size=172
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Нас интересует строчка, начинающаяся с CodeDirectory. Там указана версия 20400, что является ОК для iOS 15. На данный момент актуальная версия — это 20500, 20400 еще поддерживается, а те, что ниже — уже нет.&lt;/p&gt;
&lt;p&gt;Хорошо, далее давайте выясним про отсутствующие entitlements накинув еще буковок v в ключи:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="bash"&gt;
$ &lt;b&gt;codesign -dvvvvv myapp/Payload/MyApp.app&lt;/b&gt;

...
Executable Segment base=0
Executable Segment limit=5947392
Executable Segment flags=0x11
&lt;i&gt;Page size=4096
    -5=64203b52bcae912f72da800d8f5ba398f18836e21f5bf5420100c2d80596a533
    -4=0000000000000000000000000000000000000000000000000000000000000000
    -3=9a769c2a1f43913f3debfef5dbabf7948f5b5bdf8727a8045e72d5e389d0ba1b
    -2=6691168b40a4cc2b8fd5dc03ffe9b50ac20d8069856167fba9f1540b00a5b2b6&lt;/i&gt;
CDHash=50273bc68865c7fdf98e788149240977ad1e3076
Signature size=4793
...
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Для того, чтобы понять есть ли DER entitlements в подписи, достаточно бегло взглянуть на хеши в блоке &lt;samp&gt;Page size&lt;/samp&gt;. Если вы видите хеш &lt;samp&gt;-5&lt;/samp&gt;, но у вас нет хеша &lt;samp&gt;-7&lt;/samp&gt; или этот хеш заполнен нулями, то необходимые entitlements отсутствуют.&lt;/p&gt;
&lt;h2&gt;Как это исправить&lt;/h2&gt;
&lt;p&gt;На самом деле, виной устаревшей подписи является не сам Xcode, а версия MacOS. К сожалению, на сборочных нодах с Xcode 12.x мы не обновили MacOS до 11 и сборщик работал на 10.15. Он хоть и подписывал приложения валидной версией (20400), но не включал те самые DER entitlements.&lt;/p&gt;
&lt;p&gt;Соответственно, самый простой способ — обновить MacOS и запустить сборку заново. Но если обновиться возможности нет, то можно переподписать приложение, явно указав что нужны entitlements:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="bash"&gt;
$ &lt;b&gt;unzip my app.ipa -d myapp&lt;/b&gt;
$ &lt;b&gt;codesign -s &lt;i&gt;"iPhone Distribution: Your Certificate ID"&lt;/i&gt; -f --preserve-metadata --generate-entitlement-der myapp/Payload/MyApp.app&lt;/b&gt;
$ &lt;b&gt;zip -qr app_fixed.ipa myapp/Payload&lt;/b&gt;
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Конечно же, для исправления подписи вам потребуется тот же сертификат, которым было подписано приложение (точнее, один из тех, которые были указаны в провижен файле при оригинальной подписи).&lt;/p&gt;
&lt;p&gt;Случай очень частный, но, надеюсь, кому-нибудь эта мини-статья окажется полезной.&lt;/p&gt;
</description>
</item>

<item>
<title>Работаем с Bluetooth в iOS</title>
<guid isPermaLink="false">6</guid>
<link>https://sidorov.tech/all/bluetooth-ios/</link>
<pubDate>Sun, 04 Jul 2021 15:39:01 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/bluetooth-ios/</comments>
<description>
&lt;p&gt;Одно из моих любимых направлений в мобильной разработке — это разработка мобильных приложений, которые работают с объектами вне их инфраструктуры, с различными интересными гаджетами, датчиками и объектами из мира интернета вещей. Сегодня поговорим как раз про это направление, а точнее, про Bluetooth.&lt;/p&gt;
&lt;p&gt;&lt;cut/&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/kei-sidorov/ble-ios-example"&gt;📦 Демо проект с простым приложением для приёма сообщений&lt;/a&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;&lt;img src="/images/podlodka.png" style="height: 36px; margin-left: -10px;" /&gt;&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Эта статья — конспект к моему докладу на неделе «Нестандартной разработки iOS» прошедшей в рамках пятого сезона &lt;a href="https://podlodka.io/crew"&gt;Podlodka iOS Crew&lt;/a&gt;. На этой неделе, помимо работы с Bluetooth, были рассмотрены и другие интересные моменты: Metal, Core Audio и многое другое! Вы еще можете приобрести доступ к докладам или залететь на следующий сезон!&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Наверное, никому из вас не нужно объяснять что такое блютус и зачем он применяется. Сейчас эта технология беспроводной передачи данных имеет индекс версии 5.1 и очень сильно скакнула в своём развитии. Свой первый айфон я купил в 2009 году, и даже тогда мне приходилось выслушивать тонны хейта по поводу несовершенства этого девайса. Одним из саааамых частых и бесящих меня поинтов было постоянные упреки в отсутствии передачи файлов по блютусу. &lt;b&gt;И вообще в неполноценности Bluetooth на iOS&lt;/b&gt; и невозможности им пользоваться кроме как подключить беспроводные наушники. Курьезный факт: уже через 3 года, iOS имела самый мощный среди мобильных платформ фреймворк, который конкуренты еще долго не смогут догнать (ИМХО и не догнали). Действительно, большинство вещей, о которых я сегодня расскажу были представлены в 2012 году (а часть и вовсе в 2011) и мало менялись с тех пор.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/history@2x.png" width="1257" height="477" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Итак, Блютус. Я не буду давать каких-то формальных объяснений или уходить глубоко в обзорную историю. Пройдусь максимально поверхностно. Для нас, как для разработчиков, это интерфейс взаимодействия с внешними устройствами. История его развития начинается в далёком 1998 году, но более менее массовое распространение он получил в середине 2000х с широким развитием рынка смартфонов и коммуникаторов. На заре iOS, вплоть до 4й версии, разработчики не имели возможности взаимодействовать с устройствами посредством блютус вообще, но всё изменилось в 2011 году, с выходом iOS 5. Ключом к такому стремительному росту стало принятие стандарта Bluetooth 4.0, который в 2010 году, который стал отвечать требованием рынка по параметрам скорости и энергопотребления. Последнее играло ключевую роль. Именно тогда появился стандарт, а точнее спецификация, BLE – &lt;a href="https://ru.wikipedia.org/wiki/Bluetooth_с_низким_энергопотреблением"&gt;Bluetooth Low Energy&lt;/a&gt;, который позволил сильно расширить сферу применения этой беспроводной технологии.&lt;/p&gt;
&lt;p&gt;В чем же особенности BLE? Ну, как не сложно догадаться из названия, главная фишка в экстремально (на 2010 год уж точно) низком энергопотреблении. Спецификация позволяет разрабатывать устройства, которые будут минимально расходовать свою батарею и работать до одного года на одном заряде небольшого элемента питания. Спецификацию разработала компания Bluetooth Special Interest Group (SIG) и эти ребята настолько заморочились, насколько, как мне кажется, это было вообще возможно. Чтобы не давать волю полёта фантазии разработчикам и не плодить кучу конкурирующих страндартов, SIG разработала спецификацию под все возможные типы устройств, начиная с носимых датчиков пульса, заканчивая стационарными дверными замками. Именно взаимодействие через Bluetooth LE нам доступно через встроенный фреймворк CoreBluetooth. Apple тоже проделали гигантскую работу, скрыв от нас все детали работы с низкоуровневыми штучками и предоставили очень простой API с которым разберется и любой начинающий iOS разработчик!&lt;/p&gt;
&lt;h3&gt;Bluetooth Low Energy в деталях&lt;/h3&gt;
&lt;p&gt;Основой BLE является профиль GATT – General Attribute Profile, который предоставляет нам абстракции сервиса и характеристик. Характеристика — это, грубо говоря, ячейка данных, а сервис — это некоторое логическое их объединение. Может звучать непонятно, но сейчас я приведу пример и всё сразу встанет на свои места. Самый классический и на самом деле распространенный пример — термометр. Он может иметь сервис измерения температуры и иметь две характеристики: единицу измерения (℃, ℉) и собственно саму температуру. А еще он может определять влажность и для неё будет отдельный сервис с одной лишь характеристикой — влажности в процентах.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/term@2x.png" width="415" height="467" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Всё взаимодействие в BLE строиться на классической клиент-серверной модели: тут тоже есть клиент и сервер, есть запросы. С этого момента я буду плавно переходить к самому фреймворку CoreBluetooth и уже новые термины объяснять ближе к предметной области, т.к. большинство терминов совпадают с теримнами BLE, только имеют префикс CB — CoreBluetooth.&lt;/p&gt;
&lt;h3&gt;CoreBluetooth&lt;/h3&gt;
&lt;p&gt;Итак, CoreBluetooth. Как уже было сказано, появился в iOS 5, был серьезно доработан в iOS 6 и существует до сих пор, не претерпев каких-либо серьезных изменений с тех пор. Как я уже сказал, всё взаимодействие строиться на классической клиент-серверной модели и в CB у нас есть сервер – Peripheral и клиент Сentral. Такой нейминг может слегка путать некоторое время, т.к. мы привыкли что именно сервер является неким центром, а CB переворачивает все с ног на голову. Но с другой стороны всё логично: Peripheral device — периферийное устройство, внешнее по отношению к нашему, центральному. Peripheral device’ом может быть фитнес трекер, умная лампочка, замок или целая система умного дома, а может быть и другое мобильное устройство или компьютер.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/ble-model@2x.png" width="1082" height="449" alt="" /&gt;
&lt;/div&gt;
&lt;h3&gt;Поиск&lt;/h3&gt;
&lt;p&gt;Хотя и процесс является клиент-серверным, в отличии от HTTP, точный адрес или алиас устройства нам не известен, поэтому чтобы начать с ним взаимодействие необходимо его найти в эфире. Процесс обнаружения устройства называется сканированием или дискаверингом. Он выполняется Central устройством, той стороной, которая хочет к чему-то подключиться. Процесс заключается в сканировании Bluetooth эфира на предмет так называемых Advertisement-пакетов, которые должно отправлять периферийное устройство. Advertisement-пакет — это крохотный пакет регулярно отправляемый периферийным устройством, когда оно хочет чтобы его нашли. Он обычно содержит базовую информацию, необходимую для того, чтобы понять что класс устройства: термометр, замок, самокат или другой айфон. В зависимости от режима работы устройства и настроек его энергопотребления, пакеты могут рассылаться с различными интервалом, от раза в несколько микросекунд, до раза в десятки секунд, поэтому длительность сканирования напрямую зависит от режима работы периферийного устройства.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/advertisement@2x.png" width="706" height="486" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Как только искомый девайс будет найден, мы можем подключиться к нему и спросить, какие сервисы он имеет и какие характеристики нам доступны в этих сервисах. Давайте перейдем ближе к коду.&lt;/p&gt;
&lt;p&gt;Итак, мы хотим найти термометр, который для нас является периферийным устройством. Значит, мы в этом случае будем клиентом, то есть Central Device. Объект который представляет клиента в фреймворке CoreBluetooth имеет тип &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanager"&gt;CBCentralManager&lt;/a&gt;&lt;/samp&gt;. Как не сложно догадаться, всё взаимодействие c внешним устройством предполагается асинхронным. И еще проще догадаться, что Apple предпочла паттерн делегат для обеспечения асинхронной работы. Нужный нам делегат имеет тип &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate"&gt;CBCentralManagerDelegate&lt;/a&gt;&lt;/samp&gt; и объект, реализующий этот протокол и будет получать уведомления о событиях связанных с поиском и подключением.&lt;/p&gt;
&lt;p&gt;Как только мы будем готовы сканировать эфир, нужно вызвать метод &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518986-scanforperipherals"&gt;scanForPeripherals&lt;/a&gt;&lt;/samp&gt; у менеджера &lt;samp&gt;CBCentralManager&lt;/samp&gt;. Как только наше устройство увидит новый рекламный пакет, будет вызван метод делегата &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518937-centralmanagerdidDiscoverPeripheral"&gt;didDiscoverPeripheral&lt;/a&gt;&lt;/samp&gt; с объектом &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral"&gt;CBPeripheral&lt;/a&gt;&lt;/samp&gt; в параметрах метода. Когда мы найдем нужное устройство или устройства, нам нужно остановить сканирование методом &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518984-stopscan"&gt;stopScan&lt;/a&gt;&lt;/samp&gt; и вызвать &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518766-connect"&gt;connect&lt;/a&gt;&lt;/samp&gt; указав экземпляр класса &lt;samp&gt;CBPeripheral&lt;/samp&gt;.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/central-mgr@2x.png" width="984" height="491" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Поиск сервисов&lt;/h3&gt;
&lt;p&gt;Как только соединение будет установлено, мы можем начать опрашивать устройство. С этого момента мы уже работаем с объектом &lt;samp&gt;CBPeripheral&lt;/samp&gt; и его делегат &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate"&gt;CBPeripheralDelegate&lt;/a&gt;&lt;/samp&gt; которые представляют внешнее устройство, к которому мы подключились. Чтобы считать какое-то значение (характеристику) сначала нужно найти сервис, в составе которого эта характеристика существует. Запустим поиск сервисов. Мы можем как указать сервис или сервисы которые нас точно интересует, либо не указывать их и найти все, что реализует устройство.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝🏻&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Тут нужно отвлечься и объяснить как мы можем отличать сервисы друг от друга. Идентификатором для них и их характеристик служит UUID, уникальный идентификатор который бывает двух видов: 16 бит и 128 бит. 128 битные идентификаторы мы можем генерировать и использовать на своё усмотрение при написании собственных клиент-серверных приложений. А вот 16 битные зарезервированы SIG. Помните, я говорил что эти ребята сильно упоролись и описали почти всё что взаимодействует или может взаимодействовать по BLE? Так вот они &lt;a href="https://www.bluetooth.com/specifications/assigned-numbers/"&gt;пронумеровали&lt;/a&gt; все эти сервисы и характеристики и дали им уникальный 16 битный UUID. Если вы когда-либо захотите разработать железное устройство, с которым сможет работать не только ваше приложение, то вам следует изучить их спеки и следовать этим рекомендациям. Ну а если вы разрабатываете приложение для какого-то класса устройств, вы можете использовать эту спеку чтобы найти идентификатор нужного вам сервиса. Мы ищем термометр, можем смело указывать UUID &lt;samp&gt;0x1809&lt;/samp&gt; и игнорировать остальные (если они есть).&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3&gt;Поиск характеристик&lt;/h3&gt;
&lt;p&gt;Окей, мы нашли сервисы. О чем получим уведомление в делегате. Теперь схожим образом можем найти характеристики в них, для каждой вызовем &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518797-discovercharacteristics"&gt;discoverCharacteristics(for:&lt;/a&gt;)&lt;/samp&gt;. Точно так же, мы можем указать UUIDы интересующих нас характеристик, так и получить все, но это будет медленнее. Когда процесс исследования характеристик завершиться делегат получит уведомление вызовом метода &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate/1518821-peripheral"&gt;didDiscoverCharacteristicsForService&lt;/a&gt;&lt;/samp&gt; а самы характеристики будут доступны в соответствующем свойстве.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/peripheral@2x.png" width="1083" height="491" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Характеристики&lt;/h3&gt;
&lt;p&gt;На этом этапе мы наконец добрались до самого «вкусного» — до данных, а точнее, до характеристик. В большинстве несложных кейсов их можно разделить по типам операций которые они поддерживают: read, write, notify. На самом деле опций (корректнее — свойств) несколько больше, но в рамках сегодняшней статьи мы рассмотрим только эти.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/term-characteristics@2x.png" width="430" height="512" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Характеристика должна поддерживать хотя-бы одну из них, но может поддерживать и все вместе. Интересным тут является тип notify. Как не сложно догадаться, вместо прямого считывания, у нас есть возможность получить уведомление когда устройство самостоятельно решит отправить нам данные, например, когда они изменились. Возвращаясь к примеру с термометром, характеристика с единицей измерения будет иметь флаг read и write, а сама температура read и notify. Единицу измерения достаточно считать при подключении, а вот температуру считывать по таймеру — не лучшее решение. Мы же пытаемся быть Low Energy — постоянно опрашивать устройство может быть накладно как для нас, так и для периферийного устройства.&lt;/p&gt;
&lt;p&gt;Для работы с характеристиками используется три основных метода:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518759-readvalue"&gt;readValue(for:&lt;/a&gt;)&lt;/samp&gt;&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518747-writevalue"&gt;writeValue(_, for:, type:&lt;/a&gt;)&lt;/samp&gt;&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518949-setnotifyvalue"&gt;setNotifyValue(_, for: &lt;/a&gt;)&lt;/samp&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;С чтением данных всё просто: дернули метод &lt;samp&gt;readValue&lt;/samp&gt; — отправиться запрос на устройство. Ответ получим в делегате &lt;samp&gt;CBPeripheralDelegate&lt;/samp&gt; когда данные придут. Тоже самое произойдет, если данные обновятся и мы будем подписаны на них с помощью &lt;samp&gt;setNotifyValue&lt;/samp&gt;&lt;/p&gt;
&lt;p&gt;Запись же бывает двух видов —  &lt;samp&gt;withResponse&lt;/samp&gt; и &lt;samp&gt;withoutResponse&lt;/samp&gt;. Не вдаваясь в подробности, это запись с и без подтверждения. Конечно, &lt;samp&gt;withResponse&lt;/samp&gt; медленнее, т.к. на каждый запрос записи будет генерироваться ответ. Это стоит учитывать особенно, для передачи относительно больших объемов данных — запись &lt;samp&gt;withoutResponse&lt;/samp&gt; будет предпочтительнее в этом случае.&lt;/p&gt;
&lt;h3&gt;Ограничения&lt;/h3&gt;
&lt;p&gt;Кстати, а что по лимитам? Не забываем что в названии маячит LE, что значит что он был спроектирован с максимальной экономией на всём, в том числе и размерах пакетов. BLE — это совсем не про стриминг и большие объемы данных. Скорость по стандарту 4.0 согласно Википедии всего 0,27 Мбит/сек.&lt;/p&gt;
&lt;p&gt;По умолчанию, размер пакета с полезной информацией равен &lt;b&gt;23 байта&lt;/b&gt;, из которых 3 зарезервированных системой. Остаётся &lt;b&gt;20 байт&lt;/b&gt; полезной нагрузки. Работая с BLE вам придется столкнуться с этим ограничением и придумывать как делить ваши данные на пакеты вручную, т.к. очень часто 20 байт оказыватся недостаточно. В ряде случаев, когда обмен информацией будет происходить с современным смартфоном, данное ограничение может быть увеличено вплоть до 512 байт. CoreBluetooth берет на себя ответственность за переговоры о максимальном MTU, ровно как и на автоматическое деление на пакеты, если это поддерживается устройством. Если вам нужна другая скорость или объемы, то вероятно вам нужно использовать L2CAP канал и работать более низкоуровнево, минуя GATT, благо такая возможность есть с iOS 11 (только для устройств Apple).&lt;/p&gt;
&lt;h3&gt;Серверная часть&lt;/h3&gt;
&lt;p&gt;На данном этапе вы уже познакомились со всеми ключевыми классами и понятиями CoreBluetooth и скорее всего уже сможете написать своё первое приложение которое будет подключаться к BLE устройству. А что если этим устройством будет другой айфон? Давайте в кратце рассмотрим обратную сторону, сторону сервера или в нашем случае, Peripheral.&lt;/p&gt;
&lt;p&gt;По аналогии с &lt;samp&gt;CBCentralManager&lt;/samp&gt;, для создания сервера у нас есть &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager"&gt;CBPeripheralManager&lt;/a&gt;&lt;/samp&gt; и его &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheralmanagerdelegate"&gt;CBPeripheralManagerDelegate&lt;/a&gt;&lt;/samp&gt;. Для создания нашего сервиса нам нужно будет в первую очередь создать сервис и его характеристики. Пусть у нас будет только один сервис с единственной характеристикой доступной только для чтения. Ответная часть CBService на стороне сервера — это &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbmutableservice"&gt;CBMutableService&lt;/a&gt;&lt;/samp&gt;. Да, API CoreBluetooth не «освифтили», поэтому у нас есть префикс Mutual для мутабельных версий классов. При создании нам нужно указать UUID, который можно сгенерировать в консоле командой &lt;samp&gt;uuidgen&lt;/samp&gt; и указать основной ли это сервис у нашего устройства.&lt;/p&gt;
&lt;p&gt;После создания сервиса его нужно наполнить характеристиками. Их представляет класс &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbmutablecharacteristic"&gt;CBMutableCharacteristic&lt;/a&gt;&lt;/samp&gt;. При его создании указывается&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UUID&lt;/li&gt;
&lt;li&gt;Свойства&lt;/li&gt;
&lt;li&gt;Начальное значение (для статичных характеристик)&lt;/li&gt;
&lt;li&gt;Права доступа&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Собираем наше крохотное дерево с одним листочком, добавляем в менеджер и можем заявить о себе путем рассылки рекламных пакетов. Для этого у &lt;samp&gt;CBPeripheralManager&lt;/samp&gt; есть метод &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393252-startadvertising/"&gt;startAdvertising&lt;/a&gt;&lt;/samp&gt;. На вход он принимает словарь с параметрами рассылки. Я не хочу углубляться сейчас в детали структуры рекламного пакета, но кое-что хотел бы подсветить.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝🏻&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;В начале рассказа о &lt;samp&gt;CBCentral&lt;/samp&gt; я намеренно упустил момент, что при сканировании эфира можно указать UUID сервисов в которых мы заинтересованы. Согласитесь, было бы глупо и долго сканировать эфир, и потом подключаться ко всем найденым устройствам просто чтобы узнать есть ли у них искомый сервис или нет. Вместо этого сервер может добавить в рекламный пакет UUID наших главных сервисов, а клиенты при сканировании смогут фильтровать устройства которые им не интересны без подключения и считывания списка сервисов. Сделать это мы можем как раз указав соотвествующий ключ в упомянутый ранее словарь на начале Advertising`а.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Как только начнется рассылка пакетов, мы получим соответсвующее уведомление в делегат. И как только к нам подключиться клиент... Ничего не произойдет. У делегата менеджера нет соответствующего метода. Вместо этого, у нас есть методы для реакции на события чтения, записи и подписки на характеристики. Используя эти методы мы можем решить готовы ли мы сейчас предоставить или записать значение, провалидировать его и так далее.&lt;/p&gt;
&lt;h3&gt;Состояния&lt;/h3&gt;
&lt;p&gt;До сих пор я ничего не говорил о ситуациях когда Bluetooth недоступен или выключен. Состояние менеджера очень важно отслеживать и выполнять операции только в разрешенных состояниях, иначе не избежать крешей. Менеджер, будь то Central или Peripheral может находиться в следующих состояниях:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unknown&lt;/li&gt;
&lt;li&gt;resetting — BLE стек перезагружается&lt;/li&gt;
&lt;li&gt;unsupported — BLE не поддерживается&lt;/li&gt;
&lt;li&gt;unauthorized — Пользователь отклонил передача прав&lt;/li&gt;
&lt;li&gt;poweredOff — Bluetooth выключен&lt;/li&gt;
&lt;li&gt;poweredOn — Bluetooth включен, можно работать&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Начинать поиск устройств, рассылать Advertisement пакеты и даже добавлять сервисы в менеджер можно только в &lt;samp&gt;poweredOn&lt;/samp&gt; состоянии, поэтому обделить вниманием метод этот метод делегата не получиться, на это нам намекает и сам протокол делегата менеджера — метод для отслеживания состояния единственный помечен как обязательный.&lt;/p&gt;
&lt;h3&gt;Безопасность&lt;/h3&gt;
&lt;p&gt;Как уже упоминалось не раз, в BLE вопрос энергоэффективности всегда ставился в первый  ряд. Поэтому, по-умолчанию весь трафик между устройствами не шифруется. Экономятся и ресурсы ЦП и байты в пакетах. Такой трафик может быть легко перехвачен или даже подменен злоумышленником. Для большинства прикладных задач, которые инжинеры решают с помощью BLE, такой вариант вполне подходит. Ведь нет ничего страшного, если кто-то перехватит температуру вашего термометра или заряд батареи. Но есть и случаи, когда важно защитить приватные данные от чтения злоумышленником или быть уверенным что запись происходит от доверенного источника.&lt;/p&gt;
&lt;p&gt;В случае, когда требуется дополнительная защита, можно установить соотвествующие права на характеристику. При попытке чтения или записи такой характиристики сервер отклонит процесс, сообщив, что общение должно быть зашифровано, ответив на запрос ошибкой Insufficient Authetication. Чтобы продолжить, клиенту нужно создать защищенное соединение, которые мы чаще привыкли называть пейрингом, реже — бондингом.&lt;/p&gt;
&lt;p&gt;Процесс пейринга может быть различен для разных устройств, кто-то требует ввода константных чисел, кто-то показывает цифры на экране обоих устройств или просто просит подтвердить соответствие. В результате пейринга будут сгенерированы ключи на обоих сторонах процесса и общение будет зашифровано.&lt;/p&gt;
&lt;p&gt;После удачного пейринга, нужно заново подключиться к устройству и перечитать характеристику. К счастью, CB берет весь этот процесс на себя, скрывая под капот все эти сложные действия. То есть, если вы попытаетесь считать или записать защищенную хар-ку, то CoreBluetooth провернет все эти действия и вы как ни в чем ни бывало получите ответ в делегате, только чуть позже чем обычно.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/pairing@2x.png" width="1107" height="448" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h4&gt;Пермишенны&lt;/h4&gt;
&lt;p&gt;Говоря о безопасности, стоит упомянуть о том, что с недавних пор Apple требует указания в Info.plist текста с объяснением для конечного пользователя для чего вашему приложению нужен доступ к Bluetooth. При первой попытке сканирования или начала отправки рекламных пакетов, система спросит пользователя разрешает ли он использовать Bluetooth, точно так же, как это обычно происходит при запросе прав на фотогалерею или пуш уведомления. У соседей по платформе, в Андроид, система также требует прав на локацию низкой точности, т.к. потенциально можно сканирование можно использовать для определения положения пользователя. Возможно, такие правила не за горами и в iOS.&lt;/p&gt;
&lt;h3&gt;Переподключение&lt;/h3&gt;
&lt;p&gt;При работе с Bluetooth, как и с любой другой технологией связи, случаются разрывы соединения. Разрыв может быть вызван как по инициативе одной из сторон, так и при потере сигнала. Фреймворк CoreBluetooth устроен таким образом, что пытается постоянно поддерживать соединение до тех пор, пока не будет вызван метод  &lt;samp&gt;cancelConnection(from:)&lt;/samp&gt;. Таким образом, если связь прервалась по причине неустойчивого соединения, или же при выключении удаленного устройства, CoreBluetooth будет пытаться восстановить соединение в фоновом режиме пока запущено ваше приложение. Он самостоятельно будет сканировать эфир, и как только появиться возможность соединиться сделает это. Кажется довольно простой вещью, но поверьте — вам не захочется писать этот алгоритм с нуля. Коллеги по платформе, опять же не имеют такого механизма из коробки и команде iOS сэкономил несколько дней обсуждений и имплементации.&lt;/p&gt;
&lt;p&gt;В примере выше рассмотрели кейс, где дисконнект был неожиданным для нас явлением. Но стоит рассмотреть переподключение как естественный процесс жизненного цикла. Допустим, поработав с устройством пару минут, мы узнали от него всё что хотели на данный момент, и не хотим больше тратить ресурсы на поддержание соединения. В этом случае мы можем сохранить уникальный UUID переферийного устройства и вызвать метод &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518952-cancelperipheralconnection/"&gt;cancelPeripheralConnection(&lt;/a&gt;)&lt;/samp&gt;. Позже, когда нам снова потребуются какие-то данные от утройства, мы можем использовать сохраненный UUID для подключения. Подсвечу тот факт, что метод connect принимает на вход параметр экземпляр класса CBPeripheral, который не имеет публичного конструктора. Для получения экземплара у нас есть метод   &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1519127-retrieveperipherals/"&gt;retrievePeripherals(withIdentifiers:&lt;/a&gt;)&lt;/samp&gt; у &lt;samp&gt;CBCentralManager&lt;/samp&gt;, которому можно скормить массив UUID.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;⚠️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Важно, что метод работет уже не CBUUID, а с UUID из Foundation библиотеки, т.к. это уже платформенная реализация и не имеет референса в спецификации CoreBluetooth. После маппинга UUID в массив CBPerepheral можно к ним подключаться.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Тут может образоваться вопрос, как быть, если искомое устройство не в зоне видимости Bluetooth или же ушло в сон на какое-то время. В этом случае... ничего не произойдет. Мы не получим ошибки таймаута или какой-то другой. CoreBluetooth будет пытаться подключиться к устройству бесконечно, пока вы не вызовете &lt;samp&gt;cancelPeripheralConnection(:)&lt;/samp&gt;. Такая логика может показаться странной, действительно, зачем мне бесконечно подключаться к термометру, если он не в зоне видимости? Но не забываем, что сценариве использования очень много, и мы работаем с LE его версией. Это значит, что какие-то устройства by design могут выходить в эфир на довольно редкие промежутки времени для экономии батареи. Тогда такое поведение приобретает смысл. В конечном итоге, накинуть логику с таймаутом намного легче, чем написать цепочку переподключения, будь этот таймаут системным.&lt;/p&gt;
&lt;h3&gt;Работа в бекграунде&lt;/h3&gt;
&lt;p&gt;Как и все ресурсоёмкие операции в iOS работа с устройствами в бекграунде по умолчанию недоступна по умолчанию. При сворачивании приложения, будь ваше приложение сервером или клиентом потеряет соединение и как и все остальные перестанет обслуживать все очереди. Но по возращению в форграунд, CoreBluetooth восстановит соединение.&lt;/p&gt;
&lt;p&gt;Автоматическое восстановление соединения это конечно хорошо, но явно недостаточно. Полноценно контролировать процесс взаимодействия с устройствами в фоне можно проставив соответствующий бекраунд мод в Info.plist.&lt;/p&gt;
&lt;p&gt;Для каждой роли, сервера и клиента, есть отдельный чекбокс. Можно отметить оба. При активации бекграунд мода, приложение сможет взаимодействовать с устройствами, читать и писать характеристики, сканировать сеть и т.д., но конечно же процесс этот не вечный и как это происходит с почти с любым приложением, рано или поздно iOS может выгрузить из памяти приложение. С этим ничего не поделать и придется с этим жить. Хорошая новость в том, что по возвращению в приложение, в CoreBluetooth есть возможность восстановить состояние, со всеми подключенными устройствами, найденными характеристиками и т.д. Всё выше сказанное распространяется на случаи, когда приложение было выгружено по просьбе системы, а не закрыто пользователем из таск-менеджера.&lt;/p&gt;
&lt;p&gt;Еще раз напомню, что Apple серьезно позаботилась об энергопотреблении, поэтому процессы в бекграунде будут медленнее, например, сканирование будет происходить дискретно и менее интенсивно.&lt;/p&gt;
&lt;p&gt;Важно отметить, что при реализации соединения между двумя iPhone, iPad или Mac без соответствующего бекграунд-мода, у серверной части уход в бекграунд будет сопровождаться отключением зарегистрированных сервисов, а не завершением Bluetooth соединения. То есть удаленная сторона (клиент) не получит события отключения, но получит уведомление &lt;samp&gt;didModifyServices&lt;/samp&gt; о том, что часть сервисов у Peripheral устройства пропало. Помните, в этом случае вы подключены к одному физическому устройству — другому телефону, планшету или компьютеру, а не программе как в случае HTTP взаимодействия.&lt;/p&gt;
&lt;h3&gt;L2CAP соединение&lt;/h3&gt;
&lt;p&gt;В 2017 году Apple представила разработчикам возможность коммуницировать с Apple устройствами не только через GATT протокол, но и открывать своё низкоуровневое соединение через низлежащий протокол L2CAP. Это позволяет написать свою реализацию взаимодействия по радиоканалу, минуя ограничения характеристик GATT. Это, конечно, сложнее в реализации, чем читать и записывать характеристики, но всё равно достаточно просто.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/l2cap@2x.png" width="406" height="427" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Со стороны сервера, мы всё так же, должны просканировать эфир, найти и подключиться к устройству. Если устройство поддерживает такой тип соединения, оно будет содержать специальный сервис с характеристикой содержащей ID канала. Для начала обмена информацией нужно считать этот ID и попросить CBPeripheral открыть канал методом &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral/2880151-openl2capchannel/"&gt;openL2CAPChannel(&lt;/a&gt;)&lt;/samp&gt;.&lt;/p&gt;
&lt;p&gt;Если вы реализуете серверную часть, то вам нужно заявить о том, что вы поддерживаете L2CAP. Для этого, в соотвествующем менеджере есть метод &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/2880160-publishl2capchannelwithencryptio/"&gt;publishL2CAPChannelWithEncryption(&lt;/a&gt;)&lt;/samp&gt; который подготовит канал, сервис и характеристику и автоматически опубликует нужные данные.&lt;/p&gt;
&lt;p&gt;После успешного соделинения, делегаты соотвествующих сторон процесс получат уведомление &lt;samp&gt;didOpenL2CAPChannel&lt;/samp&gt; с указанием канала. Сам канал имеет два свойства &lt;samp&gt;inputStream&lt;/samp&gt; и &lt;samp&gt;outputStream&lt;/samp&gt; которые позволят реализовать и чтение и запись данных по каналу используя стандартную для iOS / Mac абстракцию потоков.&lt;/p&gt;
&lt;h3&gt;Грабли&lt;/h3&gt;
&lt;p&gt;Мы уже очень близко к завершению моего повествования. В конце я бы хотел кратко осветить те грабли, на которые пришлось наступить при работе с CL.&lt;/p&gt;
&lt;h4&gt;Кеширование&lt;/h4&gt;
&lt;p&gt;Разрабатывая и отлаживая приложение с BLE всегда помните о кешировании над которым вы не властны. CoreBluetooth попытается закешировать и сервисы и характеристики для вашего устройства. Помимо этого, кешируется мета-информация, например имя устройства.&lt;/p&gt;
&lt;p&gt;На одном из проектов, потребовалось написать симулятор железного устройтва, чтобы QA могли тестировать взаимодействие на всякие краевые кейсы, которые с реальным девайсом не воспроизвести, или воспроизведение было кране долгим. При написании серверной части, мы можем изменить имя нашего устройства в рекламном пакете и надеятся что мы увидим измененное имя. Если бы мы были единственными пользователями Bluetooth на устройстве, всё бы было замечательно: указали имя в реалмном пакете и именно его увидит клиент. Но Bluetooth на устройстве использует и ОС, часто отправляя рекламные пакеты с именем устройства из системных настроек. Например для быстрого обнаружения устройства сервисом AirDrop. Если устройство, где вы планируете тестировать вашу клиентскую часть уже поймала хоть один рекламный пакет, то имя закешируется и при попытке чтения имени CBPeripheral мы увидим именно это закешированное имя, а не то, которое указали в настройках рекламного пакета.&lt;/p&gt;
&lt;p&gt;Тоже самое может коснуться и набора сервисов и характеристик для спейреных устройтв. Если вы добавите сервис после установки соединения, вполне вероятно что клиент его не может не увидеть.&lt;/p&gt;
&lt;h4&gt;Версионирование&lt;/h4&gt;
&lt;p&gt;Очень короткая “грабля” где я призываю вас предусмотреть характеристику с версией ПО. Если вы работаете с отдельным устройством такой привелегии может и не быть, но если реализуете клиент-серверное взаимодействие между Apple устройствами, то не поленитесь это сделать. Это уменьшит количество костылей в коде, когда вы будете догадываться о версии устройства или ПО по наличию/отсутствию того или иного сервиса или характеристики. Такое часто приходиться делать чтобы определить поддерживаемые функции на периферийном устройстве.&lt;/p&gt;
&lt;h4&gt;Хардварные проблемы&lt;/h4&gt;
&lt;p&gt;Готовьтесь что всё пойдет не так, как вы предполагали. Разработчики железа — тоже разработчики, у которых тоже случаются и баги и креши, но только релизный цикл обычно у них намного длиньше, особенно, когда у вас ограничены коммуникации с ними. Будьте готовы, что кто-то отойдет от стандартов. Например, не будет включать в рекламный пакет информацию о сервисах.&lt;/p&gt;
&lt;p&gt;Так, я в самом начале моего пути на проекте с BLE я столкнулся с такой проблемой. Написал и отладил клиентскую сторону по документации, проверил с написанным симулятором, все отлично работает. Когда через месяц пришел реальный девайс — его просто нельзя было найти. Я вижу что он есть в эфире, а уведомления в делегат я не вижу. Несолько дней переписок и по счастливой случайности я методом перебора понимаю, что если отключить фильтрацию по UUID сервисов всё работает. Пришлось фильтровать по имени, а имя иногда менялось, и тут вспоминаем проблему с кешированием. Короче, весело.&lt;/p&gt;
&lt;p&gt;Как я уже говорил в начале, устройство может и крешиться и зависать. К счастью, на физическом железном устройстве почти всегда есть watchdog который их перезагрузит, но понять что идет не так, может быть достаточно трудно. Как я уже говорил, там сидят такие же разработчики, и в такой низкоуровневой системе запросто можно поймать какой-нибудь рейскондишен. Я несколько раз ловил неожиданные перезагрузки устройства при чтении характеристик, когда для них нет данных, или когда они есть, но считывать их предполагалось в другой последовательности.&lt;/p&gt;
</description>
</item>

<item>
<title>Устройство многопоточности в iOS</title>
<guid isPermaLink="false">4</guid>
<link>https://sidorov.tech/all/ustroystvo-mnogopotochnosti-v-ios/</link>
<pubDate>Mon, 16 Nov 2020 11:23:29 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/ustroystvo-mnogopotochnosti-v-ios/</comments>
<description>
&lt;p&gt;В 2000 году Apple выпустила открытую unix-like ОС &lt;a href="https://ru.wikipedia.org/wiki/Darwin"&gt;Darwin&lt;/a&gt;, которая уже в следующем году послужит базой для первой версии Mac OS X — 10.0,  которая, в свою очередь, в будущем станет прародителем всех операционных систем Apple, начиная от современных macOS и iOS, заканчивая watchOS в часах и audioOS в «умных» колонках.&lt;/p&gt;
&lt;p&gt;&lt;cut/&gt;&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;🎓&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Эта статья написана по мотивам сессии «&lt;a href="https://youtube.com/watch?v=GVXyrLB1tbk"&gt;Устройство многопоточности в iOS&lt;/a&gt;» проходившей в рамках третьего сезона &lt;a href="https://podlodka.io/crew"&gt;Podlodka iOS Crew&lt;/a&gt;, спикером которой был &lt;a href="https://twitter.com/looneyconey"&gt;Александр Андрюхин&lt;/a&gt;. Я позволили себе изменить последовательность повествования, а так же более полно раскрыть некоторые темы, а некоторые вовсе убрать.&lt;/p&gt;
&lt;p&gt;В любом случае, статья не будет заменой видео, а видео — статье. &lt;a href="https://podlodka.io/ioscrew"&gt;Залетайте на остаток сезона&lt;/a&gt; — впереди целая неделя живых докладов, посвященых скиллам, которые нужны, чтобы сделать из обычного приложения крутой продукт! Приятным бонусом будет доступ к видео с сессиями первой недели про многопоточность.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Darwin построен на &lt;a href="https://ru.wikipedia.org/wiki/XNU"&gt;XNU&lt;/a&gt; — гибридном ядре, включающим в себя микроядро &lt;a href="https://ru.wikipedia.org/wiki/Mach"&gt;Mach&lt;/a&gt; и некоторые части ОС семейства BSD. В контексте этой заметке нам важно что Darwin получил от BSD модель процессов unix и модель тредов &lt;a href="https://ru.wikipedia.org/wiki/POSIX"&gt;POSIX&lt;/a&gt;, а от Mach — слегка переосмысленное предствление процессов как задач.&lt;/p&gt;
&lt;p&gt;Для работы с потоками ОС разработчикам доступна С библиотека &lt;a href="https://ru.wikipedia.org/wiki/POSIX_Threads"&gt;pthread&lt;/a&gt; — первый слой абстракции в наших операционных системах. Несмотря на то, что использовать её в своём коде можно и по сей день, Apple никогда не рекомендовала использовать pthread напрямую. Уже с первых версий версий Mac OS, разработчикам была доступна абстракция Apple поверх phread — &lt;a href="https://developer.apple.com/documentation/foundation/thread"&gt;NSThread&lt;/a&gt;.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Runloop@2x.png" width="765" height="333" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;NSThread — второй слой абстракции, которая нам заботливо предоставила Apple. Помимо более привычного Objective-C синтаксиса для создания и управления потоками, корпорация предоставила &lt;a href="https://developer.apple.com/documentation/foundation/runloop"&gt;RunLoop&lt;/a&gt; — цикл обслуживания задач и событий. RunLoop использует инструменты микроядра Mach (порты, XPC, ивенты и задачи) для управления потоком POSIX и может перевести поток в режим сна, если ему нечего делать и пробудить, когда появиться работа.&lt;/p&gt;
&lt;p&gt;Конечно, пользоваться NSThread можно и по сей день, но стоит помнить, что создание потока — дорогая операция, т.к. сам поток, мы должны запросить у самой ОС. Кроме того, синхронизация потоков и доступа к ресурсам несколько неудобна для повседневной разработки, поэтому разработчики Apple задумались о решении проблемы удобства работы с асинхронным кодом.&lt;/p&gt;
&lt;h2&gt;Grand Central Dispatch&lt;/h2&gt;
&lt;p&gt;С выходом iOS 4, Apple подняла разработчиков выше еще на один уровень абстракции и представила &lt;a href="https://developer.apple.com/documentation/DISPATCH"&gt;Grand Central Dispatch&lt;/a&gt; и вводит понятие очередей и задач для организации асинхронного кода. GCD — это высокоуровневый API, позволяющее создавать пользовательские очереди, управлять задачами в них, решать вопросы синхронизации и делать это максимально эффективно.&lt;/p&gt;
&lt;p&gt;Т.к. очереди — это всего лишь слой абстракции, под капотом они используют всё те же системные треды, но механизм их создания и использования оптимизирован. CGD имеет пул предсозданных потоков и распределяет задачи эффективно, максимально утилизируя процессор если это необходимо. Разработчикам более не нужно думать о самих потоках, их создании и управлении.&lt;/p&gt;
&lt;p&gt;Помимо создании новой очереди вручную, GCD предоставляет доступ к главной очереди, на которой работает UI и доступ к нескольким системным (глобальным) очередям.&lt;/p&gt;
&lt;p&gt;Очереди GCD бывают двух типов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;serial — последовательные&lt;/li&gt;
&lt;li&gt;concurrent — параллельные&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Concurent@2x.png" width="809" height="393" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Не сложно догадаться, что на serial очереди задачи будут выполняться последовательно, друг за другом, а на параллельной будут выполнятся одновременно. По умолчанию очередь создаётся с последовательными выполнением задач, а чтобы создать concurrent очередь, необходимо явно это указать&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;let queue = DispatchQueue(&amp;quot;com.company.name.app&amp;quot;, attributes: .concurrent)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Как уже было сказано, GCD предоставляет уже созданные, глобальные очереди, которые отличаются приоритетом:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .userInteractive)&lt;/samp&gt; — Для заданий, которые взаимодействуют с пользователем в данный момент и занимают очень мало времени.&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .userInitiated)&lt;/samp&gt; — Для заданий, которые инициируются пользователем и требуют обратной связи.&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .utility)&lt;/samp&gt; — Для заданий, которые требуют некоторого времени для выполнения и не требуют немедленной обратной связи.&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;global(qos: .background)&lt;/samp&gt; — Для заданий, не связанных с визуализацией и не критичных ко времени исполнения.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ Все глобальные очереди — очереди с параллельным выполнением задач.&lt;/p&gt;
&lt;h2&gt;Постановка задачи в очередь&lt;/h2&gt;
&lt;p&gt;Задачи в любою очередь, параллельную или последовательную, могут быть поставлены синхронно и асинхронно. При асинхронной постановке задачи в очередь, код, следующий за постановкой задачи в очередь, продолжит выполняться.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Async@2x.png" width="809" height="478" alt="" /&gt;
&lt;/div&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;...
DispatchQueue.global().async {
   processImage()
}
doRequest() // выполнется сразу, не дожидаясь processImage()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;А в случае с синхронной постановкой, код, следующей за ней, не продолжит своё выполнение, пока не будет выполнена поставленная в очередь задача.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;...
DispatchQueue.global().sync {
   processImage()
}
doRequest() // будет ждать processImage()&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;DispatchWorkItem&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/dispatch/dispatchworkitem"&gt;DispatchWorkItem&lt;/a&gt; — специальный класс GCD, более объектно-ориентированную альтернативу замыканию (блоку) для постановки задачи в очередь. В отличии от обычной постановки задачи в очередь, DispatchWorkItem имеет возможность:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;указать приоритет задачи&lt;/li&gt;
&lt;li&gt;получить уведомление о завершении задачи&lt;/li&gt;
&lt;li&gt;отменить задачу&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Важно понимать, что отмена задания работает до момента старта задачи, то есть пока она находиться в очереди. Если GCD начал исполнять код в блоке DispatchWorkItem отмена задания не приведет ни к какому результату, код продолжит своё выполнение.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;NSOperation&lt;/h2&gt;
&lt;p&gt;GCD представляет удобную абстракцию для написания асинхронного кода, как с точки зрения идеи, так и с точки зрения синтаксиса. Но так было не всегда. В Objective-C (да и в первых версиях Swift) оперирование очередями и задачами было не таким удобным как в современном Swift да и вообще шло вразрез самому названию языка программирования. Apple необходимо было предоставить объектно ориентированную альтернативу GCD, и она это сделала представив &lt;a href="https://developer.apple.com/documentation/foundation/operation"&gt;NSOperation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;NSOperations — это по сути те же очереди, вместо DispatchQueue здесь OperationQueue, а вместо DispatchWorkItem — Operation. Но помимо ООПшного синтаксиса API Operations предоставляет два крутых перимущества:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Возможность указывать максимальное кол-во выполняемых одновременно задач в очереди.&lt;/li&gt;
&lt;li&gt;Указывать зависимые операции, выстраивая таким образом иерархию операций. В таком случае операция пойдет на выполнение только тогда, когда все операции, на которых она зависит, завершатся.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Последний пункт очень удобен для построения цепочки запросов, когда для выполнения одного запроса нужна информация от нескольких других.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;class CustomOperation: Operation {
    var outputValue: Int?

    var inputValue: Int {
        return dependencies
            .filter({ $0 is CustomOperation })
            .first as? CustomOperation
            .outputValue ?? 0
    }
    ...
}

let operation1 = CustomOperation()
operation1.start()

let operation2 = CustomOperation()
operation2.addDependency(operation1)
operation2.start()&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Ошибки и проблемы&lt;/h2&gt;
&lt;p&gt;Говоря об асинхронности и многопоточности, нельзя обойти тему основных основных ошибок, которые разрабочики могут совершить при написании кода, выполняющегося параллельно.&lt;/p&gt;
&lt;h2&gt;Race Condition или состояние гонки&lt;/h2&gt;
&lt;p&gt;Race Condition или состояние гонки — это ошибка проектирования многопоточных систем, при которой доступ к ресурсу не синхронизирован и результат выполнения может зависеть от последовательности выполнения кода. Определение сложно понять без примера, поэтому лучше разобраться опираясь на какой-то пример.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Racecodition@2x.png" width="809" height="296" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;На иллюстрации изображено два потока, в которых происходит увеличение какого-то кода. Чтобы инкрементировать значение переменной, процессору нужно будет совершить три действия:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;считать значение переменной из общей памяти в регистр&lt;/li&gt;
&lt;li&gt;увеличить значение в регистре на 1&lt;/li&gt;
&lt;li&gt;записать значение из регистра обратно в общую память&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Как можно заметить на иллюстрации, считывание значения переменной вторым потом происходит до того, как первый поток успел записать увеличенное значение. В итоге теряется одно увеличение счетчика что может быть как причиной безобидного бага, так и серьезной проблемы приводящей к аварийному завершению.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;💡&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Самым серьёзным последствием Race Condition считается кейс ПО медицинсокого аппарата для лучевой терапии &lt;a href="https://ru.wikipedia.org/wiki/Therac-25"&gt;Therac-25&lt;/a&gt;. Состояние гонки приводило к неправильным значениям в переменой, используемой для определения режима работы лучевого механизма.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Deadlock&lt;/h2&gt;
&lt;p&gt;Deadlock или взаимная блокировка — ошибка многопоточного ПО, при которой несколько потоков могут взаимно ожидать освобождения некоторого ресурса бесконечное время.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Deadlock@2x.png" width="809" height="311" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Разберем на примере. Допустим задача, в потоке А блокирует доступ к некому ресурсу А, пусть это будет некий SettingsStorage. Задача А блокирует доступ к стораджу, читает оттуда некоторые значения, и что-то вычисляет. В это время стартует задача B и блокирует доступ к ресурсу B, пусть это будет база данных. Чтобы выполнить некоторые вычисления задаче В тоже потребовался доступ к SettingsStorage и задача начинает ждать, когда задача А его освободит. В это время, задаче А понадобился доступ к БД, но он уже заблокирован задачей В. Происходит взаимна блокировка: задача А ожидает БД, которая заблокирована задачей В, которая ожидает сторадж, заблокированный задачей А.&lt;/p&gt;
&lt;h2&gt;Инверсия приоритетов&lt;/h2&gt;
&lt;p&gt;Инверсия приоритетов — ошибка, приводящая к смене приоритетов у потоков, которой не предполагался разработчиком.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/PriorityInversion@2x.png" width="809" height="311" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Пусть у нас есть всего две задачи с разным приоритетом и всего 1 ресурс, пусть это снова будет БД. Первым в очередь помещается задача с низким приоритетом. Она выполняет свою работу и в момент времени Т1 ей понадобилась БД и она блокирует доступ к ней. Почти сразу же после этого, стартует высокоприоритетная задача и вытесняет низкоприоритетную. Все идет по плану до момента Т3, где высокоприоритетная задача пытается завладеть БД. Так как ресурс заблокирован, задача высокоприоритетная задача переводится в ожидание, а низкоприоритетная получает процессорное время. Временной промежуток T3-T4 называют ограниченной инверсией приоритетов. В этом промежутке наблюдается логическое несоответствие с правилами планирования — задача с более высоким приоритетом находится в ожидании в то время как низкоприоритетная задача выполняется.&lt;/p&gt;
&lt;div class="e2-text-video"&gt;
&lt;iframe src="https://www.youtube.com/embed/GVXyrLB1tbk" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
</description>
</item>

<item>
<title>Копаем внутрь SpringBoard</title>
<guid isPermaLink="false">3</guid>
<link>https://sidorov.tech/all/kopaem-vnutr-springboard/</link>
<pubDate>Sun, 15 Nov 2020 17:29:35 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/kopaem-vnutr-springboard/</comments>
<description>
&lt;p&gt;Я, наверное как и многие iOS и macOS разработчики, каждый год жду WWDC чтобы увидеть новые API, новые инструменты и улучшения существующих. Но помимо всего, связанного с разработкой, я жду саму ОС — хочу увидеть что для меня, как для обычного пользователя ОС, изменилось.&lt;/p&gt;
&lt;p&gt;В этом году произошло два больших изменения домашнего экрана которые у всех на слуху: добавили возможность размещать виджеты и представили библиотеку приложений. В первый же день я удалил весь хлам, бережливо расфасованный по папочкам «Other», «Utility» с домашнего экрана, оставив только то, чем пользуюсь каждый несколько раз в день на регулярной основе. Я уже давно не ищу глазами иконку нужного приложения, а пользуюсь поиском Spotlight, поэтому Apps Library стала для меня фишкой номер один в этом релизе.&lt;/p&gt;
&lt;p&gt;Помимо удобного для меня механизма организации и поиска приложений, я обратил внимание на верхний бар со строкой поиска. Мой взгляд зацепился за блюр который, который я ранее не встречал в системе. Это градиентный блюр, радиус размытия которого плавно меняется от 0 до заданного значения. Выглядит реально круто, особенно в динамике, просто попробуйте поскройлить список! Мне стало жутко интересно, можно ли и как сделать своими руками.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/all-library-gif.gif" width="717" height="468" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Немного про блюр в iOS&lt;/h2&gt;
&lt;p&gt;Немного отвлечемся и поговорим про блюр. Несмотря на то, что Apple предоставляет разработчикам богатое API, позволяющее сделать блюр и другие эффекты для изображения, возможность делать это с живым UI существенно ограничены. Конечно всегда можно отрендерить слой, наложить на изображение эффекты и фильтры и показать это в ImageView, но как несложно догадаться, производительность такого решения будет оставлять лучшего. Всё, что остаётся — использовать дарованный с барского плеча UIVisualEffectView, который предоставляет возможность сделать эффект матового стекла с помощью блюра и поиграться с Vibrancy эффектом.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Несмотря на то, что Apple предоставляет разработчикам богатое API, позволяющее сделать блюр и другие эффекты для изображения, возможность делать это такое с живым UI существенно ограничены.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Начинаем раскопки&lt;/h2&gt;
&lt;p&gt;Итак мы поняли, что из коробки сделать ничего не получиться. Что делать, куда копать?&lt;/p&gt;
&lt;p&gt;Без джейлбрейка мы не можем подключиться к чужому процессу и сдампить иерархию вьюх чтобы покопаться и найти нужную нам вьюху. Придется искать в слепую. Благо у нас есть симулятор и возможность покопаться в некоторых системных файлах.&lt;/p&gt;
&lt;p&gt;Первым делом определимся где будем искать. Библиотека App Library находится на домашнем экране, значит нам нужно найти что-то связанное с процессом SpringBoard — приложением, которое ответственное за домашний экран, запуск приложений и всё с этим связанное. Попробуем его найти:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/search_springboard@2x.png" width="874" height="391" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Удача! Попробуем загрузить бинарник в &lt;a href="https://www.hopperapp.com"&gt;Hopper Disassembler&lt;/a&gt; чтобы посмотреть что там внутри. Надежды на то, что внутри что-то полезное мало, т.к. размер бинарника всего 140 КБайт. Дизассемблер встречает нас диалоговым окном с просьбой выбрать архитектуру, т.к. &lt;a href="https://en.wikipedia.org/wiki/Fat_binary"&gt;бинарник жирный&lt;/a&gt;.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Ага! Значит Apple, начиная с Xcode 12 поставляет «потроха» скомпилированные под классический x86_64 и под новый ARM (Apple Silicon). Сразу становится понятным почему Xcode 12 занимает так много места!&lt;/p&gt;
&lt;/div&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/hopper_sb@2x.png" width="1000" height="654" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Как и ожидалось, внутри исполняемого файла ничего полезного нет. Лишь несколько процедур отвественных за старт. Значит интересное нам прячется где-то в системных фреймворках. Посмотрим с чем слинкован исполняемый файл.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/otool@2x.png" width="1146" height="355" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Приватные фреймворки&lt;/h2&gt;
&lt;p&gt;Много анализировать не приходится, «кишки» SpringBoard’а находятся в одноименном приватном фреймворке. Вернемся вверх по дереву файлов до iOS.simruntime и поищем его относительно этой директории. А вот и папка с приватными библиотеками и нужный нам фреймворк! Грузим в дизассемблер.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/search_sym@2x.png" width="1000" height="654" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Сразу попытаемся найти какие-нибудь символы, связанные с AppLibrary. Что-то есть, но не жирно. Поиграемся с поиском попробуем найти вью контроллер этой библиотеки. Есть зацепка! По строке &lt;samp&gt;LibraryViewController&lt;/samp&gt; находится сеттеры и геттеры этого контроллера для корневого SBIconController’а (не трудно догадаться его назначение), но реализацией тут не пахнет. Если её тут нет, значит она есть где-то в другом фреймворке, другого не дано. Попробуем посмотреть с otool.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/sb_otool@2x.png.jpg" width="2560" height="1453" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Ого! Этот фреймворк использует 170 внешних зависимостей! Методом перебора не справимся, нужен другой способ. В сегменте Mach-O файлов (коим является бинарник фреймворка) есть сегмент External Symbols, который содержит список внешних символов, которые содержаться во внешних библиотеках. Возвращаемся в Hopper и попробуем найти символ используя список отфильтрованный по &lt;samp&gt;libraryviewcontroller&lt;/samp&gt;.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/objc_symb_ext_ref@2x.png" width="1000" height="654" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Вот и символ &lt;samp&gt;_OBJC_CLASS_$_SBHLibraryViewController&lt;/samp&gt;, но нажатию на который мы можем увидеть, что объявлен он в соседнем фреймворке SpringBoardHome. Грузим его в Hopper! Теперь найдем и покопаемся в классе &lt;samp&gt;SBHLibraryViewController&lt;/samp&gt;.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/blur_init@2x.png" width="1000" height="654" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Ноете что у вас MassiveViewController? Взгляните на заголовок этого класса: &lt;a href="https://gist.github.com/kei-sidorov/ed4b9969ba238e25f019eef1aad05d99"&gt;147 методов&lt;/a&gt; вам готовы насмеять в лицо вашему вьюконтроллеру:)&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Сдампить все все методы всех Objective-C классов внутри Mach-O файла поможет &lt;a href="https://github.com/nygard/class-dump"&gt;class-dump&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Ладно, первым делом глянем метод &lt;samp&gt;init&lt;/samp&gt;, переключившись в режим псевдокода — Hopper настолько хорош, что может попытаться сделать человекопонятный псевдокод из машинного.&lt;/p&gt;
&lt;h2&gt;Первая зацепка&lt;/h2&gt;
&lt;p&gt;Метод init дергает &lt;samp&gt;initWithCategoryMapProvider:&lt;/samp&gt; который по сути инициализирует родительский контроллер &lt;samp&gt;SBNestingViewController&lt;/samp&gt; через &lt;samp&gt;initWithNib:bundle:&lt;/samp&gt; и определяет очередь неких событий. В &lt;samp&gt;initWithNib:bundle:&lt;/samp&gt; передаются два nil’a что говорит о том, что инициализация вью происходит в коде. Вспоминаем жизней цикл вью контроллера (наканец-то пригодилось!) — для загрузки вью обычно используется &lt;samp&gt;loadView&lt;/samp&gt;, посмотрим что там.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/load_view@2x.png" width="683" height="548" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Ага, метод переопределен и тут вызывается &lt;samp&gt;_setupIconTableViewController&lt;/samp&gt;.&lt;/p&gt;
&lt;p&gt;Смотрим что внутри. Метод массивный, сильно углубляться не хочется. Посмотрим какие вьюхи или контроллеры там создаются. Просто поищем вхождения строки alloc.&lt;/p&gt;
&lt;p&gt;Находится три потенциальных класса &lt;samp&gt;SBHIconLibraryTableViewController&lt;/samp&gt;, &lt;samp&gt;SBHLibraryPodFolderController&lt;/samp&gt;, &lt;samp&gt;SBHLibrarySearchController&lt;/samp&gt;. Самым перспективным кажется последний, т.к. строка в топ баре, который я хочу повторить есть поиска. Увы, никаких зацепок найти не удаётся. Следующим на очереди был &lt;samp&gt;SBHLibraryPodFolderController&lt;/samp&gt;, но и там ничего интересного найти не удалось. Наверное я бы искал еще долго, если бы по счастливой случайности/запарке я не стал стирать название целиком, а стер бекспейсом только слово Controller. Оказывается, есть еще  &lt;samp&gt;SBHLibraryPodFolderView&lt;/samp&gt; у которого есть метод-геттер &lt;samp&gt;navigationBar&lt;/samp&gt;! Да! Это то, что нам нужно; похоже мы нашли нужный нам класс — &lt;samp&gt;SBHFeatherBlurNavigationBar&lt;/samp&gt;!&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/init_w_frame@2x.png" width="697" height="658" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;И почему я раньше просто не поискал по строке Blur?! Ладно, не время себя винить, глянем что внутри. Первым делом видим initWithFrame, где создается &lt;samp&gt;SBHFeatherBlurView&lt;/samp&gt; методом &lt;samp&gt;initWithRecipe:&lt;/samp&gt; с параметром &lt;samp&gt;0x2&lt;/samp&gt;.&lt;/p&gt;
&lt;p&gt;Hoper в сторону! Настало время экспериментов. Создадим новый проект в Xcode предварительно указав язык ObjectiveC. Накинем UIScrollView и какой нибудь контент для эксперимента. Не будем напрямую линковать фреймворк, загрузим его в рантайме.&lt;/p&gt;
&lt;p&gt;Далее воспользуемся возможностями рантайма ObjectiveC и создадим инстанс искомого класса. Добавим его на нашу вью и проверим верна ли наша догадка.  Специально для статьи я сделал пример похожий на оригинальный экран, чтобы было с чем сравнить. Чтож, запустим на симуляторе.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class="objc"&gt;
#import "FeatherBlurView.h"
#import &lt;dlfcn.h&gt;

#define SBH_PATH @"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/SpringBoardHome.framework/SpringBoardHome"

// Объявим интерфейс класса из SpribngBoardHome для удобной инициализации
@interface SBHFeatherBlurView : UIView
- (instancetype) initWithRecipe:(int)arg1;
@end

@implementation FeatherBlurView

+ (void)load {
    const char *path = [SBH_PATH cStringUsingEncoding:NSUTF8StringEncoding];
    dlopen(path, RTLD_NOW);
}

- (instancetype) init {
    self = [super init];
    if (self) {
        SBHFeatherBlurView *view = [NSClassFromString(@"SBHFeatherBlurView") alloc];
        view = [view initWithRecipe:0x2];
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:view];
        [[view.topAnchor constraintEqualToAnchor:self.topAnchor] setActive:YES];
        [[view.bottomAnchor constraintEqualToAnchor:self.bottomAnchor] setActive:YES];
        [[view.leftAnchor constraintEqualToAnchor:self.leftAnchor] setActive:YES];
        [[view.rightAnchor constraintEqualToAnchor:self.rightAnchor] setActive:YES];
    }
    return self;
}

@end
&lt;/code&gt;
&lt;/pre&gt;
&lt;h2&gt;Первая победа!&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/kei-sidorov/feather-blur-sample"&gt;📦 Тестовый проект доступен на GitHub&lt;/a&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/Result@2x.png" width="600" height="582" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Йоу! Всё получилось и работает как я и хотел! Можете &lt;a href="https://github.com/kei-sidorov/feather-blur-sample"&gt;скачать тестовый проект&lt;/a&gt; и поиграться самостоятельно.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;🖖&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Время подвести некоторые выводы и подчеркнуть интересные факты&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Изучать внутренности ОС не так сложно и очень интересно&lt;/li&gt;
&lt;li&gt;Знания Objective-C и жизненного цикла вью контроллера могут пригодиться в 2k20&lt;/li&gt;
&lt;li&gt;Все фреймворки и утилиты внутри Xcode поставляются в виде жирных бинарников, собранных под ARM и x86_64, это частично объясняет увеличенный почти вдвое размер Xcode&lt;/li&gt;
&lt;li&gt;Несмотря на 2020 год и актуальный Swift версии 5.4, Apple активно использует Objective-C в системных фреймворках. Даже код для управления виджетами (которые, на секунду SwiftUI-only) написан на Objective-C&lt;/li&gt;
&lt;li&gt;В Apple не бояться Massive View Controller и глубокого наследования&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Но неужели так всё просто и такой блюр можно использовать в своих проектах?! К сожалению, нет. Дело даже не в использовании приватных библиотек, что запрещает Apple, а дело в том, что у нас в принципе нет возможности использовать внешние фреймворки находясь в песочнице. То есть даже сейчас запустить на девайсе это не получиться. Но это не повод опускать руки! Уже в следующей статье мы поковыряемся во внутренностях этой вью и попытаемся воссоздать её так, чтобы код можно было запустить и на девайсе! Stay tuned!&lt;/p&gt;
&lt;h2&gt;iOS 15&lt;/h2&gt;
&lt;p&gt;&lt;samp&gt;SBHFeatherBlurView&lt;/samp&gt; был перемещен в новый фреймворк &lt;samp&gt;SpringBoardFoundation&lt;/samp&gt; и переименован в &lt;samp&gt;SBFFeatherBlurView&lt;/samp&gt;.&lt;/p&gt;
</description>
</item>

<item>
<title>Устройство UI в iOS</title>
<guid isPermaLink="false">2</guid>
<link>https://sidorov.tech/all/ustroystvo-ui-v-ios/</link>
<pubDate>Mon, 08 Jun 2020 06:04:00 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/ustroystvo-ui-v-ios/</comments>
<description>
&lt;p&gt;Как бы мы не любили UIKit, это всего лишь еще один UI фреймворк позволяющий облегчить для разработчиков процесс создания интерфейса для взаимодействия с пользователем. По сути, это I/O фреймворк для диалога с пользователем вашего приложения.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;🎓&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Эта заметка написана по мотивам сессии «Устройство UI в iOS» проходившей в рамках второго сезона &lt;a href="https://podlodka.io/crew"&gt;Podlodka iOS Crew&lt;/a&gt; спикерами которой были &lt;a href="https://t.me/a_rychkov"&gt;@a_rychkov&lt;/a&gt; и &lt;a href="https://twitter.com/antonsergeev88"&gt;@ antonsergeev88&lt;/a&gt;, которые в свою очередь вдохновлялись книжкой &lt;a href="https://books.apple.com/us/book/ios-core-animation/"&gt; iOS Core Animation Advanced Techniques&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Вы можете &lt;a href="https://podlodka.io/crew#shop"&gt;🎥 купить доступ к видео с сессиями этого сезона&lt;/a&gt; или присоедениться к новому.&lt;/p&gt;
&lt;p&gt;Ребята — топ, подлодка — топ, книжка — топ. Рекомендую!&lt;/p&gt;
&lt;/code&gt;&lt;/div&gt;&lt;h2&gt;Input&lt;/h2&gt;
&lt;p&gt;UIKit содержит в себе все необходимые компоненты для предоставления доступа к девайсам, через которые пользователь общается с вашим приложением. Это и акселероментры, и хардварные кнопки, внешние клавиатуры, специальные устройства ввода для людей с ограниченными способностями, мыши и карандаши (Apple Pencil).&lt;/p&gt;
&lt;p&gt;Несмотря на то, что UIKit содержит в себе огромное кол-во функциональности, его размер исчисляется в десятках килобайт. Причиной тому является факт, что UIKit в современном iOS это по сути &lt;a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Tasks/IncludingFrameworks.html"&gt;umbrella header&lt;/a&gt;, предоставляющий единую точку импорта.&lt;/p&gt;
&lt;p&gt;Не стоит забывать, что помимо перечисленных выше устройстов ввода, UIKit получает и обрабатывает массу информации от системы, начиная с низкоуровневых событий жизненного цикла приложения и memory warnings, заканчивая Push-уведомлениями уровнем повыше.&lt;/p&gt;
&lt;p&gt;Для того чтобы эффективно обслуживать такое большое количество входящих источников событий UIKit’у нужен Event Loop, который мы привыкли называть &lt;a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html"&gt;RunLoop&lt;/a&gt;. Тут UIKit вводит понятие главного потока, последовательного обслуживающего входящие источники и наш код. Принято считать, что главный поток это что-то неотъемлемое, что есть у приложения, но на самом деле — это абстракция, которую вводит и предоставляет именно UIKit.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/runloop@2x.png" width="720" height="313.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Может показаться, что знание RunLoop’а — это что-то хардкорное и вовсе не нужное обычным разработчикам знание, но это не так. Понимание того, как UIKit обслуживает входящие события и открисовку UI важно для некоторых оптимизаций. Например, довольно частой задачей может быть добавление таймера для некоторых целей. Опытные разработчики могли встречаться таким эффектом, что таймер работает корректно и отсчитывает время до тех пор, пока пользователь не начинит скролить таблицу. В этот момент таймер просто перестаёт работать. Дело тут вовсе не в нехватке ресурсов девайса, а в том, что все таймеры обслуживаются RunLoop’ом, который в момент скрола  переводится UIKit’ом в режим &lt;samp&gt;UI Tracking Mode&lt;/samp&gt;. В этом режиме он отдает приоритет отрисовке UI, оставляя в очереди события из некоторых источников.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Чтобы таймер работал корректно, его нужно создавать не с помощью class-метода &lt;samp&gt;Timer.scheduledTimer&lt;/samp&gt;, а обычным созданием объекта и добавлением его в RunLoop в специальном режиме &lt;samp&gt;.commonModes&lt;/samp&gt;:&lt;/p&gt;
&lt;code class="swift"&gt;let timer = Timer(timeInterval: 1.0, ...)
RunLoop.current.add(timer, forMode: .commonModes)
&lt;/code&gt;
&lt;/div&gt;&lt;h2&gt;Output&lt;/h2&gt;
&lt;p&gt;С «выходом» у UIKit все чуть попроще. Нам доступен экран и Haptic. Следуя определению UI фреймворка можно было бы возразить, что пользователь может взаимодействовать с приложением и с помощью звука, и было бы логично отнести эту часть взаимодействия тоже в UIKit. Но в силу сложности работы с аудио, разработчики Apple выделили это в отдельный фреймворк Core Audio.&lt;/p&gt;
&lt;p&gt;Основную часть времени работы над пользовательским интерфейсом разработчик, так или иначе тратит на графический интерфейс. Работая с графикой в iOS, как и на большинстве других платформ, мы имеем дело 2D пространством и прямоугольниками, которые как-то комбинируются и располагаются на экране. Сама абстракция прямоугольных областей очень удобна: с одной стороны это очень понятная схема для разработчиков, с другой стороны, очень понятная хардварной части и GPU. Работая с такими прямоугольниками перед разработчиком всегда стоит две задачи: &lt;b&gt;расположить&lt;/b&gt; эти элементы на экране и &lt;b&gt;нарисовать&lt;/b&gt; их.&lt;/p&gt;
&lt;h2&gt;Layout&lt;/h2&gt;
&lt;p&gt;Для решения первой задачи UIKit использует тривиальную и очень удобную структуру данных — дерево. Ни для кого не секрет, что view’шки можно организовывать в иерархию, бесконечно добавляя subview для subview. В конечном мы будем иметь дело с обычным деревом. Его легко и просто обходить, а использование такой структуры нам дает классную возможность верстать относительно, указывая значения координат родительского элемента, а не абсолютное значение на экране.&lt;/p&gt;
&lt;p&gt;Здесь можно возразить и сказать, что мы уже давно не верстаем на фреймах, а используем autolayout или 3rd-party библиотеки для верстки. С этим трудно не согласиться, но все системы верстки для iOS сводятся к одному — в конечном итоге они проставляют фреймы, разница только в том &lt;b&gt;когда они их считают&lt;/b&gt; и &lt;b&gt;в какой момент присваивают&lt;/b&gt; элементам.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Помимо ручного расчета абсолютных величин для фреймов или использования autolayout для верстки &lt;b&gt;существует еще и третий встроенный в iOS метод верстки&lt;/b&gt;. Если спуститься на уровень ниже от UIView касаемо отрисовки элементов, мы попадем на слой Core Animation, который позволяет c помощью свойства &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/quartzcore/calayer/1410817-anchorpoint"&gt;anchorPoint&lt;/a&gt;&lt;/samp&gt; спозиционировать элементы используя относительные координаты и указывать позицию элемента в процентах от родительской.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Drawings&lt;/h2&gt;
&lt;p&gt;Расположить прямоугольники в нужном порядке и в нужном месте — только половина дела. Теперь в них нужно еще и что-то нарисовать. Для решения этой задачи Apple разработала целый фреймворк, первоначально назвав его &lt;b&gt;LayerKit&lt;/b&gt;, а после переименовала в &lt;b&gt;Core Animation&lt;/b&gt;.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Название &lt;b&gt;Core Animation&lt;/b&gt; часто вводит в заблуждение — многие думают что этот фреймворк исключительно для создания анимаций, но это не так. Дело в том, что он ответственный вообще за любое отображение всего и вся на экране устройства, но спроектирован таким образом, что по-умолчанию любые изменения пытается анимировать. В отличии от большинства других, где нужно писать дополнительный код чтобы сделать что-то анимировано, в Core Animation нужно писать код, чтобы изменения прошли без анимации.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Core Animation предоставляет абстракцию, называемую слои. Почти любая UIView содержит в себе слой CALayer, который используется для отрисовки графического интерфейса. Слои, как и view, организуются в иерархию. Тут следует уточнить: несмотря на то, что принято считать именно UIView строительным кирпичиком UI, она по сути является фасадом для CALayer. При добавлении дочерней view, под капотом происходит добавление дочернего слоя на родительский. Все изменения &lt;samp&gt;frame&lt;/samp&gt;, &lt;samp&gt;bounds&lt;/samp&gt;, &lt;samp&gt;center&lt;/samp&gt;, &lt;samp&gt;backgroundColor&lt;/samp&gt; и многих прочих просто проксируются в CALayer.&lt;/p&gt;
&lt;div style="max-width: 720px; text-align: center;"&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/hierarchy-clone@2x.png" width="487" height="346.5" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Таким образом UIView разделяет отвественности: иерархия UIView ответственна за User Interaction, а иерархия CALayer за графическое представление.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Core Animation используется не только на iOS с UIKit для &lt;samp&gt;UIView&lt;/samp&gt;, но и на macOS с AppKit с её &lt;samp&gt;NSView&lt;/samp&gt;. В macOS система коодинат отличается от iOS: начало ее коодинат — нижний левый угол, против верхнего левого в iOS. Для кросплатформенной работы Core Animation Apple предоставляет свойство &lt;samp&gt;geometryFlipped&lt;/samp&gt; у CALayer. Система коодинат macOS является системой по умолчанию, а UIKit проставляет &lt;samp&gt;geometryFlipped = true&lt;/samp&gt; всем слоям при создании. Но возможны случае, когда созданому слою нужно будет указать значение этого свойства вручную, например, при добавлении слоёв на слой с видеоплеером.&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;Как уже говорилось ранее, Core Animation вводит понятие слоёв, из которых можно собрать визуальное представление программы. Самый базовый класс, &lt;samp&gt;CALayer&lt;/samp&gt; позволяет только закрасить себя каким-то цветом или отобразить CoreGraphics контент. Для решения более сложных задач существуют специализированные слои, такие как &lt;samp&gt;CAShapeLayer&lt;/samp&gt;, &lt;samp&gt;CATextLayer&lt;/samp&gt;, &lt;samp&gt;CAGradientLayer&lt;/samp&gt; и другие. Эти типы слоёв позволяют решить ту или иную задачу эффективным способом, проводя рисование на GPU.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;💡&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Тут стоит прояснить разницу между использованием специализированных слоёв и рисованием произвольной графики, используя метод UIView &lt;samp&gt;draw(in:)&lt;/samp&gt;. Как уже было сказано ранее, специализированные слои позволяют отрисовать контент оптимизированным способом на GPU, в то время как используя &lt;samp&gt;draw(in:)&lt;/samp&gt; разработчик будет прибегать к рисованию с помощью CoreGraphics, который работает на CPU. Такой подход может приводить к фризам UI. Конечно, CoreGraphics можно пользоваться не из главного потока (не забывая то, что он не потокобезопасный), но стоит всегда помнить что он загружает CPU.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Animations&lt;/h2&gt;
&lt;p&gt;Какие бы задачи разработчики не решал с помощью CoreAnimation, он неизбежно касается вопроса анимаций: если задать какое-либо свойство CALayer (или его специализированных наследников) и это свойство окажется анимируемым, то изменения получиться увидеть только во времени.&lt;/p&gt;
&lt;h2&gt;Implicit Animations&lt;/h2&gt;
&lt;p&gt;Так происходит, потому что CoreAnimation запускает неявные анимации, делая это автоматически, без каких-либо усилий разработчика. Чтобы понять почему так происходит, нужно сначала рассказать про CATransaction. CATransaction — это контейнер, который инкапсулирует группу анимаций, управляет их длительностью и таймингом. UIKit создает корневой CATransaction в начале каждого вращения RunLoop’а, а в конце отправляет его на рендер. Именно по этому, любое изменение свойств слоёв «упаковано» в анимацию. Довольно часто стандартная анимация может не подходить разработчику, в том случае можно создать свой CATransaction, настроить скорость и указать тайминг функцию.&lt;/p&gt;
&lt;p&gt;Описанная логика работы CALayer идет вразрез факту о том, что UIView является всего-лишь прокси для слоя. Ведь при изменении &lt;samp&gt;frame&lt;/samp&gt; у UIView его положение и размер меняются мгновенно, не анимированно, а по логике должно перекинуться на слой и тот должен санимироваться. Тут дело в том, что корневой слой UIView ссылается на этот view как на делегата. И при любом изменении свойства, слой спрашивает нужно ли ему анимировать это свойство, вызывая метод делегата &lt;a href="https://developer.apple.com/documentation/quartzcore/calayerdelegate/2097264-action"&gt;action(for:forKey:&lt;/a&gt;). View будет отвечать nil’ом на все изменения, выполняемые не в блоке анимации &lt;samp&gt;UIView.animate(...)&lt;/samp&gt;, таким образом блокируя анимации при простановки различных свойств.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;💡&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Если у слоя нет делегата, то он обратиться за анимацией к собственному словарю actions, в котором предустановлены стандартные анимации со стандартными длительностями и тайминг-функциями для различных свойств.&lt;/p&gt;
&lt;p&gt;Мы можем из кода создать дочерней слой, добавить его к основному через addSublayer() и после санимировать UIView через&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;UIView.animate(withDuration:5)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;При этом будет наблюдаться различие в анимациях: изменения на корневом слое будут длиться 5 секунд, в то время как его дочерний (созданный нами) будет анимироваться куда быстрее. Это необходимо помнить и понимать чтобы сэкономить часы на отладке.&lt;/p&gt;
&lt;p&gt;⚠️ UIView может быть делегатом только у одного слоя. Несмотря на то, что мы можем из кода задать эту view делегатом у дочернего слоя, &lt;b&gt;работать это не будет&lt;/b&gt; и очень скоро приведет к падению приложения.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Explicit&lt;/h2&gt;
&lt;p&gt;Помимо неявных анимаций, конечно же существуют и явные, которыми мы можем управлять более гибко и удобно. За создание и управление явными анимациями ответственен абстрактный класс &lt;samp&gt;CAAnimation&lt;/samp&gt;, который позволяет задать делегата анимации (для отслеживания прогресса), тайминг-функцию, длительность, значение «от», значение «до» и прочий контекст. Как уже было сказано, &lt;samp&gt;CAAnimation&lt;/samp&gt; — абстрактный класс, и его использовать нельзя. Для анимаций мы можем реализовать свою анимацию, или воспользоваться его потомками, доступными «из коробки»:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/quartzcore/cabasicanimation"&gt;CABasicAnimation&lt;/a&gt;&lt;/samp&gt; — обычная анимация, интерполирующая значение между &lt;samp&gt;fromPoint&lt;/samp&gt; и &lt;samp&gt;toPoint&lt;/samp&gt;&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/quartzcore/cakeyframeanimation"&gt;CAKeyFrameAnimation&lt;/a&gt;&lt;/samp&gt; — анимация, интерполирующая значения между двумя ключевыми кадрами, заданные с помощью массивов &lt;samp&gt;values&lt;/samp&gt; и &lt;samp&gt;keyTimes&lt;/samp&gt;&lt;/li&gt;
&lt;li&gt;&lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/quartzcore/caspringanimation"&gt;CASpringAnimation&lt;/a&gt;&lt;/samp&gt; — пружинная анимация&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Анимация будет изменять &lt;a href="https://developer.apple.com/documentation/quartzcore/calayer/1410744-presentation"&gt;&lt;samp&gt;presentationLayer&lt;/samp&gt;&lt;/a&gt; анимируемого слоя. Это копия этого слоя, которая отражает его состояние в конкретный момент времени. Если вы хоть раз применяли анимации к слою, то вероятно знаете, что пока слой анимируется, значения анимируемых свойств у слоя не меняются. Дело в том, что «спрашивая» у слоя значения свойств мы «спрашиваем» значения свойств его модели. Использование же &lt;a href="https://developer.apple.com/documentation/quartzcore/calayer/1410744-presentation"&gt;&lt;samp&gt;presentationLayer&lt;/samp&gt;&lt;/a&gt; позволяет узнать эти значения в конкретный период, такие, какие они сейчас на экране. Это может быть полезно для нескольких кейсов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Остановка анимации с сохранением текущего состояния (&lt;i&gt;просто удалив анимацию слой вернется к значениям из его модельного представления&lt;/i&gt;)&lt;/li&gt;
&lt;li&gt;Бесшовная смена анимации (&lt;i&gt;для старта новой анимации нужны значения &lt;samp&gt;fromValue&lt;/samp&gt; из presentation слоя&lt;/i&gt;)&lt;/li&gt;
&lt;li&gt;Корректная обработка нажатий на анимируемый элемент (во время анимации &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/uikit/uiview/1622469-hittest"&gt;hitTest(_:with:&lt;/a&gt;)&lt;/samp&gt; (точнее &lt;samp&gt;&lt;a href="https://developer.apple.com/documentation/uikit/uiview/1622533-point"&gt;point(inside:with:&lt;/a&gt;)&lt;/samp&gt;) будет опираться на значения фрейма из модельного представления, и чтобы верно обрабатывать нажатия, необходимо будет переопределить &lt;samp&gt;point(inside:with:)&lt;/samp&gt; для работы с презентационным слоем)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;💡&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Именно по этой причине после окончания анимации слой возвращается к исходным значениями, если не сменить у анимации значение свойства &lt;a href="https://developer.apple.com/documentation/quartzcore/caanimation/1412458-isremovedoncompletion"&gt;isRemovedOnCompletion&lt;/a&gt;. При установке этого свойства в &lt;samp&gt;false&lt;/samp&gt; конечные значения анимируемых свойств сохранятся в модельном представлении слоя.&lt;/p&gt;
&lt;/div&gt;&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Стоит помнить о том, что анимации зависят от жизненного цикла приложения и самого слоя. При уходе приложения в бекграунд или удалении слоя из &lt;i&gt;superview&lt;/i&gt; анимации &lt;samp&gt;CAAnimation&lt;/samp&gt; удаляются, поэтому свернув приложение в середине анимации, вы увидите объект в том состоянии, в котором он был до начала анимации.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Управление анимациями&lt;/h2&gt;
&lt;p&gt;Анимациями в Core Animations можно управлять. Базовым кейсом является постановка анимации на паузу и возобновление, но таких методов «из коробки» CA не предоставляет. Взамен этого, разработчикам доступно управление временем и скоростью анимаций. Установив скорость слоя в 0, мы можем поставить анимацию на паузу, вернув исходное значение — продолжить её выполнение.&lt;/p&gt;
&lt;p&gt;Скорость анимации и время в CA являются относительной величиной и зависят от скорости родителя. Время корневого слоя равно времени &lt;samp&gt;CACurrentMediaTime()&lt;/samp&gt; а для всех его дочерних элементов будет вычисляться относительно иерархии скоростей. Таким образом можно замедлить или ускорить все анимации в приложении, изменив скорость у корневого слоя — слоя UIVindow.&lt;/p&gt;
&lt;p&gt;Помимо скорости у анимации есть тайминг-функция. Эта функция определяет как прирастает виртуальное время относительно реального.&lt;/p&gt;
&lt;div style="max-width: 720px; text-align: center;"&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/time_function@2x.png" width="145.5" height="23.5" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Разработчикам доступны несколько видов тайминг-функций «из коробки».&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/time_func_visual@2x.png" width="720" height="193" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Тайминг-функции, можно создавать самостоятельно, описывая контрольные точки кривой Безье, но стоит помнить, что для тайминг-функции она может содержать только 2 перегиба, что не позволяет сделать сложные эффекты, типа пружинной анимации. И именно поэтому в CA существует отдельный класс для такой анимации.&lt;/p&gt;
</description>
</item>

<item>
<title>Продвинутая отладка в Xcode</title>
<guid isPermaLink="false">1</guid>
<link>https://sidorov.tech/all/advanced-debugging-v-xcode-sredstva-otladki-pro-kotorye-chasto-z/</link>
<pubDate>Thu, 12 Mar 2020 15:58:00 +0000</pubDate>
<author>Кирилл Сидоров</author>
<comments>https://sidorov.tech/all/advanced-debugging-v-xcode-sredstva-otladki-pro-kotorye-chasto-z/</comments>
<description>
&lt;p&gt;Каждый разработчик, независимо от квалификации и типа текущей задачи, постоянно находится в знакомом всем цикле: мы пишем код, запускаем и исправляем. Количество итераций у каждого разное, но делаем мы это ежедневно множество раз.&lt;/p&gt;
&lt;p&gt;По данным некоторых исследований, мы в среднем тратим до 60% на отладку — и это именно усредненное значение, для кого-то, особенно для начинающих разработчиков, оно может быть ещё больше. Эта статья призвана уменьшить это время и сделать процесс отладки эффективнее и приятнее.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;🎓&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;Эта статья является условным пересказом одноименного доклада для любимой iOS команды &lt;a href="https://noveogroup.com"&gt;Noveo&lt;/a&gt;. Факт того, что это пересказ накладывает определённый след на статью и стилистику изложения. Если что-то останется непонятным, welcome в комментарии, постараюсь ответить на все вопросы.&lt;/p&gt;
&lt;/code&gt;&lt;/div&gt;&lt;h2&gt;Пример&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/source_code@2x.png" width="720" height="261.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Давайте посмотрим на процесс изнутри. Я привёл немного странный пример, но он достаточно связан с ежедневной рутиной и отлично опишет большинство кейсов. Этот код производит вычисления высоты для ячейки таблицы: высота зависит от некоторых констант и других функций. Представим, что есть баг, где высота рассчитывается неверно и нашей задачей становится изучить эту функцию и исправить.&lt;/p&gt;
&lt;p&gt;Первым под поздозрение попадает флага showTitle и мы хотим знать его значение в момент вычисления. Что первым приходит на ум? Правильно, поместить print для отладки.&lt;/p&gt;
&lt;p&gt;Запускаем проект — он у нас большой, монолитный, да и Xcode не идеальный. Чаще всего происходит всем знакомая ситуация: добавили одну строчку и ждём несколько минут компиляцию. &lt;s&gt;А тут и твиттер манит и тик-ток (у кого что, в зависимости знаете ли вы ObjC 😁 ) и вот уже пара минут компиляции превращается в 20 с выпадом из задачи&lt;/s&gt;. А ведь кроме сборки и запуска нужно ещё и восстановить условия, добраться до нужного экрана, воспроизвести проблему и только после этого посмотреть вывод свежедобавленного print’a.&lt;/p&gt;
&lt;p&gt;И хорошо если догадка верна, но, как это обычно бывает, с первого раза расставить print’ы в полезных местах не получается. Само по себе знание о состояниии флага нам почти ни о чём не говорит, поэтому было решено добавить ещё один print с информацией об элементе, для которого производится расчёт. Затем мы пожелали переопределить некоторые значения констант и сделать исключение для одного элемента. Получается примерно такое:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/edited_code@2x.png" width="720" height="318.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;И снова нас ждёт сборка, воспроизведение условий и анализ. Сразу оговорюсь — отладка через print’ы сама по себе не является чем-то плохим. Порой это единственный способ отладить что-то в сложных ситуациях, например когда баг наблюдается только в релизной сборке с включенными оптимизациями. Но сегодня разговор пойдёт о простых, рутинных сценариях, которые чаще всего происходят во время разработки.&lt;/p&gt;
&lt;h2&gt;Breakpoints&lt;/h2&gt;
&lt;p&gt;Мы уже выяснили, что отладка через принты не совсем эффективна и нам нужен какой-то инструмент, который облегчит жизнь. Таким инструментом являются брейкпоинты! Этот механизм представлен практически во всех средах разработки, на множестве платформ и языков. Где-то он реализован лучше, где-то хуже, но Apple нам предоставила мощный и гибкий механизм точек останова. Однако, работая с разными разработчиками, я заметил, что пользуются брейкпоинтами в большинстве случаев только для остановки программы — просто чтобы убедиться, что её выполнение пошло по запланированному сценарию. Иногда люди пользуются консолью отладки и командой  &lt;samp&gt;po&lt;/samp&gt;, но лишь в тех случаях, когда нужно разок выяснить состояние переменной. Я предлагаю рассмотреть дополнительные возможности отладчика, встроенного в нашу IDE, и привести примеры ситуаций, в которых они могут пригодиться.&lt;/p&gt;
&lt;h2&gt;Условные брейкпоинты&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/bp_edit@2x.png" width="720" height="224" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Начнём с очевидного: conditional breakpoints. Как ни странно, строка, позволяющая указать условия срабатывания брейкпоинта, всегда у нас под носом, но почему-то люди удивляются такой возможности. Для удивлённых самим наличием такого диалога — его можно увидеть, если дважды нажать на сам брейкпоинт. В поле Condition можно записать любое выражение, которое может вернуть булево значение, будь то сравнение переменной из текущей области видимости или вовсе значение какого-то синглтона. Но будьте внимательны — медленно вычисляемое выражение способно существенно снизить производительность вашей программы.&lt;/p&gt;
&lt;h2&gt;Skipping&lt;/h2&gt;
&lt;p&gt;Следующей возможностью, которая тоже обделена вниманием разработчиков, является игнорирование N-го количества срабатываний. Эта возможность может пригодиться, например, в рекурсивных функциях, чтобы посмотреть, что происходит на N-ой глубине, или же посмотреть результат функции для N-го элемента массива. В примере с массивом этот способ будет предпочтительнее установки условия, т.к. не требует вычисления выражения.&lt;/p&gt;
&lt;h2&gt;Actions&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/bp_actions@2x.png" width="720" height="245.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Но самое интересное дня нас кроется за кнопкой &lt;samp&gt;Add Action&lt;/samp&gt;. Эта кнопка позволяет добавить дополнительное действие, которое будет вызвано в момент срабатывания брейкпоинта. Как вы видите, есть 6 типов действий, которыми можно дополнить брейкпоинт:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Apple script.&lt;/b&gt; Позволяет запустить скрипт на одноименном языке.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Capture GPU Frame.&lt;/b&gt; Для отладки приложений, использующих движок Metal, может потребоваться эта опция.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Debugger command.&lt;/b&gt; Позволяет выполнить команду отладчика. О ней мы поговорим позже.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Log-message.&lt;/b&gt; Позволяет вывести текстовое сообщение в лог.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Shell command.&lt;/b&gt; Позволяет выполнить произвольную команду в среде, дефолтной для системы командной оболочки, sh/bash/zsh.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sound.&lt;/b&gt; Позволяет проиграть звук из динамиков компьютера, на котором запущен Xcode.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Я не буду рассказывать о первых двух типах — они слишком специфичны и вряд ли вам пригодятся. А ещё пропущу последний пункт, так как особо рассказывать там нечего. Но помнить о нём стоит — он может вам пригодиться, например, когда нужно быстро совершить некое действие в приложении вслед за триггером, которым и может являтся звук от брейкпоинта, поставленного в нужное место.&lt;/p&gt;
&lt;h2&gt;Log messages&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/bp_log_actions@2x.png" width="720" height="361.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Рассмотрим чуть более подробно тип дополнительного действия &lt;i&gt;«Log message»&lt;/i&gt;. Если мы его выберем, к нашим услугам окажется строка ввода формата сообщения. Обратите внимание, что в строке можно указывать полезные плейсхолдеры, два из которых позволяют подставить информацию о брейкпоинте и одно, самое полезное, позволяет подставить результат вычисления произвольного выражения. Таким выражением может быть переменная или любая другая конструкция используемого вами языка программирования. Но это не имеет никакого смысла, если не поставить галочку &lt;i&gt;«Automatically continue after evaluating actions»&lt;/i&gt;. Именно она в паре с любым из действий позволит нам экономить время на дебаге. Больше не нужно писать &lt;samp&gt;print()&lt;/samp&gt;, пересобирать проект и ждать вечность. В любой момент времени, без перезапуска проекта вам доступен вывод в консоль отладки любой информации о ходе выполнения программы. А для знающих толк в извращениях дебаге Apple предусмотрела возможность воспроизвести выражения, используя встроенный синтезатор речи.&lt;/p&gt;
&lt;h2&gt;Shell command&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/bp_shell_commands@2x.png" width="720" height="405.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Нетрудно догадаться, что этот экшен позволяет запустить произвольную команду в стандартной оболочке терминала ОС. Как и &lt;i&gt;«Log message»&lt;/i&gt;, она позволяет вычислить результат выражения в текущем контексте и дополнить им аргументы вызова команды. Для чего это может быть полезно? Примеров использования можно придумать массу. Из реальной жизни: запуск троттлинга через Charles. Необходимо было замедлять запросы из определённой точки, при этом в остальное время соединение должно было быть полноценным. Я не успевал включать-выключать троттлинг вручную и ещё совершать действия в симуляторе. Такой трюк с брейкпоинтом и &lt;i&gt;«Shell command»&lt;/i&gt; отлично меня выручил. В другой раз мне понадобилось изменять информацию на сервере прямо параллельно с запросом, чтобы отловить довольно странный баг. Тут тоже был кстати этот вид брейкпоинта. Особые извращенцы могут собрать конструкцию на Arduino с электрошокером и бить себя током при каждом срабатывании нежелательного кода. Шучу. Не пытайтесь это воспроизвести в реальной жизни.&lt;/p&gt;
&lt;h2&gt;Debugger command&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/bp_lldb_cmd@2x.png" width="720" height="286.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Одним из самых интересных видов экшенов я считаю &lt;i&gt;«Debugger command»&lt;/i&gt;. Этот экшен позволяет действительно безгранично влиять на отлаживаемую программу. Debugger command — это команды отладчика LLDB, а LLDB — это отладчик для проекта LLVM, который сейчас используется Apple и Xcode для сборки программ. Отладчик LLDB позволяет подключаться к процессу, прерывать выполнение программы и воздействовать на её память. Для этого отладчик имеет множество команд, некоторые из которых станут героями сегодняшнего повествования. Именно благодаря отладчику LLDB у нас в принципе есть такая замечательная возможность отлаживать программу, в частности устанавливать брейкпоинты.&lt;/p&gt;
&lt;p&gt;Начнём мы с самой известной команды — &lt;samp&gt;po&lt;/samp&gt;. Наверняка многие из вас уже не раз использовали эту команду при отладке, но для меня в своё время это стало открытием, хотя я уже имел некоторый опыт в разработке под iOS на тот момент. &lt;samp&gt;po&lt;/samp&gt; — это сокращение от &lt;samp&gt;print object&lt;/samp&gt;. Команда позволяет вычислить выражение из правой части от команды и распечатать в консоли результат выполнения. При этом у объекта запросится его &lt;samp&gt;debugDescription&lt;/samp&gt;, если он определён, или просто &lt;samp&gt;description&lt;/samp&gt;, если нет. У po существует команда-прародитель — &lt;samp&gt;print&lt;/samp&gt;, или p, которая точно так же вычислит выражение и распечатает результат, но только в этом случае вам будет доступна сырая информация об объекте или скалярном типе. Обе эти команды будут компилировать введенное выражение в текущем контексте, что неминуемо замедлит выполнение кода при срабатывании брейкпоинта. К счастью, в Xcode 10.2 Apple добавили ещё одну команду отладчика — &lt;samp&gt;v&lt;/samp&gt;, которая работает значительно быстрее. Она позволяет вывести в консоль значение переменной из текущей области видимости, но, в отличии от &lt;samp&gt;p&lt;/samp&gt; и &lt;samp&gt;po&lt;/samp&gt;, без компиляции выражения. Естественное ограничение, накладываемое этой особенностью, — вывод в консоль возможен только для хранимых свойств.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/lldb_v_po@2x.png" width="720" height="303.5" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Влияние на ход программы&lt;/h2&gt;
&lt;p&gt;Такая комбинация (брейкпоинт + debugger command po + автоматическое продолжение) заменит нам описанную ранее Log message. Что же ещё мы можем сделать с помощью такой комбинации? Например, с помощью дебаггера мы можем пропустить выполнение нескольких строчек кода, будто они закомментированы. При этом вам не нужно пересобирать программу и заново воспроизводить условия. Для этого достаточно ввести&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;thread jump --by 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;для скачка вперёд на одну строчку или же&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;thread jump --line 44&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;для перехода, как вы уже могли догадаться, к 44 строчке.&lt;/p&gt;
&lt;div class="callout"&gt;&lt;div class="pin"&gt;&lt;p&gt;☝️&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;&lt;b&gt;Но будьте осторожны — вы не можете на 100% безопасно перепрыгивать по строчкам.&lt;/b&gt; Дело в том, что вы можете перепрыгнуть через инициализацию некоторой переменной, и это вызовет краш. Дело осложняется тем, что Swift «ленив» по своей природе, и инициализация может происходить не там, где вам кажется. Плюс компилятор при сборке вашей программы вставляет дополнительные инструкции, например для управления памятью, пропуская которые вы рискуете получить в лучшем случае утечку, в худшем — краш.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;Влияние на дебаггер&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/bp_leo@2x.png" width="720" height="286.5" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Кроме влияния на вашу программу, с помощью отладчика вы можете влиять на сам отладчик. Например, мы можем поставить брейкпоинт из брейкпоинта. Вы спросите, зачем это нужно? Бывают методы общего назначения, которые срабатывают по ряду триггеров. Например функция по отправке сообщения в аналитику может вызываться сотню раз в секунду, а нам нужно отловить именно ту отправку, которую породит нажатие на кнопку. В этом случае мы можем поставить брейкпоинт на метод нажатия кнопки и добавить команду установки брейкпоинта на произвольной строке программы в произвольном файле. Команда &lt;samp&gt;bp s -o -f Calc.swift -l 44&lt;/samp&gt; расшифровывается как &lt;b&gt;b&lt;/b&gt;reak&lt;b&gt;p&lt;/b&gt;oint &lt;b&gt;s&lt;/b&gt;et &lt;b&gt;o&lt;/b&gt;ne-shot на &lt;b&gt;ф&lt;/b&gt;айл Calc.swift на строку 44. Модификатор &lt;samp&gt;-o&lt;/samp&gt; или &lt;samp&gt;--one-shot&lt;/samp&gt; создаст специальный тип брейкпоинта, который «живёт» ровно до момента своего срабатывания, а после исчезает. Таким нехитрым способом мы можем создавать интересные алгоритмы установки брейкпоинтов для отладки нетривиальных багов.&lt;/p&gt;
&lt;h2&gt;Other breakpoints types&lt;/h2&gt;
&lt;p&gt;А есть ли ещё виды брейкпоинтов, о которых мы можем не знать? Конечно, есть. Xcode позволяет добавить некоторые виды брейкпоинтов, которые не относятся к какому-то конкретному файлу и строке. В Xcode есть вкладка Breakpoint Navigator, которая позволяет управлять уже созданными брейкпоинтами сквозь все файлы проекта, а также создавать новые. Внизу окна нашего IDE есть кнопка со значком плюса.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/dbg_new@2x.png" width="500" height="328" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Это позволяет использовать 6 дополнительных типов брейкпоинтов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Swift Exception брейкпоинт&lt;/b&gt; — брейкпоинт, останавливающий программу при срабатывании не перехваченного throw для Swift кода.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Exception брейкпоинт&lt;/b&gt; — то же самое, но для мира ObjC. Может показаться, что это не актуальный в современном мире брейкпоинт, но это не так. Стоит помнить, что нам пока всё ещё нужен UIKit, написанный на ObjC, ошибки которого мы можем отловить с помощью такого вот брейкпоинта.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Symbolic breakpoint&lt;/b&gt; — позволяет останавливать процесс выполнения программы при выполнении кода, ассоциированного с некоторым идентификатором, который Apple называет символом. О символах я расскажу чуть позже.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OpenGL ES Error брейкпоинт&lt;/b&gt; — брейкпоинт, останавливающий программу при возникновении ошибки OpenGL при разработке соответствующих приложений.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Constraint Error breakpoint&lt;/b&gt; — очевидно, остановит вашу программу при возникновении ошибки автолейаута.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Test Failure breakpoint&lt;/b&gt; может вам помочь при отладке тестов.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Так как уместить в этой сессии обзор всех типов точек останова не представляется возможным, я остановлюсь только на самых часто используемых. По своему опыту — я всегда использую &lt;b&gt;Exception breakpoint&lt;/b&gt;. Довольно часто при разработке программ я сталкиваюсь с перехваченными системными исключениями, отладить которые порой проблематично из-за крайне неинформативного call stack’а. Думаю, вы сталкивались хоть раз с такой или подобной ошибкой:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/excep_crash@2x.png" width="720" height="496.5" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Exception breakpoint&lt;/h2&gt;
&lt;p&gt;Для того, чтобы сделать стек вызова более информативным, мы можем добавить Exception breakpoint. Он позволит остановить программу прямо на моменте выброса исключения и отследить цепочку событий, которые привели к такому результату. По умолчанию неперехваченное исключение вызовет аварийную остановку приложения, и в стеке вызова мы ничего полезного не увидим, т.к. исключение будет пробрасываться вверх по стеку вызова и вся информация о месте выброса будет утеряна. Exception breakpoint позволяет остановить программу в момент выброса исключения и уже привычными нами методами получить гораздо больше информации о проблеме, пройдясь по стеку вызова и просмотрев значения переменных, если это необходимо. Я считаю этот тип брейкпоинта очень полезным и использую его на всех проектах по умолчанию. Для этого в Xcode есть удобный механизм, который позволяет указать брейкпоинту уровень и хранить его на трёх уровнях:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Проект.&lt;/li&gt;
&lt;li&gt;Воркспейс.&lt;/li&gt;
&lt;li&gt;Пользователь.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Просто нажмите на брейкпоинт правой кнопкой мыши и выберите Move breakpoint. Перенесённый на уровень пользователя, брейкпоинт будет доступен на всех проектах, какой бы вы ни открыли в вашем Xcode.&lt;/p&gt;
&lt;h2&gt;Symbolic Breakpoint&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/symbolic_bp@2x.png" width="720" height="316" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Вторым часто используемым типом брейкпоинтов является Symbolic Breakpoint. Ранее я уже писал, что этот брейкпоинт позволяет останавливать программу при выполнении кода, ассоциированного с каким-то символом, и обещал рассказать подробнее про символы. Так вот, символы — это человекопонятные идентификаторы, которые ассоциируются с тем или иным адресом в памяти. LLDB умеет маппить известные ей символы в адреса функций и наоборот. При каждой сборке проекта система создаёт особый бандл из специальных файлов в формате dSYM, которые расшифровываются как Debug Symbols. Эти файлы хранят что-то вроде таблицы, содержащей в себе некоторые адреса методов и некоторые идентификаторы, среди которых сигнатуры методов, имена файлов, смещения и номера строк. Именно благодаря этим файлам мы можем поставить брейкпоинт на строку файла, получить читаемый стек вызова или расшифровать crashlog приложения из AppStore.&lt;/p&gt;
&lt;p&gt;Благодаря этому механизму мы можем поставить брейкпоинт на любом методе класса, зная только его название. При этом нам не нужно достоверно знать, где этот метод объявлен и доступны ли вообще нам исходные файлы. Давайте рассмотрим реальный пример. Вас перевели на новый проект, и первая задача — исправить непонятное поведение на форме ввода данных кредитной карты, когда посреди набора фокус вдруг перепрыгивал на поле ввода имени. Сходу ничего не понятно, кода много, но симптомы ясны.&lt;/p&gt;
&lt;p&gt;Для расследования необходимо понять, кто и почему инициирует смену фокуса. Можно долго читать код, искать логику в неочевидных расширениях классов, а как надоест — сделать наследника UITextField’a, переопределив там метод &lt;samp&gt;becomeFirstResponder()&lt;/samp&gt;, поменять реализации и уже там поставить брейкпоинт. А можно за 10 секунд создать символьный брейкпоинт &lt;samp&gt;-[UITextField becomeFirstResponder]&lt;/samp&gt;, и программа остановится в момент смены фокуса. По цепочке бэктрейса мы сможем легко восстановить последовательность событий, которые приводят к нежелательным результатам.&lt;/p&gt;
&lt;p&gt;У тех, кто пользуется таким видом брейкпоинта в первый раз, наверняка возник вопрос: а что это за символ&lt;br /&gt;
&lt;samp&gt;-[UITextField becomeFirstResponder]&lt;/samp&gt;? Это ObjectiveC-сигнатура метода установки текста для лейбла. Использование ObjectiveC обусловлено тем, что UIKit написан именно на этом языке.&lt;/p&gt;
&lt;p&gt;Пара слов для тех, кто имел мало опыта с ObjectiveC. Знак минуса обозначает, что нас интересует инстанс-метод, а не метод класса, далее в квадратных скобках записывается название класса и через пробел метод, двоеточие указывает на то, что этот метод принимает параметр.&lt;/p&gt;
&lt;p&gt;Тут можно возразить, что пример притянут за уши. Я согласен — в хорошем коде не будет десятка мест с установкой текста лейбла, но моя цель — показать, как это может работать. Давайте рассмотрим более реальный пример. Допустим, для целей отладки нам может понадобиться распечатать последовательность показа вью контроллеров. Добавляем брейкпоинт с символом &lt;samp&gt;-[UIViewController viewDidAppear:]&lt;/samp&gt;, указываем дополнительное действие &lt;samp&gt;po NSStringFromClass([instance class])&lt;/samp&gt; и, конечно же, не забываем поставить галочку &lt;i&gt;«Automatically continue after evaluating actions»&lt;/i&gt;.&lt;/p&gt;
&lt;p&gt;Мы снова вынуждены использовать ObjC, даже в дополнительной команде, так как находимся в его контексте. Что касается Swift, то символы записываются как название &lt;samp&gt;ClassName.methodName(param:)&lt;/samp&gt;. Прописывать параметры не обязательно, LLDB попытается разрешить неоднозначность, если есть методы с одинаковым названием, но разными параметрами.&lt;/p&gt;
&lt;h2&gt;Поиск символов&lt;/h2&gt;
&lt;p&gt;Рассказывая о символьных брейкпоинтах, я не могу не рассказать о возможности искать символы. Остановив программу любым способом, с помощью брейкпоинта или же просто нажав на пиктограмму паузы, мы можем воспользоваться командой&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;image lookup -r -n&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;и найти интересующие вас символы в вашей программе и во всех загруженных библиотеках. Это действительно делает вас чуть ли не богом дебага, потому как вы властны искать символы везде, скажем в UIKit’e, искать приватные методы, останавливать и изучать внутреннее устройство системных библиотек. Надеюсь, я убедил в вас в силе этого метода и он не раз поможет вам сэкономить время.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/sym_search@2x.png" width="720" height="264" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Watchpoints&lt;/h2&gt;
&lt;p&gt;Вотчпоинты позволяют останавливать программу, когда изменяется значение переменной. Корректнее будет сказать, что этот механизм позволяет следить за изменениями памяти по заданному адресу с заданным размером, но благодаря LLDB и Xcode разработчику достаточно сделать несколько кликов. Использование вотчпоинтов будет удобным, когда за изменением переменной не следует никакого сайд-эффекта прямо после изменения, но её состояние важно для отложенных вычислений. В ряде случаев может быть непонятно, что инициирует это изменение, и вотчпоинты позволят быстро узнать это. Достаточно приостановить выполнение программы в контексте нужного класса и воспользоваться окном Variables View.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/wp_set@2x.png" width="720" height="356" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Тут будут перечислены переменные в текущем фрейме, доступные к отлаживанию. В крупных проектах вычисление доступных переменных и их типов может занимать некоторое время, поэтому иногда нужно подождать несколько (десятков?) секунд перед тем, как переменные будут доступны к манипуляциям над ними. Приятным бонусом является возможность «заглянуть» внутрь объектов Objective-C: функциональность Variables View позволяет увидеть приватные переменные этих объектов. По клику правой кнопки мыши по переменной нам доступно не так много опций — мы можем изменять значение переменных скалярных типов и, собственно, добавлять вотчпоинты.&lt;/p&gt;
&lt;p&gt;Конечно же, вотчпоинт можно установить и командой LLDB: &lt;samp&gt;watchpoint set variable variable_name&lt;/samp&gt;, или, пользуясь функцией сокращения команд LLDB, просто: &lt;samp&gt;w s v variable_name&lt;/samp&gt;, но помните, что переменная должна быть видна отладчику, то есть находиться в текущем фрейме. Помимо установки брейкпоинта на изменение переменной, нам доступна установка вотчпоинта на область памяти:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;watchpoint set expression — 0x0d78ab5ea8&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;. В обоих случаях при изменении содержимого памяти по отслеживаемому адресу произойдет прерывание программы. Установленные точки останова можно посмотреть командой&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;watchpoint list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;или в Debugger navigator. Так как любые вотчпоинты в итоге следят за адресом памяти, они становятся неактуальны после перезапуска и не сохраняются между перезапусками приложения. Даже если вы установили брейкпоинт на изменение переменной, под капотом механизм lldb вычислил её адрес и поставил вотчпоинт по этому адресу.&lt;/p&gt;
&lt;h2&gt;Влияем на состояние&lt;/h2&gt;
&lt;p&gt;Будем закругляться. Последнее, о чем я хотел поведать в рамках этой статьи, — влияние на состояние приложения из LLDB. До этого я говорил только об изменении состояния какого-либо объекта системы при остановке по брейкпоинту. Но что, если нам требуется приостановить программу в произвольный момент времени? Нажатие на значок паузы приводит к приостановке программы, но вот вместо привычного нам кода мы увидим код ассемблера. Так как же добраться до произвольного объекта и выполнить с ним хитрые манипуляции?&lt;/p&gt;
&lt;h2&gt;Memory graph&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/mem_graph@2x.png" width="1012" height="159" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Большинство iOS-разработчиков уже с первых месяцев своей работы используют этот инструмент. Для тех, кто ни разу им не пользовался, поясню. Memory graph позволяет сделать дамп памяти программы и отобразить в виде списка и графа все экземпляры объектов, которые сейчас находятся в памяти. Зачастую этот инструмент используется для выявления утечек объектов и анализа связей, которые привели к такому результату. Но сегодня от этого инструмента нам нужна только возможность остановить программу в произвольное время, найти нужный объект и узнать его адрес. Но что мы можем сделать с этой, казалось бы, бесполезной информацией?&lt;/p&gt;
&lt;p&gt;На самом деле — всё, что угодно. Тут нам на помощь приходит мощь ObjC. Мы можем написать&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;[0x7fafffa54a5 setValue:[UIColor redColor] forKey:@&amp;quot;switchedOffColor&amp;quot;]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;— и мы уже поменяли значение цвета выключенной лампы на красный, используя стандартные методы NSObject, доступные нам из коробки. Но что, если нам недостаточно этих методов, а нужно «дёрнуть» за свои рычаги? Всё просто — мы можем использовать кастинг:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;[(MyLamp *)0x7fafffa54a5 powerOff]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;. Используя подобные техники можно воздействовать на любые сервисы, менеджеры и вью модели вашего приложения в любой момент времени.&lt;/p&gt;
&lt;p&gt;Мы можем сохранить значение этого адреса в переменную для удобства:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;(MyLamp *)$lamp = 0x7fafffa54a5&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;. Важно, что название переменной должно начинаться со знака доллара. Это переменная будет жить до полной остановки программы, то есть ей можно пользоваться не только в текущем сеансе отладки, но и при следующем прерывании программы в рамках одного запуска.&lt;/p&gt;
&lt;p&gt;ObjectiveС предоставляет поистине широкие возможности для того, чтобы похакать текущее состояние и обойти многие ограничения, но что делать с классами, доступными только в Swift? Конечно же, при попытке кастинга Swift-класса в ObjC-контексте ничего не произойдёт. К счастью, в Swift есть подобный механизм. Точнее, функция, имя которой — &lt;samp&gt;unsafeBitCast(_:to:)&lt;/samp&gt;. Мы вправе использовать его с адресом:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;unsafeBitCast(0x7fafffa54a5, to: MySwiftLamp.self)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;и получить экземпляр класса MySwiftLamp по адресу. Помните, её использование небезопасно, о чём нам намекает её имя, и её крайне осторожно нужно применять в коде приложения. Хотя, когда вам осознанно нужно будет использовать эту функцию, вы будете достаточно опытны для таких предупреждений.&lt;/p&gt;
&lt;h2&gt;View Hierarchy&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/view_hierarchy@2x.png" width="1012" height="152" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Рядом со инструментом Debug Memory Graph соседствует другой, не менее полезный инструмент, — View Hierarchy. Он позволяет быстро найти нужную View, посмотреть её параметры и лейаут, посмотреть активные и неактивные констрейнты. С iOS 11 этот инструмент ещё научился отображать ViewController’ы в иерархии, таким образом находить нужную View стало легче. Неочевидным тут является возможность фильтрации по имени и возможность отключить/включить отображение View, скрытых за экраном. Также я обратил внимание, что редко кто пользуется панелью управления внизу окна визуального отображения View.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://sidorov.tech/pictures/view_h_tools@2x.png" width="1012" height="152" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Кроме того, что она может регулировать глубину просмотра иерархии, она позволяет указать «включить отображение обрезанного контента» и «включать отображение констрейнтов». Обязательно поиграйтесь со всеми инструментами, я уверен — вы найдете полезное для себя применение для некоторых из них.&lt;/p&gt;
&lt;p&gt;Но в рамках этого рассказа нам нужна только возможность найти нужную View и узнать её адрес. Далее действуем по накатанной:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;po unsafeBitCast(0x7fafffa54a5, to: UIView.self)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;но в таком случае мы получим ошибку, т.к. сейчас находимся в контексте ObjectiveC и не можем использовать &lt;samp&gt;po&lt;/samp&gt; со Swift-кодом. Мы вынуждены использовать команду expession, или просто &lt;samp&gt;e&lt;/samp&gt; с указанием языка:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;e -l Swift -- unsafeBitCast(0x7fafffa54a5, to: UIView.self)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Но и тут наши попытки не увенчаются успехом, мы получим ошибку &lt;i&gt;error: &lt;EXPR&gt;:3:35: error: use of unresolved identifier ‘UIView’&lt;/i&gt;.&lt;/p&gt;
&lt;p&gt;Это произойдет из-за модульной природы Swift’а. Для успешного выполнения операции нам потребуется сделать импорт модуля UiKit:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;e -l Swift -- import UIKit&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;, и после этого мы наконец добьёмся результата:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;e -l Swift -- unsafeBitCast(0x7fafffa54a5, to: UIView.self)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;.&lt;br /&gt;
Ура! Мы получили описание в консоли. Теперь давайте попробуем поменять, скажем, цвет её бэкграунда. Для начала сохраним View в переменную, чтобы облегчить процесс доступа к ней. Как и в случае с ObjectiveC, при создании переменной в LLDB контексте её название должно начинаться со знака доллара:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;e -l Swift -- let $view = unsafeBitCast(0x7fafffa54a5, to: UIView.self)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;далее мы можем применить необходимые изменения:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;e -l Swift -- $view.backgroundColor = .red&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Чтобы увидеть изменения, необходимо продолжить выполнение программы. Но есть способ увидеть изменения и без этого, находясь в режиме «паузы».&lt;/p&gt;
&lt;p&gt;Дело в том, что мы не видим изменения не потому, что приложение приостановлено, а потому, что все изменения UIView копятся в транзакцию CALayer и применяются только в конце «вращения» текущего RunLoop’а с помощью вызова &lt;samp&gt;CATrasaction.flush()&lt;/samp&gt;. Когда приложение приостановлено для отладки, операционная система всё ещё живёт своей жизнью, вы можете свернуть это приложение и открыть другое. Операционная система всё ещё опрашивает состояние UI вашего приложения и отрисовывает ваше приложение несколько десятков раз в секунду, только RunLoop приостановлен, CATrasaction.flush не вызывается, изменения не применяются.&lt;/p&gt;
&lt;p&gt;Так что, достаточно самостоятельно сделать вызов&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;e -l Swift -- CATrasaction.flush()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;, и мы увидим изменения.&lt;/p&gt;
&lt;p&gt;На этом пора завязывать. Надеюсь, приведённые примеры кому-то облегчат жизнь, сохранят время и нервы. Добавьте в закладки, и в следующий раз, когда на поиск и отладку очередного бага у вас будет уходить более 15 минут, загляните в эту статью — возможно, какой-нибудь приём вам пригодится.&lt;/p&gt;
</description>
</item>


</channel>
</rss>