Библиотека мобильного разработчика | Android, iOS, Swift, Retrofit, Moshi, Chuck
9.51K subscribers
1.72K photos
86 videos
52 files
4.55K links
Все самое полезное для мобильного разработчика в одном канале.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/b60af5a4

Для обратной связи: @proglibrary_feeedback_bot

РКН: https://gosuslugi.ru/snet/67a4adec1b17b35b6c0d8389
Download Telegram
✏️ adb shell dumpsys meminfo – детальный анализ использования памяти приложением

Мощный инструмент для мониторинга использования памяти вашим приложением в реальном времени. Помогает находить утечки памяти и оптимизировать потребление RAM.

📌 Ключевые метрики:

1. PSS (Proportional Set Size):

• Реальная память, занимаемая приложением
• Включает разделяемую память (пропорционально)

2. Private Dirty:

• Память, принадлежащая только вашему приложению
• Не может быть выгружена в swap

3. Java Heap:

• Память, управляемая Dalvik/ART
• Ключевой показатель для поиска утечек

⚡️ Практические сценарии:

Мониторинг в реальном времени:

# Каждые 2 секунды обновлять информацию
watch -n 2 "adb shell dumpsys meminfo com.yourapp.package"


Сравнение состояний:


# До и после выполнения операции
adb shell dumpsys meminfo com.yourapp.package > before.txt
# Выполняем действия в приложении
adb shell dumpsys meminfo com.yourapp.package > after.txt
diff before.txt after.txt


Поиск утечек в Activity:

# Проверить, не остались ли Activity в памяти
adb shell dumpsys meminfo | grep -E "Activity|View|Context"


⚙️ Анализ конкретных компонентов:

Только Java Heap:

adb shell dumpsys meminfo com.yourapp.package | grep -A 10 "Java Heap"


Нативные allocations:

adb shell dumpsys meminfo com.yourapp.package | grep -A 5 "Native Heap"


📊 Пример вывода для анализа:

Applications Memory Usage (in KB)
Uptime: 1234567 Realtime: 1234567

** MEMINFO in pid 1234 [com.yourapp] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 12345 12000 345 100 15000 14000 1000
Dalvik Heap 5678 5600 78 50 6000 5800 200


🔧 Продвинутое использование:

Профилирование конкретной операции:

#!/bin/bash
echo "Память до:" > mem_log.txt
adb shell dumpsys meminfo com.yourapp.package >> mem_log.txt

# Запускаем тестовый сценарий
adb shell am start -n com.yourapp/.TestActivity

echo "Память после:" >> mem_log.txt
adb shell dumpsys meminfo com.yourapp.package >> mem_log.txt
Pro-совет: Используйте с procstats для долгосрочного мониторинга:

bash
adb shell dumpsys procstats --hours 1 com.yourapp.package


Как вы отслеживаете использование памяти в своих приложениях? 💬

🐸 Библиотека мобильного разработчика

#буст #MiddlePath #Android
Please open Telegram to view this post
VIEW IN TELEGRAM
2
⚙️ Превращение меню SwiftUI в мини-панель настроек

Меню в SwiftUI часто используются для быстрых одноразовых команд: выберите пункт, выполните действие — и всё готово. Но что, если вы хотите, чтобы меню функционировало не как простой селектор, а как мини-панель настроек, где пользователи могут последовательно переключать несколько пунктов, прежде чем сделать окончательный выбор? По умолчанию меню закрывается, как только пользователь нажимает на пункт, но с помощью подходящего API это поведение можно изменить.

Переосмысление меню

Обычно при добавлении Menu к метке — например, к значку с тремя точками или кнопке — каждое касание внутри меню выполняет действие и немедленно закрывает меню. Это ожидаемый рабочий процесс «выбрать и закрыть».

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

🔘 Меню, допускающее несколько переключателей.
🔘 Меню, сохраняющее постоянное состояние, пока пользователь перемещается по набору пунктов.
🔘 Меню, которое остаётся открытым, пока пользователь пробует различные настройки, возможно, просматривая эффект в режиме реального времени, и закрывается только после того, как пользователь будет удовлетворен результатом.

В таких случаях поведение закрытия по умолчанию не является идеальным.

Настройка поведения закрытия

SwiftUI расширяет меню с помощью модификатора menuActionDismissBehavior(_:). Это даёт вам возможность точно контролировать, должно ли касание внутри меню приводить к его закрытию или оставаться открытым.

Модификатор принимает один параметр типа MenuActionDismissBehavior, который представляет собой перечисление, определяемое примерно следующим образом:

public enum MenuActionDismissBehavior {
case automatic // system-default behaviour
case enabled // explicitly force dismissal on each tap
case disabled // keep the menu open after taps
}


Применение модификатора выглядит так:

Menu("Options") {
Button("Toggle A") { /* … */ }
Button("Toggle B") { /* … */ }
Divider()
Button("Done") { /* … */ }
}
.menuActionDismissBehavior(.disabled)


При использовании .disabled меню остаётся открытым после каждого действия, позволяя пользователю выполнить несколько действий, прежде чем решить закрыть его.

Наглядный пример: пакетное переключение

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

struct FeatureToggleMenu: View {
@State private var featureA = false
@State private var featureB = false
@State private var featureC = false

var body: some View {
Menu {
Section {
Toggle("Feature A", isOn: $featureA)
Toggle("Feature B", isOn: $featureB)
Toggle("Feature C", isOn: $featureC)
}
.menuActionDismissBehavior(.disabled)

Button("Apply Changes") {
// commit logic here
}
} label: {
Label("Settings", systemImage: "gearshape")
}
}
}


В этом макете:

🔘 Пользователь открывает «Настройки» и переключает различные функции, не закрывая меню при каждом нажатии переключателя.
🔘 После выбора он нажимает «Применить изменения», а затем может закрыть меню (в зависимости от реализации).
🔘 Вы даже можете вручную принудительно закрыть меню, используя специальную кнопку «Готово».

Заключение

