Implementing Image Gallery in Mobile Application
Gallery looks like a simple component until real content arrives. User uploads 600 vacation photos, scrolls the grid, and a second later app gets memory warning on iPhone 12. Problem isn't quantity — it's how UICollectionView or RecyclerView manage image decoding and caching.
Common Failure Points
Synchronous decoding on main thread. UIImage(data:) inside cellForItemAt blocks main thread. On 3×3 grid with 4K photos, it's immediately noticeable: stuttering on scroll, FPS drop to 30 on iPhone SE 2nd gen. Solution — ImageRenderer with preparingThumbnail(of:) or Kingfisher with processor: DownsamplingImageProcessor. On Android similarly: Glide with override(targetWidth, targetHeight) downsamples to displayed size, not loading 12MP original into bitmap.
Wrong cache size. NSCache without limit isn't a cache, it's a leak under load. Typical story: NSCache.countLimit = 100, but each object is UIImage at 8 MB, total 800 MB in RAM on 4GB device. Correctly — limit by totalCostLimit in bytes, not by count.
Cell lifecycle and cancelled tasks. On fast scroll in UICollectionView, cell reuses before previous image load finishes. Without canceling URLSessionDataTask at prepareForReuse, wrong photo inserts. SDWebImage solves via sd_cancelCurrentImageLoad() in prepareForReuse, Kingfisher — via ImageView.kf binding.
Building Gallery
For iOS — Kingfisher 7.x as main image pipeline: built-in memory + disk cache, DownsamplingImageProcessor for thumbnail, FadeTransition on load. For complex transition animations between grid and fullscreen — UIViewControllerTransitioningDelegate with UIPresentationController, hero animation via matched geometry effect in SwiftUI.
On Android — Coil 2.x (Kotlin-native, Coroutines-based). AsyncImage in Compose with rememberAsyncImagePainter, diskCachePolicy = CachePolicy.ENABLED, size = Size.ORIGINAL only for fullscreen. In LazyVerticalGrid — contentScale = ContentScale.Crop with explicit size at modifier level.
For fullscreen: PhotoView on Android (pinch-to-zoom via Matrix transform), MagnificationGesture + ScrollView in SwiftUI or custom UIPinchGestureRecognizer in UIKit. Swipe down to close — interactive dismiss with UIPercentDrivenInteractiveTransition.
// iOS — downsampling on thumbnail load
let processor = DownsamplingImageProcessor(size: thumbnailSize)
imageView.kf.setImage(
with: url,
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.cacheOriginalImage,
.transition(.fade(0.2))
]
)
Selection and upload from device. iOS — PHPickerViewController (iOS 14+, no full library permission request). Android — ActivityResultContracts.PickMultipleVisualMedia (Photo Picker API, Android 13+) or ACTION_OPEN_DOCUMENT for older. Both return URI; create copies in internal storage before upload — otherwise URI expires after app restart.
Offline and sync. Local disk cache — SQLite via Room/CoreData for metadata (id, url, localPath, syncStatus), files in FileManager.default.urls(for: .cachesDirectory) / context.getExternalFilesDir(). On network recovery — upload queue via BGTaskScheduler (iOS) or WorkManager (Android).
What's Included
- Grid layout with different ratios support (squares, masonry, responsive)
- Fullscreen view with pinch-to-zoom and swipe-to-dismiss
- Load from device via system picker
- Upload/download from server with progress
- Memory + disk cache with correct limits
- Offline mode for previously loaded images
Timeline
2–3 working days for standard gallery. Complex animated transitions, masonry with different ratios or cloud storage integration — up to 5 days. Cost calculated individually.







