Пользователь нажимает на скрепку, выбирает фото из галереи — и ждёт. Если за это время ничего не произошло, он нажимает ещё раз. Такой сценарий мы часто видим, когда загрузка изображения реализована без очереди и прогресс-индикатора. Дальше — дубли в чате, крэш на slow network, отзыв в 1 звезду. Наш опыт показывает, что даже простая отправка фото требует внимания к деталям: правильный pipeline избавляет от 90% проблем с производительностью и UX. Свяжитесь с нами, чтобы реализовать надёжную отправку изображений в вашем приложении.
Реализация отправки изображений в чате: почему нужен правильный pipeline?
Самая частая ошибка — загружать оригинал напрямую. На современных смартфонах фото из камеры весит 4–12 МБ. Отправить такое в чат через multipart/form-data без предварительного сжатия означает: долгое ожидание, зависание UI на main thread (если сжатие вынесено туда же), и дублирование при повторной отправке после таймаута.
Почему наивная реализация приводит к ошибкам?
На iOS типичная реализация через PHPickerViewController (iOS 14+) отдаёт NSItemProvider, из которого нужно асинхронно получить UIImage. Если сделать это синхронно в completion handler и сразу вызвать ImageIO для ресайза — интерфейс подвиснет на ~300 мс на iPhone 12, и ещё дольше на SE 2nd gen. Правильный путь: получить данные в фоне через loadObject(ofClass:), затем пробросить в DispatchQueue.global(qos:.userInitiated) для сжатия через vImage или UIGraphicsImageRenderer с target size под экран получателя. Apple Developer Documentation
На Android аналогичная проблема с ActivityResultContracts.GetContent(): если декодировать Bitmap на главном потоке через BitmapFactory.decodeStream() без inSampleSize — OutOfMemoryError на устройствах с 2 ГБ RAM при выборе нескольких фото подряд.
Как выстроить надёжный pipeline для отправки изображений?
Полная цепочка включает следующие шаги:
-
Выбор и валидация. На iOS —
PHPickerViewControllerсfilter:.images, лимит выбора черезselectionLimit. Проверяем MIME-тип черезUTTypeдо загрузки данных. На Android —PhotoPicker API(Android 13+) илиIntent(Intent.ACTION_PICK)для более старых версий; проверка черезContentResolver.getType(). В React Native используемreact-native-image-pickerс опциейmediaType: 'photo'. -
Сжатие. Цель — не более 1 МБ для превью в чате. На iOS:
UIGraphicsImageRendererс target size 1280×1280,jpegData(compressionQuality: 0.75). На Android:Bitmap.createScaledBitmap()+compress(Bitmap.CompressFormat.JPEG, 80, outputStream). В Flutter используемflutter_image_compress— он вызывает нативный кодек, поэтому не блокирует Dart isolate. -
Upload с прогрессом. Многочастная загрузка через
URLSession.uploadTask(with:from:)на iOS с делегатомurlSession(_:task:didSendBodyData:)для прогресса. На Android —OkHttpсRequestBody.create()и кастомнымCountingRequestBody. В React Native удобнееaxiosсonUploadProgress, но нужно помнить: прогресс-событие срабатывает на JS-потоке, обновление state надо дебаунсировать. -
Оптимистичный UI. Показываем thumbnail сразу после выбора, статус "загружается" — пока идёт upload. Если запрос упал — не удаляем сообщение, а показываем кнопку retry. Для этого у каждого сообщения должен быть локальный
localIdи статус (pending/sent/failed). -
Полноэкранный просмотр. На iOS —
UIScrollView+UIImageViewс pinch-to-zoom черезUIPinchGestureRecognizer. Ленивая загрузка оригинала при открытии черезSDWebImageилиKingfisher. На Android —PhotoViewlibrary илиZoomableImageViewизcoil+accompanist.
| Платформа | Инструмент | Ресайз | Качество | Итоговый размер (на 12 МБ фото) |
|---|---|---|---|---|
| iOS | UIGraphicsImageRenderer | 1280×1280 | 0.75 | ~800 КБ |
| Android | Bitmap.compress + createScaledBitmap | 1280×1280 | 80% | ~900 КБ |
| Flutter | flutter_image_compress | maxWidth:1280 | 80 | ~850 КБ |
| React Native | react-native-image-resizer | maxWidth:1280 | 80 | ~850 КБ |
Закажите внедрение pipeline в ваше приложение уже сегодня.
Хранение и CDN
Изображения не хранятся в базе данных. Загружаем в S3-совместимое хранилище (AWS S3, Cloudflare R2, MinIO), в чат пишем только URL. Для превью генерируем thumbnail на стороне сервера через Lambda/Cloud Function при загрузке — это избавляет клиент от повторного сжатия при отображении списка сообщений.
Подписанные URL (presigned URL) с TTL 1–24 часа — обязательно, если чат приватный. На клиенте кэшируем через NSCache (iOS) или DiskLruCache (Android). Мы используем CDN с edge-кэшированием для быстрой доставки даже в регионы с высокой задержкой.
Процесс и сроки
Базовая реализация (выбор, сжатие, upload, thumbnail, полноэкранный просмотр) — 2–3 дня при готовом бэкенде. Если нужен мультивыбор (до 10 фото), очередь загрузки с паузой/возобновлением и поддержка GIF — ещё 1–2 дня. Стоимость рассчитывается индивидуально после анализа требований.
Что входит в работу?
- Документация API и интеграции
- Исходный код pipeline с комментариями
- Настройка CDN и presigned URL
- Интеграция с существующим бэкендом (REST/GraphQL)
- Тестирование на slow network и edge-cases
- Поддержка при релизе в App Store и Google Play
Типичные ошибки и их решения
| Ошибка | Последствия | Решение |
|---|---|---|
| Загрузка оригинала без сжатия | Долгое ожидание, расход трафика | Сжатие до 1 МБ |
| Синхронная обработка на main thread | Зависание UI | Асинхронная обработка в фоне |
| Отсутствие progress-индикатора | Повторные нажатия, дубли | Прогрессбар и блокировка кнопки |
| Нет повторной отправки при ошибке | Потеря фото, негатив | Retry с локальным статусом failed |
| Игнорирование кэширования thumbnail | Частые загрузки, тормоза | Кэш через Kingfisher/Coil |
| Неиспользование presigned URL | Угроза безопасности приватных чатов | Подписанные URL с TTL |
Получите консультацию по реализации отправки изображений — бесплатно.