Модификатор menuActionDismissBehavior(_:) — это удобный API для преобразования типичных меню SwiftUI в более надежные мини-интерфейсы для настроек, переключателей и многошаговых рабочих процессов. Продуманное использование этого может привести к более понятному и интуитивно понятному пользовательскому интерфейсу, когда вам нужно больше, чем простое взаимодействие «выбрать и применить».

🐸 Библиотека мобильного разработчика

#буст #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
1😁1
Организация представлений SwiftUI с помощью ToolbarContent и @ToolbarContentBuilder

По мере роста проектов SwiftUI одна из частых проблем — управление сложными иерархиями представлений. Даже простой экран может быстро превратиться в десятки вложенных модификаторов. После снятия лимита в 10 вложенных представлений стало проще писать глубоко вложенные body, но код стал труднее читать и сопровождать.

🔹 Разбираем крупные реализации body

Когда body растягивается на десятки строк, страдает читаемость. Лучше разбивать большие представления SwiftUI на мелкие подпредставления или выделять повторно используемые части в вычисляемые свойства или функции. Это сохраняет лаконичность и упрощает понимание, тестирование и повторное использование.

Тот же принцип применим к невизуальным элементам, например панелям инструментов, которые быстро разрастаются при добавлении множества кнопок и пунктов меню.

🔹 Проблема с панелями инструментов

Модификатор .toolbar позволяет создавать кнопки, меню и элементы управления, адаптируемые под разные платформы. Но если элементов становится много, код внутри .toolbar { ... } быстро теряет читаемость.
Перенести логику в вычисляемое свойство нельзя — .toolbar ожидает содержимое, соответствующее ToolbarContent.

🔹 ToolbarContent и @ToolbarContentBuilder

SwiftUI решает это с помощью:

ToolbarContent — протокола для элементов панели инструментов;
@ToolbarContentBuilder — билдера, создающего набор элементов панели.

Объединив их, можно вынести содержимое панели инструментов в отдельный блок, сделав body чище и понятнее.

Пример

struct DemoView: View {
@State private var message = "Hello, world!"

var body: some View {
NavigationStack {
Text(message)
.font(.title2)
.padding()
.navigationTitle("Home")
.toolbar { toolbarContent }
}
}

@ToolbarContentBuilder
private var toolbarContent: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
Button { message = "Left tapped" } label: {
Label("Left", systemImage: "line.3.horizontal")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button { message = "Right tapped" } label: {
Label("Right", systemImage: "star")
}
}
}
}


🔹 Заключение

Использование @ToolbarContentBuilder для отделения содержимого панели инструментов от основного окна имеет ряд преимуществ:

Улучшена читаемость: Ваш body текст по-прежнему сосредоточен на вёрстке, а логика панели инструментов реализована в другом месте.
Лучшая организация: Группировка элементов панели инструментов в отдельном блоке позволяет с первого взгляда оценить их структуру и расположение.
Масштабируемость: Когда на панели инструментов появляется несколько кнопок, меню или условная логика, поддерживать её в рабочем состоянии становится намного проще.

🐸 Библиотека мобильного разработчика

#PixelPerfect #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
2
🌛 Реализация офлайн-режима

Хотите, чтобы ваше приложение работало даже без интернета? Этот промпт поможет реализовать надежный офлайн-режим и синхронизацию данных.

📝 Промпт:

Implement offline-first approach for a mobile app that includes:

— Set up local database (Room/SQLite/CoreData)
— Implement data synchronization strategy
— Handle conflict resolution for concurrent modifications
— Manage offline queue for pending operations
— Add network connectivity detection
— Implement cache management and expiration policies
— Provide user feedback for sync status


💡 Расширения:

— Добавьте Implement reactive UI updates using observables для автоматического обновления интерфейса
— Добавьте Add retry mechanisms with exponential backoff для надежной синхронизации
— Добавьте Support large file caching with storage management для работы с медиа в офлайне

🐸 Библиотека мобильного разработчика

#буст #MiddlePath
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱2🔥1🤔1
Эффект ЭЛТ-экрана в Jetpack Compose

ЭЛТ-мониторы — это размытые края, линии сканирования и лёгкое свечение. Такой эффект можно воспроизвести в Compose с помощью GraphicsLayer, градиентов и размытия.

🔹 Базовый принцип

Мы один раз записываем контент во внеэкранный буфер и многократно перерисовываем его разными слоями.

val graphicsLayer = rememberGraphicsLayer()

Box(Modifier.drawWithContent {
graphicsLayer.record { drawContent() }
}) {
content()
}


Теперь drawLayer(graphicsLayer) можно использовать в любых эффектах.

🔹 Линии сканирования

Создаём повторяющиеся градиенты — вертикальные и горизонтальные:

private fun DrawScope.drawScanLines(alpha: Float, blend: BlendMode) {
val c = Colors.Black.copy(alpha)
drawRect(
brush = Brush.verticalGradient(
0f to c, 0.4f to c, 0.4f to Colors.Transparent, 1f to Colors.Transparent,
tileMode = TileMode.Repeated, endY = 10f
),
blendMode = blend
)
}


Добавляем их поверх слоя:

.drawBehind {
layer {
drawLayer(graphicsLayer)
drawScanLines(alpha = 1f, blend = BlendMode.DstOut)
}
}


DstOut вычитает градиент и создаёт характерный "CRT-срез".

🔹 Размытие и свечение

Для реалистичного свечения рисуем несколько слоёв с разным blur/scale/alpha:

val blurLayers = listOf(
Triple(5.dp, .3f, 1.02f to 1.03f),
Triple(0.dp, .8f, 1f to 1f),
Triple(10.dp, .6f, 1.001f to 1f),
)


Каждый слой:

Box(
Modifier
.matchParentSize()
.blur(blur, BlurredEdgeTreatment.Unbounded)
.graphicsLayer { scaleX = scale.first; scaleY = scale.second; this.alpha = alpha }
.drawBehind {
layer {
drawLayer(graphicsLayer)
drawScanLines(1f, BlendMode.DstOut)
}
}
)


🔹 Дрожание экрана

