Please open Telegram to view this post
VIEW IN TELEGRAM
❤2
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 часто используются для быстрых одноразовых команд: выберите пункт, выполните действие — и всё готово. Но что, если вы хотите, чтобы меню функционировало не как простой селектор, а как мини-панель настроек, где пользователи могут последовательно переключать несколько пунктов, прежде чем сделать окончательный выбор? По умолчанию меню закрывается, как только пользователь нажимает на пункт, но с помощью подходящего 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
Please open Telegram to view this post
VIEW IN TELEGRAM
ToolbarContent и @ToolbarContentBuilderПо мере роста проектов SwiftUI одна из частых проблем — управление сложными иерархиями представлений. Даже простой экран может быстро превратиться в десятки вложенных модификаторов. После снятия лимита в 10 вложенных представлений стало проще писать глубоко вложенные
body, но код стал труднее читать и сопровождать.🔹 Разбираем крупные реализации body
Когда
body растягивается на десятки строк, страдает читаемость. Лучше разбивать большие представления SwiftUI на мелкие подпредставления или выделять повторно используемые части в вычисляемые свойства или функции. Это сохраняет лаконичность и упрощает понимание, тестирование и повторное использование.Тот же принцип применим к невизуальным элементам, например панелям инструментов, которые быстро разрастаются при добавлении множества кнопок и пунктов меню.
🔹 Проблема с панелями инструментов
Модификатор
.toolbar позволяет создавать кнопки, меню и элементы управления, адаптируемые под разные платформы. Но если элементов становится много, код внутри .toolbar { ... } быстро теряет читаемость.Перенести логику в вычисляемое свойство нельзя —
.toolbar ожидает содержимое, соответствующее ToolbarContent.🔹
ToolbarContent и @ToolbarContentBuilderSwiftUI решает это с помощью:
•
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
ЭЛТ-мониторы — это размытые края, линии сканирования и лёгкое свечение. Такой эффект можно воспроизвести в 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
А вы знали, что почти все ViewModels нестабильны?
Когда мы только изучаем Compose,
нас учат использовать стабильный класс, а не нестабильный.
Но ViewModels нестабильны. Так почему же никто ничего не говорит о том, что мы используем нестабильные ViewModels?
🔹 Как Compose определяет стабильность?
Компилятор Compose считает класс стабильным, если:
val/var) неизменяемы (val).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 как объект не «пробрасывается» глубже и не триггерит лишних рекомпозиций.🔹 Итоги
Так что можете спать спокойно — с вашим кодом всё в порядке.
#АрхитектурныйКод #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
При создании интерфейсов на основе данных в 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 , когда:🔹 Ползунок — обеспечивает прямое управление пользователем
В отличие от
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, необходимо выполнить следующие действия:
1. Включение облегченной миграции
2. Создание новой версии модели
3. Изменение новой модели
Давайте рассмотрим эти этапы более подробно.
Когда мы используем класс 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 автоматически определять модель сопоставления и переносить данные при обнаружении совместимого изменения схемы.
Для автоматической миграции Core Data нужны исходная и конечная версии нашей модели данных.
Если мы изменим существующий
.xcdatamodeld сразу после выпуска нашего приложения, Core Data не будет знать, как выполнить миграцию, и выдаст ошибку о несовместимости моделей.Чтобы избежать этого, мы создаем новую версию модели, выбрав
Редактор > Добавить версию модели в меню Xcode. Это добавит новый файл .xcdatamodel в наш проект.После добавления новой версии мы можем установить текущую версию в инспекторе файлов 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
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
Современные архитектуры 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 позволяет внедрять информацию о стиле на более высоком уровне с помощью таких модификаторов, как
.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 представления являются декларативными и автоматически перестраиваются при изменении их базового состояния. Однако бывают ситуации, когда требуется принудительно перезагрузить представление. Один из распространённых способов добиться этого — использовать модификатор
.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
Структуры в Swift легковесны и быстры, но их неправильное применение бьет по производительности. Разбираем главные ошибки.
Структуры — типы значений. При передаче или присваивании создается копия:
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, а массивы используются совместно.Для стандартных типов (
String, Array, Dictionary) Swift применяет оптимизацию Copy-on-Write: физическое копирование происходит только при изменении.Пишите в структуры сколько угодно
[String], [Int] и т.д. — пока не меняете их, копирования не будет.Структуры не разделяют состояние:
struct Counter { var count = 0 }
var a = Counter()
var b = a
a.count += 1
print(b.count) // 0, а не 1Для общего состояния нужен класс.
Итог:
Правильное использование структур сохранит скорость и безопасность вашего кода.
🔹 Курс «Основы IT для непрограммистов»
🔹 Получить консультацию менеджера
🔹 Сайт Академии 🔹 Сайт Proglib
#буст #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3
Коллеги, давайте разберем, как легко рисовать и анимировать полосатые узоры в 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 — это мощный инструмент, который позволяет вашим пользовательским типам данных легко участвовать в системных операциях, таких как перетаскивание (Drag & Drop), копирование-вставка и общий доступ. По сути, он «учит» систему, как преобразовать вашу модель в данные для передачи и как восстановить её на другом конце.🔹 Зачем это нужно?
PasteButton, ShareLink и модификаторами для Drag & 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).Этот протокол открывает двери к гораздо более глубокой и естественной интеграции вашего приложения в экосистему iOS, iPadOS и macOS, делая взаимодействие с данными плавным и интуитивно понятным для пользователя.
#АрхитектурныйКод #MiddlePath #Swift
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
В 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) с высокой частотой.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
Всякий раз, когда мы создаем задачу, например с помощью
Task {}, Swift фиксирует все значения, на которые мы ссылаемся в теле задачи. По умолчанию эти фиксации являются сильными.Это означает:
• Задача сохраняет захваченные значения до завершения задачи.
• Если мы захватываем
self сильным захватом, а self также содержит ссылку на задачу, мы создаём цикл сохранения.• Но важно отметить, что этот цикл длится только в течение времени существования задачи.
Давайте рассмотрим конкретный пример:
класс ImageLoader {
приватное поле task: Задача<Пустота, Никогда>?
func load() {
задача = Задача {
ожидание загрузка изображения()
}
}
func loadImage() async {
// ...
}
}То, что здесь происходит, это:
• экземпляр
ImageLoader строго удерживает задачу (поскольку задача является сохранённым свойством)• задача строго удерживает своё замыкание
• замыкание строго удерживает
self (поскольку мы вызываем downloadImages())Таким образом, пока задача выполняется, у нас есть цикл сохранения:
ImageLoader → Task → ImageLoaderОднако у задач есть важное отличие от классических обработчиков сохранённых завершений: задача освобождает своё замыкание после завершения.
Это означает:
• В этом примере цикл длится ровно столько, сколько загружается изображение.
• Когда задача завершается, она отменяет своё закрытие и, следовательно, освобождает
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
🔥3❤1