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

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

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

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

РКН: https://gosuslugi.ru/snet/67a4adec1b17b35b6c0d8389
Download Telegram
⚙️ Превращение меню 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