var shake by remember { mutableStateOf(Offset.Zero) }

LaunchedEffect(Unit) {
while (true) {
shake = Offset(
Random.nextFloat() * Random.nextInt(-1, 1),
Random.nextFloat() * Random.nextInt(-1, 1),
)
delay(32)
}
}


И применяем:

.graphicsLayer {
translationX = shake.x
translationY = shake.y
}


🐸 Библиотека мобильного разработчика

#PixelPerfect #MiddlePath #Kotlin
Please open Telegram to view this post
VIEW IN TELEGRAM
🎮 Почему ваша ViewModel технически нестабильна и почему Compose не возражает

А вы знали, что почти все ViewModels нестабильны?

Когда мы только изучаем Compose,
нас учат использовать стабильный класс, а не нестабильный.

Но ViewModels нестабильны. Так почему же никто ничего не говорит о том, что мы используем нестабильные ViewModels?

🔹 Как Compose определяет стабильность?

Компилятор Compose считает класс стабильным, если:

🔘 Все его свойства (val/var) неизменяемы (val).

🔘 Или их тип «отслеживается» рантаймом Compose (например, MutableState<T>).

Взгляните на примеры:

// Стабильный класс
data class Stable(
val a: Int, // Стабильно
val b: MutableState<Int>, // Стабильно, отслеживается Compose
val c: SnapshotStateList<Int> // Стабильно, отслеживается Compose
)

// Нестабильный класс
data class Unstable(
var b: Int // Нестабильно из-за `var`
)

// "Неопределенная" стабильность
data class Runtime(
val i: Interface // Компилятор не знает, какая реализация будет на runtime.
)


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

🔹 Что происходит на границах модулей?

Допустим, вы создали стабильную data class в слое данных (data) и внедрили её в ViewModel в слое презентации.

Логично ожидать, что ViewModel тоже будет стабильным. Но на практике — нет!

Compose-компилятор в модуле презентации не может заглянуть в модуль данных, чтобы проверить стабильность вашего класса. Поэтому он перестраховывается и помечает любой класс извне как нестабильный.

А раз наш ViewModel зависит от репозиториев и UseCase из других модулей (domain/data), то и он сам автоматически становится нестабильным.

🔹 Так почему же нестабильный ViewModel — это норма?

Ответ простой и лежит на поверхности: мы не передаем сам ViewModel в дочерние композаблы.

Вместо этого мы:

1. Создаем ViewModel один раз наверху (например, в NavGraph).

2. Коллектим его состояние (state), которое уже является стабильным.

3. Пробрасываем это стабильное состояние вниз по дереву композиции.

@Composable
fun Screen(viewModel: TestViewModel) { // ViewModel нестабилен, и это ок
val state by viewModel.state.collectAsState() // Состояние - стабильно

Child(state) // Передаем стабильный state
}

@Composable
fun Child(state: TestState) { // Стабильный пропс -> рекомпозиции оптимизированы
Text(state.data)
}


Compose-рантайм следит за изменениями в state. Сам ViewModel как объект не «пробрасывается» глубже и не триггерит лишних рекомпозиций.

🔹 Итоги

🔘 Для монолитных проектов: Это не проблема, компилятор видит все классы и корректно определяет стабильность.

🔘 Для многомодульных проектов: ViewModel почти всегда будет нестабильным из-за зависимостей из других модулей. И это нормально и безопасно, если вы передаете в композаблы не его самого, а его состояние.

Так что можете спать спокойно — с вашим кодом всё в порядке.

🐸 Библиотека мобильного разработчика

#АрхитектурныйКод #MiddlePath #Kotlin
Please open Telegram to view this post
VIEW IN TELEGRAM
1❤‍🔥1🔥1🤔1
✏️ adb shell am broadcast – отправка broadcast-интентов из командной строки

Мощный инструмент для тестирования обработки broadcast-сообщений в вашем приложении без необходимости эмулировать системные события через UI.

🔥 Базовое использование:

# Отправка кастомного broadcast
adb shell am broadcast -a "com.yourapp.action.CUSTOM_ACTION"

# С дополнительными данными
adb shell am broadcast -a "com.yourapp.action.CUSTOM_ACTION" --es "key" "value"


📌 Практические сценарии:

1. Тестирование системных событий:

# Имитация подключения зарядки
adb shell am broadcast -a android.intent.action.ACTION_POWER_CONNECTED

# Имитация изменения языка системы
adb shell am broadcast -a android.intent.action.LOCALE_CHANGED


2. Тестирование кастомных broadcast receivers:

# Отправка с дополнительными extras
adb shell am broadcast -a "com.yourapp.action.DATA_REFRESH" \
--es "refresh_type" "full" \
--ei "user_id" 12345 \
--ez "force_update" true


3. Отправка конкретному пакету:

# Только для вашего приложения
adb shell am broadcast -a "com.yourapp.action.TEST" -n com.yourapp.package/.ReceiverName


⚡️ Полезные системные действия:

Уведомление о низком заряде:

adb shell am broadcast -a android.intent.action.BATTERY_LOW


Изменение конфигурации:

adb shell am broadcast -a android.intent.action.CONFIGURATION_CHANGED


События времени:

adb shell am broadcast -a android.intent.action.TIME_SET


Какие broadcast-ы вы чаще всего тестируете через командную строку? 💬

🐸 Библиотека мобильного разработчика

#буст #MiddlePath #Android
Please open Telegram to view this post
VIEW IN TELEGRAM
🤝1
Gauge, ProgressView и Slider в SwiftUI

При создании интерфейсов на основе данных в SwiftUI вам часто будет требоваться визуализировать числовое значение или управлять им. Для этой цели можно использовать три встроенных представления — Gauge, ProgressView, и Slider.

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

🔹 Индикатор — отображение значения в диапазоне

Представленный в iOS 16 вид Gauge предназначен для отображения значения, а не для того, чтобы пользователи могли его изменять.

Он идеально подходит для случаев, когда вам нужен индикатор только для чтения, например для отображения уровня заряда батареи, температуры или загрузки процессора.

