Меню в 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