Імплементація галереї зображень у мобільному застосунку
Галерея видається простим компонентом — поки не приходить реальний контент. Користувач завантажує 600 фотографій з відпустки, листує сітку, і через секунду застосунок отримує memory warning на iPhone 12. Проблема не в кількості фото — вона в тому, як UICollectionView або RecyclerView керують декодуванням та кешуванням зображень.
Де частіше всього ломається
Синхронна декодування на main thread. UIImage(data:) всередину cellForItemAt — це блокування головного потоку. На сітці 3×3 з фото 4K це помітно одразу: підергування при скролі, просадка FPS до 30 на iPhone SE 2-го покоління. Рішення — ImageRenderer з preparingThumbnail(of:) або Kingfisher з processor: DownsamplingImageProcessor. На Android аналогічно: Glide з override(targetWidth, targetHeight) робить downsampling до розміру відображення, а не грузить 12 МП оригінал в bitmap.
Неправильний розмір кеша. NSCache без ліміту — це не кеш, це утечка під навантаженням. Типова історія: NSCache.countLimit = 100, але кожен об'єкт — UIImage на 8 МБ, разом 800 МБ у RAM на пристрої з 4 ГБ. Правильно — ліміцювати по totalCostLimit в байтах, а не по кількості.
Життєвий цикл ячейок та cancelled tasks. При швидкому скролі в UICollectionView ячейка переиспользується раніше, ніж завершилася завантаження попередньої. Якщо не відмінити URLSessionDataTask при prepareForReuse, в ячейку вставиться чужа фото. SDWebImage вирішує через sd_cancelCurrentImageLoad() в prepareForReuse, Kingfisher — через привязку завдання до ImageView.kf.
Як будуємо галерею
Для iOS — Kingfisher 7.x як основний image pipeline: вбудований memory + disk cache, DownsamplingImageProcessor для thumbnail, FadeTransition при завантаженні. Для складних анімацій переходів між сіткою та full-screen переглядом — UIViewControllerTransitioningDelegate з UIPresentationController, hero-анімація через matched geometry effect у SwiftUI.
На Android — Coil 2.x (Kotlin-native, Coroutines-based). AsyncImage у Compose з rememberAsyncImagePainter, diskCachePolicy = CachePolicy.ENABLED, size = Size.ORIGINAL лише для full-screen перегляду. У LazyVerticalGrid — contentScale = ContentScale.Crop з явним size на рівні модифікатора.
Для full-screen перегляду: PhotoView на Android (pinch-to-zoom через Matrix трансформації), MagnificationGesture + ScrollView у SwiftUI або кастомний UIPinchGestureRecognizer у UIKit. Свайп вниз для закриття — інтерактивний dismiss з UIPercentDrivenInteractiveTransition.
// iOS — downsampling при завантаженні thumbnail
let processor = DownsamplingImageProcessor(size: thumbnailSize)
imageView.kf.setImage(
with: url,
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.cacheOriginalImage,
.transition(.fade(0.2))
]
)
Вибір та завантаження з пристрою. На iOS — PHPickerViewController (iOS 14+, без запиту дозволів на повну бібліотеку). На Android — ActivityResultContracts.PickMultipleVisualMedia (Photo Picker API, Android 13+) або ACTION_OPEN_DOCUMENT для старих версій. Обидва варіанти повертають URI, з яких потрібно створювати копії у внутрішньому сховищі перед завантаженням на сервер — інакше URI протухне після перезапуску застосунку.
Offline та синхронізація. Локальний кеш на диску — SQLite через Room/CoreData для метаданих (id, url, localPath, syncStatus), файли в FileManager.default.urls(for: .cachesDirectory) / context.getExternalFilesDir(). При відновленні мережі — черга завантажень через BGTaskScheduler (iOS) або WorkManager (Android).
Що входить у роботу
- Сіточний layout з підтримкою різних пропорцій (квадрати, masonry, адаптив)
- Full-screen перегляд з pinch-to-zoom та swipe-to-dismiss
- Завантаження з пристрою через системний picker
- Завантаження/скачування з сервера з прогрес-індикатором
- Memory + disk кеш з правильними лімітами
- Offline-режим для попередньо завантажених зображень
Строки
2–3 робочих дні для стандартної галереї. Складні анімовані переходи, masonry layout з різними пропорціями або інтеграція з хмарним сховищем — до 5 днів. Вартість розраховується індивідуально.