Gauge(value: currentTemp, in: 0...100) {
Text("Temperature")
} currentValueLabel: {
Text("\(Int(currentTemp))°C")
}
.tint(.orange)
Gauge(value: currentTemp, in: 0...100) {
Text("Temperature")
} currentValueLabel: {
Text("\(Int(currentTemp))°C")
}
.tint(.orange)


Индикатор может быть выполнен в нескольких стилях (.linearCapacity, .accessoryCircular и других) и естественным образом адаптируется к различным макетам — от круглых индикаторов в стиле приборной панели до компактных виджетов.

Используйте Gauge , когда:

🔘 Вам нужно визуализировать измеренное значение.
🔘 Взаимодействие с пользователем не предполагается.
🔘 Вам нужен единый стиль оформления системных виджетов и усложнений.

🔹 ProgressView — индикатор выполнения задачи

ProgressView предназначен для отслеживания прогресса, а не для отображения числовых значений. Он показывает, какая часть задачи выполнена, либо детерминированно (известная доля), либо неопределённо (индикатор вращения).

ProgressView(value: progress, total: 1.0)
.tint(.green)
ProgressView(value: progress, total: 1.0)
.tint(.green)


Он хорошо подходит для экранов загрузки/выгрузки, процессов адаптации или длительных операций.
Ключевое отличие от Gauge заключается в контексте: значение представляет не реальное измерение, а состояние процесса.

Используйте ProgressView , когда:

🔘 Вы визуализируете процент выполнения задачи.
🔘 Значение всегда стремится к 100 %.
🔘 Вам нужна стандартная системная анимация и обратная связь по доступности.

🔹 Ползунок — обеспечивает прямое управление пользователем

В отличие от Gauge и ProgressView, Slider позволяет вводить данные. Это правильный выбор, если вы хотите, чтобы пользователь мог установить или изменить числовое значение, например яркость, громкость или интенсивность фильтра.

Slider(value: $volume, in: 0...100) {
Text("Volume")
}
.tint(.blue)
Slider(value: $volume, in: 0...100) {
Text("Volume")
}
.tint(.blue)


Slider напрямую связывается со свойством состояния с помощью Binding, что делает его оптимальным вариантом для любой интерактивной числовой настройки.

Используйте Slider , когда:

🔘 Пользователь должен изменить значение.
🔘 Обратная связь должна быть оперативной и постоянной.
🔘 Вам нужно связать элементы управления пользовательского интерфейса с динамическими обновлениями (например, анимацией, предварительным просмотром).

🔹 Выбор правильного представления

При выборе из трёх вариантов:

🔘 Спросите, является ли значение измеренным, вычисленным или контролируемым пользователем.
🔘 Если значение измерено, используйте Gauge.
🔘 Если это означает завершение, используйте ProgressView.
🔘 Если пользователь манипулирует им, используйте Slider.

Каждый из этих видов соответствует принципам дизайна Apple, нап равленным на ясность и доступность. Понимание их сути поможет вам создавать интерфейсы, которые будут выглядеть правильно и вести себя естественно в экосистеме SwiftUI.

🔸 Курс «Основы IT для непрограммистов»
🔸 Получить консультацию менеджера
🔸 Сайт Академии 🔸 Сайт Proglib

🐸 Библиотека мобильного разработчика

#PixelPerfect #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
🎮 Как выполнить упрощённую миграцию в Core Data

Миграция базы данных требуется всякий раз, когда нам нужно внести изменения в модель Core Data. Для определённого набора изменений Core Data может выполнить почти автоматическую миграцию, которая называется облегчённой миграцией.

Это позволяет нам развивать нашу схему без потери существующих данных или необходимости вручную определять правила сопоставления.

Примеры поддерживаемых изменений: добавление, переименование или удаление сущностей, атрибутов или связей, а также изменение типов связей.

Когда изменения в нашей модели выходят за рамки того, что может определить Core Data, нам необходимо выполнить сложную (ручную) миграцию.

Давайте рассмотрим, как выполнить упрощённую миграцию в Core Data.

🔹 Как выполнить упрощённую миграцию в Core Data

Чтобы выполнить упрощённую миграцию в Core Data, необходимо выполнить следующие действия:

1. Включение облегченной миграции
2. Создание новой версии модели
3. Изменение новой модели

Давайте рассмотрим эти этапы более подробно.

1️⃣ Включение облегчённой миграции

Когда мы используем класс NSPersistentContainer для создания стека Core Data и управления им, нам не нужно выполнять никаких дополнительных действий по настройке, облегчённая миграция активируется автоматически.

Если мы создадим собственный стек Core Data вручную, то сможем явно включить эти параметры при добавлении хранилища:

let coordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
let options = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
do {
try coordinator.addPersistentStore(
ofType: NSSQLiteStoreType,
configurationName: nil,
at: url,
options: options
)
} catch {
// handle migration failure
}


Эти два параметра позволяют Core Data автоматически определять модель сопоставления и переносить данные при обнаружении совместимого изменения схемы.

2️⃣ Создание новой версии модели

Для автоматической миграции Core Data нужны исходная и конечная версии нашей модели данных.

Если мы изменим существующий .xcdatamodeld сразу после выпуска нашего приложения, Core Data не будет знать, как выполнить миграцию, и выдаст ошибку о несовместимости моделей.

Чтобы избежать этого, мы создаем новую версию модели, выбрав Редактор > Добавить версию модели в меню Xcode. Это добавит новый файл .xcdatamodel в наш проект.

После добавления новой версии мы можем установить текущую версию в инспекторе файлов Xcode. Теперь мы можем спокойно вносить изменения, не нарушая работу существующих хранилищ.

3️⃣ Изменение новой версии модели данных

Теперь мы можем вносить изменения в нашу модель. Чтобы Core Data могла создать предполагаемую модель сопоставления, изменения должны соответствовать определённому шаблону, например:

🔘 Добавление или удаление сущностей, атрибутов или связей.
🔘 Переименование сущностей, атрибутов или связей путем установки идентификатора переименования в качестве имени соответствующего свойства или сущности в предыдущей модели в инспекторе моделей данных Xcode.
🔘 Изменение обязательных атрибутов на необязательные или наоборот с указанием значения по умолчанию.
🔘 Изменение отношения «один ко многим» или неупорядоченного отношения на упорядоченное.
🔘 Чтобы проверить, может ли Core Data определить соответствие между двумя версиями модели, мы можем использовать:

let inferred = try? NSMappingModel.inferredMappingModel(
forSourceModel: oldModel,
destinationModel: newModel
)

if inferred != nil {
// Lightweight migration is possible
}


Если этот вызов вернёт nil, Core Data не сможет определить сопоставление, а это значит, что нам потребуется более сложная миграция вручную.

🔸 Курс «Архитектуры и шаблоны проектирования»
🔸 Получить консультацию менеджера
🔸 Сайт Академии 🔸 Сайт Proglib

🐸 Библиотека мобильного разработчика

#АрхитектурныйКод #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
1
💻 xcrun instruments – профилирование и анализ производительности iOS-приложений

Профессиональный инструмент для измерения производительности, поиска утечек памяти и анализа энергопотребления прямо из командной строки.

✏️ Основные возможности:

Запуск профилирования:

# Запуск Instruments с шаблоном Allocations
xcrun instruments -t "Allocations" -l 30000 MyApp.app

# Профилирование времени запуска
xcrun instruments -t "Time Profiler" MyApp.app -e UIResponder 30


Автоматизация тестов:

# Запуск с записью результатов в .trace файл
xcrun instruments -t "Time Profiler" -D performance.trace MyApp.app


📌 Практические сценарии:

1. Анализ утечек памяти:


# Запуск Leaks теста на 60 секунд
xcrun instruments -t "Leaks" MyApp.app -l 60000

# С сохранением результатов
xcrun instruments -t "Leaks" -D memory_leaks.trace MyApp.app -l 30000


2. Измерение времени запуска:


# Профилирование запуска приложения
xcrun instruments -t "Time Profiler" MyApp.app -e UIResponder 10 -D launch.trace


3. Анализ энергопотребления:

# Мониторинг использования батареи
xcrun instruments -t "Energy Log" MyApp.app -l 120000


⚡️ Полезные шаблоны Instruments:

🔘 Allocations – отслеживание памяти и объектов

🔘 Time Profiler – анализ производительности кода

🔘 Leaks – поиск утечек памяти

🔘 Energy Log – мониторинг энергопотребления

🔘 Network – анализ сетевой активности

🔘 Core Data – профилирование операций с базой данных

⚙️ Продвинутое использование:

Профилирование конкретного процесса:

# Присоединение к запущенному процессу
xcrun instruments -t "Time Profiler" -p 1234


Создание кастомных конфигураций:

# Сохранение конфигурации профилирования
xcrun instruments -t "Time Profiler" -c "My Custom Config" MyApp.app


Пакетное тестирование:

#!/bin/bash
# performance_test.sh
APP="MyApp.app"
TRACE_DIR="./traces"

mkdir -p $TRACE_DIR

echo "Running performance tests..."
xcrun instruments -t "Time Profiler" -D "$TRACE_DIR/time_profiler.trace" $APP -l 30000
xcrun instruments -t "Allocations" -D "$TRACE_DIR/allocations.trace" $APP -l 30000


Какие инструменты профилирования вы используете в своих проектах? 💬

🔸 Курс «Основы IT для непрограммистов»
🔸 Получить консультацию менеджера
🔸 Сайт Академии 🔸 Сайт Proglib

🐸 Библиотека мобильного разработчика

#буст #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
1🤝1
🎮 Локализация пакетов Swift в приложении для iOS

Современные архитектуры iOS часто основаны на модульности, а Swift Package Manager стал предпочтительным способом структурирования повторно используемых компонентов. Каждый модуль может содержать собственные ресурсы, в том числе локализованные строки, изображения и другие материалы. На бумаге это позволяет разрабатывать и выпускать каждую функцию независимо от других. Однако на практике локализация в Swift Packages не всегда работает так, как ожидают разработчики, после интеграции этих модулей в приложение.

Часто возникает одна и та же проблема: локализованные строки в пакете Swift работают во время разработки, но как только пакет используется в реальном приложении, все строки возвращаются к английскому варианту. Даже если пакет настроен правильно, локализованные .lproj папки игнорируются во время выполнения. Такое поведение может сбивать с толку, если вы ожидаете, что локализация будет работать автоматически во всех пакетах.

🔹 Почему игнорируются локализации в пакетах Swift

Пакеты Swift создаются в виде собственных наборов ресурсов, отдельных от основного набора ресурсов приложения. Несмотря на то, что .strings файлы хранятся в этих наборах и доступ к ним осуществляется через Bundle.module, система по-прежнему полагается на то, что хост-приложение объявляет, какие локализации оно поддерживает.

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

Это ожидаемое поведение системы локализации Apple, а не ошибка. Только основной пакет определяет языковые возможности приложения.

🔹 Необходимое исправление: объявление локализаций в основном приложении

Чтобы локализация Swift Package работала стабильно, в основном приложении необходимо объявить все поддерживаемые языки в Info.plist под ключом CFBundleLocalizations. После добавления этого списка система будет корректно обрабатывать локализованные строки из всех пакетов ресурсов, в том числе созданных с помощью Swift Package.

Минимальная конфигурация может выглядеть следующим образом:

<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>fr</string>
</array>


После добавления этой записи ранее игнорируемые .lproj папки внутри пакетов Swift Package начинают работать сразу.

🔹 Заключение

Локализация пакетов Swift Package не представляет сложности, если вы понимаете, как система определяет языки в пакетах. Даже если модули содержат собственные .lproj папки, основное приложение должно явно указывать все поддерживаемые локализации. Добавление CFBundleLocalizations в Info.plist решает проблему возврата к английскому языку и обеспечивает правильное поведение локализованных ресурсов из пакетов Swift Package.

🔸 Курс «Архитектуры и шаблоны проектирования»
🔸 Получить консультацию менеджера
🔸 Сайт Академии 🔸 Сайт Proglib

🐸 Библиотека мобильного разработчика

#АрхитектурныйКод #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
Добавление цветов в иерархию представлений SwiftUI

В SwiftUI одним из наиболее элегантных способов настройки визуальных элементов является внедрение стилей. Вместо того чтобы вручную передавать явные цвета или параметры стиля по дереву представлений, SwiftUI позволяет внедрять информацию о стиле на более высоком уровне с помощью таких модификаторов, как .foregroundStyle(), .backgroundStyle(), и .tint(). Эти внедренные стили могут использоваться любым дочерним представлением, которое ссылается на соответствующие динамические значения.

Этот подход не только лаконичен, но и позволяет комбинировать элементы и использовать декларативный подход, что полностью соответствует философии дизайна SwiftUI.

Внедрение стилей в подпредставления

Вот простой пример, демонстрирующий, как стили могут неявно передаваться по иерархии представлений:

struct DemoView: View {
var body: some View {
DetailView()
.foregroundStyle(.blue)
.backgroundStyle(.pink)
.tint(.yellow)
}
}


В этом примере DemoView применяет три модификатора стиля:

🔘 .foregroundStyle(.blue) определяет стиль переднего плана, который будет наследоваться дочерними элементами, ссылающимися на .foreground.
🔘 .backgroundStyle(.pink) добавляет стиль фона.
🔘 .tint(.yellow) устанавливает глобальный оттенок, который влияет на элементы, использующие .tint.

Ни один из этих модификаторов не ссылается явно на внутреннюю реализацию DetailView, что делает код модульным и слабосвязанным.

Использование внедренных стилей

Теперь давайте посмотрим, как DetailView использует эти стили:

struct DetailView: View {
var body: some View {
Text("Primary text")
.foregroundStyle(.background)
.background(.foreground)

Text("Secondary text")
.foregroundStyle(.tint)
}
}


Здесь:

🔘 .foregroundStyle(.background) указывает тексту использовать заданный стиль фона (в данном случае .pink).
🔘 .background(.foreground) устанавливает для фона текста стиль переднего плана (в данном случае .blue).
🔘 Во второй текстовой метке используется .foregroundStyle(.tint), которое соответствует .yellow в родительском представлении.

Такой подход позволяет создавать многократно используемые представления с учётом темы. Представлениям не нужно знать, какие конкретные цвета они будут отображать — им нужно лишь обращаться к динамическим стилям, определяемым средой, таким как .foreground, .background, или .tint.

Заключительные мысли

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

🔸 Курс «Основы IT для непрограммистов»
🔸 Получить консультацию менеджера
🔸 Сайт Академии 🔸 Сайт Proglib

🐸 Библиотека мобильного разработчика

#PixelPerfect #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
Принудительная перезагрузка представления в SwiftUI

В SwiftUI представления являются декларативными и автоматически перестраиваются при изменении их базового состояния. Однако бывают ситуации, когда требуется принудительно перезагрузить представление. Один из распространённых способов добиться этого — использовать модификатор .id(_:) в сочетании с управляемым состоянием идентификатором.

🔹 Пример реализации

Вот минимальный пример того, как можно принудительно перезагрузить представление:

struct DemoView: View {
@State private var viewId = UUID()

var body: some View {
VStack {
Text(viewId.uuidString)
.id(viewId) // forces SwiftUI to recreate this view

Button("Retry") {
viewId = UUID()
}
}
}
}


В этом примере при каждом нажатии на кнопку Retry viewId заменяется на новое UUID. Поскольку .id(viewId) прикреплено к тексту, SwiftUI считает его новым представлением и уничтожает старое поддерево, заменяя его новым.

🔹 Преимущества

Гарантированный сброс

Такой подход гарантирует, что представление и его внутреннее состояние будут полностью сброшены. Все @State или внутренние привязки в поддереве удаляются, и создаётся чистый экземпляр иерархии представлений.

Полезно для сценариев восстановления

Это может быть особенно полезно, когда вам нужно «полностью перезагрузить» представление после ошибки или при повторной попытке выполнить неудачный сетевой запрос. Вместо того чтобы вручную сбрасывать несколько переменных состояния, можно сгенерировать новый идентификатор, и это даст тот же результат, но с меньшим количеством кода.

Предсказуемое поведение

Поскольку SwiftUI в значительной степени полагается на идентификацию для согласования представлений, явное управление с помощью .id(_:) делает перезагрузку предсказуемой и понятной.

🔹 Недостатки

Снижение производительности

При принудительной перезагрузке старое представление удаляется и создаётся новое с нуля. Если иерархия представлений большая или сложная, это может привести к ненужным затратам ресурсов и снижению производительности рендеринга.

Потеря локального состояния

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

Решение, ориентированное на симптомы

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

🔹 Когда использовать

Принудительную перезагрузку представления следует рассматривать как крайнюю меру, а не как предпочтительное решение. Это полезно для повторных попыток, очистки поврежденного состояния или создания прототипов. В рабочем коде старайтесь моделировать поток состояний так, чтобы обновления SwiftUI происходили автоматически. Если вы часто прибегаете к .id(_:), возможно, стоит пересмотреть свой подход к управлению состоянием.

🔹 Заключение

Использование .id(_:) в сочетании с изменяющимся значением, например с UUID, — это простой способ принудительно перезагрузить представление SwiftUI. Это надёжный механизм для сброса настроек представления, но он имеет свои недостатки с точки зрения производительности и потери состояния. Как и большинство методов, его лучше применять выборочно, в тех случаях, когда требуется настоящий «сброс» настроек, а другие подходы менее эффективны.

🔸 Курс «Основы IT для непрограммистов»
🔸 Получить консультацию менеджера
🔸 Сайт Академии 🔸 Сайт Proglib

🐸 Библиотека мобильного разработчика

#PixelPerfect #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
🤫 Не используйте Struct в Swift таким образом — это вас замедляет

Структуры в Swift легковесны и быстры, но их неправильное применение бьет по производительности. Разбираем главные ошибки.

1️⃣ Копирование больших данных

Структуры — типы значений. При передаче или присваивании создается копия:

struct UserProfile {
let name: String
let bio: String
let posts: [Post] // Большой массив
let followers: [Follower]
let following: [Following]
}


Обновление bio вынудит скопировать все массивы, даже если меняем одно поле.

Решение: Выносим тяжёлые данные в класс:

final class UserDataStore {
var posts: [Post] = []
var followers: [Follower] = []
var following: [Following] = []
}

struct UserProfile {
let name: String
var bio: String
let store: UserDataStore // Общая ссылка
}


Теперь копируются только name и bio, а массивы используются совместно.

2️⃣ Copy-on-Write — ваше спасение

Для стандартных типов (String, Array, Dictionary) Swift применяет оптимизацию Copy-on-Write: физическое копирование происходит только при изменении.

Пишите в структуры сколько угодно [String], [Int] и т.д. — пока не меняете их, копирования не будет.

3️⃣ Не ожидайте общего состояния

Структуры не разделяют состояние:

struct Counter { var count = 0 }

var a = Counter()
var b = a
a.count += 1
print(b.count) // 0, а не 1


Для общего состояния нужен класс.

Итог:

🔘 Дробите тяжёлые структуры, выносите данные в классы.
🔘 Доверяйте COW для стандартных типов.
🔘 Не путайте value- и reference-семантику.

Правильное использование структур сохранит скорость и безопасность вашего кода.

🔹 Курс «Основы IT для непрограммистов»
🔹 Получить консультацию менеджера
🔹 Сайт Академии 🔹 Сайт Proglib

🐸 Библиотека мобильного разработчика

#буст #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
3
Анимированные полосы в Jetpack Compose

Коллеги, давайте разберем, как легко рисовать и анимировать полосатые узоры в Jetpack Compose с помощью всего лишь одной умной функции.

🔹 Основная идея

Всё строится на использовании Brush.linearGradient() и параметра colorStops. Секрет в том, чтобы разместить две цветовые точки с разными цветами в одной позиции. Это создаст не плавный переход, а резкую границу между полосами.

🔹 От простого к сложному

1. Создаем две половинки:

Brush.linearGradient(
0.0f to Color.Black, // Начало черного
0.5f to Color.Black, // Конец черного (резкий переход!)
0.5f to Color.White, // Начало белого (в той же точке)
1.0f to Color.White // Конец белого
)


2. Рисуем повторяющийся узор:

Добавляем TileMode.Repeated и задаем размер одного повторения через start и end:

start = Offset(0f, 0f),
end = Offset(20f, 0f),
tileMode = TileMode.Repeated


3. Анимируем смещением:

Двигаем начальную и конечную точки с помощью animatedOffset — и полосы "побежали".

🔹 Готовое решение: функция Brush.stripes()

Чтобы не возиться с colorStops каждый раз, можно создать удобную функцию-расширение:

fun Brush.Companion.stripes(
vararg stripes: Pair<Color, Float>, // Цвет и его "вес"
width: Float = 20f, // Ширина одного повторения
angle: Float = 45f, // Угол наклона
phase: Float = 0f // Сдвиг для анимации
): Brush { ... }


Использовать — одно удовольствие:

// Равные полосы
Brush.stripes(
Color.Pink to 1f,
Color.Transparent to 1f,
width = 10.dp.toPx(),
angle = 45f
)

// Разные ширины и цвета
Brush.stripes(
Color.Red to 1f,
Color.Blue to 2f, // В 2 раза шире
Color.Green to 1f
)


🔹 Пример анимации загрузки

val phase by rememberInfiniteTransition()
.animateFloat(0f, 1f, animationSpec = infiniteRepeatable(tween(300)))

Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.drawBehind {
drawRect(
brush = Brush.stripes(
White to 1f,
Zinc900 to 1f,
width = 10.dp.toPx(),
angle = 45f,
phase = -phase // Анимируем здесь!
)
)
}
)


🔹 Итог

Техника простая, но мощная:

Основа — резкие градиенты через colorStops
ПовторениеTileMode.Repeated
Анимация — смещение phase или точек
Упрощение — своя функция Brush.stripes()

Отлично подходит для индикаторов загрузки, фоновых текстур, визуальных эффектов.

📌 Лучшие вакансии для мобильных разработчиков

🐸 Библиотека мобильного разработчика

#PixelPerfect #MiddlePath #Kotlin
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🎮 Transferable в Swift: как ваши данные могут путешествовать между приложениями

Протокол Transferable в Swift — это мощный инструмент, который позволяет вашим пользовательским типам данных легко участвовать в системных операциях, таких как перетаскивание (Drag & Drop), копирование-вставка и общий доступ. По сути, он «учит» систему, как преобразовать вашу модель в данные для передачи и как восстановить её на другом конце.

🔹 Зачем это нужно?

🔘 Совместное использование данных: Пользователи могут копировать ваши объекты, перетаскивать их и делиться ими с другими приложениями.

🔘 Работа с системными элементами UI: Ваши типы автоматически становятся совместимыми с PasteButton, ShareLink и модификаторами для Drag & Drop.

🔘 Универсальность: Один и тот же объект может быть представлен по-разному (например, как полные данные для Drop и как текст для вставки в текстовое поле).

🔹 Как это работает?

Суть в реализации вычисляемого свойства transferRepresentation. Вам нужно описать, как ваш тип «экспортируется» в данные и «импортируется» обратно.

Основные способы представления:

🔘 CodableRepresentation: Самый простой способ, если ваш тип уже соответствует Codable.
🔘 DataRepresentation: Для преобразования объекта в бинарные данные (например, Data).
🔘 FileRepresentation: Если ваш тип представляет собой файл (изображение, PDF).
🔘 ProxyRepresentation: Позволяет использовать другой Transferable-тип (например, String) для представления вашего объекта в определённых контекстах.

🔹 Практический пример

Допустим, у нас есть структура Post, и мы хотим, чтобы её можно было копировать как текст.

import SwiftUI
import UniformTypeIdentifiers

struct Post: Codable, Transferable {
var text: String

static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .plainText) { post in
// Экспорт: Post -> Data
post.text.data(using: .utf8) ?? Data()
} importing: { data in
// Импорт: Data -> Post
let content = String(decoding: data, as: UTF8.self)
return Post(text: content)
}
// Дополнительно: можно добавить текстовое представление
ProxyRepresentation(exporting: \.title)
}
}


🔹 Итог и совет

🔘 Результат: После добавления соответствия протоколу Transferable ваш тип можно использовать с ShareLink(post), PasteButton(payload: post), а также сделать его перетаскиваемым с модификатором .draggable(post).

🔘 Ключевой выбор: Основная задача — правильно выбрать тип Representation, который лучше всего подходит для вашей модели данных (Codable, Data, File или Proxy).

🔘 Для собственных форматов: Если вы создаёте уникальный формат данных, не забудьте объявить для него Uniform Type Identifier (UTI) в настройках проекта.

Этот протокол открывает двери к гораздо более глубокой и естественной интеграции вашего приложения в экосистему iOS, iPadOS и macOS, делая взаимодействие с данными плавным и интуитивно понятным для пользователя.

📌 Лучшие вакансии для мобильных разработчиков

🐸 Библиотека мобильного разработчика

#АрхитектурныйКод #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
1
🔴 Как распознать нажатие на произвольную фигуру в Jetpack Compose

В Android-разработке определение нажатия на сложную нестандартную фигуру — нетривиальная задача. Класс Path не предоставляет готового API, а распространённые решения с проверкой пересечения маленького прямоугольника и фигуры — медленные и не всегда точные.

В Jetpack Compose есть простое и эффективное решение: PathHitTester.

🔹 Как это работает

1. Создайте хит-тестер, передав ему ваш Path:

val hitTester = PathHitTester(imagePath)


2. Проверяйте координаты нажатия с помощью метода contains(). Важно привести координаты касания к той же системе координат, что и путь:

val tap = down.position - imagePosition // Применяем обратное преобразование
val isTapped = tap in hitTester // Вызывает hitTester.contains(tap)


🔹 Ключевые особенности и преимущества

🔘 Точность: Определяет нажатие с точностью до пикселя.

🔘 Производительность: Использует специальную структуру данных для быстрой проверки, что позволяет использовать PathHitTester даже для обработки событий перетаскивания (drag) с высокой частотой.

🔘 Переиспользование: Чтобы обновить тестируемый путь, используйте метод update(newPath) вместо создания нового объекта.

🔹 Получение пути из изображения

В демонстрационном видео используется библиотека pathway. Её основная функция — генерация Path из Bitmap:

val contour = bitmap.toPath().asComposePath()


Эта операция находит контур по прозрачным пикселям, поэтому изображение должно иметь прозрачный фон.

🔹 Итог

PathHitTester — это мощный и простой API Compose, который решает старую проблему детектирования нажатий на сложные фигуры. Он быстрый, точный и избавляет вас от необходимости писать громоздкие и ненадёжные костыли.

📌 Лучшие вакансии для мобильных разработчиков

🐸 Библиотека мобильного разработчика

#PixelPerfect #MiddlePath #Kotlin
Please open Telegram to view this post
VIEW IN TELEGRAM
🎮 Как избежать циклов сохранения при работе с задачами в Swift

Всякий раз, когда мы создаем задачу, например с помощью Task {}, Swift фиксирует все значения, на которые мы ссылаемся в теле задачи. По умолчанию эти фиксации являются сильными.

Это означает:

• Задача сохраняет захваченные значения до завершения задачи.
• Если мы захватываем self сильным захватом, а self также содержит ссылку на задачу, мы создаём цикл сохранения.
• Но важно отметить, что этот цикл длится только в течение времени существования задачи.

Давайте рассмотрим конкретный пример:

класс ImageLoader {
приватное поле task: Задача<Пустота, Никогда>?

func load() {
задача = Задача {
ожидание загрузка изображения()
}
}

func loadImage() async {
// ...
}
}


То, что здесь происходит, это:

• экземпляр ImageLoader строго удерживает задачу (поскольку задача является сохранённым свойством)
• задача строго удерживает своё замыкание
• замыкание строго удерживает self (поскольку мы вызываем downloadImages())

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

ImageLoaderTaskImageLoader

Однако у задач есть важное отличие от классических обработчиков сохранённых завершений: задача освобождает своё замыкание после завершения.

Это означает:

• В этом примере цикл длится ровно столько, сколько загружается изображение.
• Когда задача завершается, она отменяет своё закрытие и, следовательно, освобождает self.

Если мы хотим избежать даже этого временного цикла, мы можем использовать слабую ссылку на self с помощью [weak self], но для краткосрочных задач это обычно необязательно.

🔹 Там, где циклы сохранения приводят к утечкам памяти

Временные циклы сохранения редко становятся проблемой, но с постоянными циклами мы можем столкнуться с утечками памяти. Это происходит, когда выполняются все три условия:

1. Мы сохраняем задачу (как свойство).
2. Замыкание задачи сильно привязывает себя к ней.
3. Задача никогда не завершается или завершается намного позже, чем мы ожидаем.

Это может происходить в длительных циклах или при потоковой обработке:

класс MessageListener {
приватное поле task: Задача<Пустота, Никогда>?

func start() {
задача = Задача { [слабый я] в
for await сообщение in await сообщения .stream() {
guard let self else { return }
self.handle(message)
}
}
}

функция обработчик(_ сообщение: Сообщение) { }
}


В этом примере цикл может выполняться бесконечно. Без [weak self] задача сохраняет self в живых, а self сохраняет задачу в живых, образуя классический цикл сохранения.

Использование слабого захвата прерывает этот цикл и гарантирует, что задача будет завершена естественным образом, как только слушатель выйдет из области видимости.

📌 Лучшие вакансии для мобильных разработчиков

🐸 Библиотека мобильного разработчика

#АрхитектурныйКод #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥31