Почему стандартные контроллеры часто не справляются
Для большинства приложений, которые позволяют пользователю выбирать фотографии или видеоклипы, первое решение — воспользоваться готовыми контроллерами Apple: UIImagePickerController и PHPickerViewController. Они действительно покрывают базовый набор функций: открывают медиатеку, позволяют выбрать один или несколько элементов, автоматически обрабатывают разрешения. Однако в реальном продукте требования часто выходят за рамки «один‑клик».
- Большие библиотеки – в современных iPhone и iPad библиотеки могут содержать десятки тысяч медиафайлов. Стандартные контроллеры начинают «тормозить» при скролле, а их UI ограничен в кастомизации.
- Сложные сценарии выбора – необходимо поддерживать предвыбор, мультиселект с ограничением количества, отображать кастомные индикаторы (например, статус загрузки в облаке).
- Интеграция с архитектурой приложения – часто требуется, чтобы процесс выбора был полностью реактивным и легко вписывался в поток данных, управляемый Combine или RxSwift.
Эти ограничения заставляют команды переходить к собственным решениям, построенным поверх Photos‑фреймворка.
Архитектура кастомного галерейного модуля
1. Работа с PHAsset и PHFetchResult
Photos‑фреймворк предоставляет доступ к медиа через объекты PHAsset. Для получения списка всех изображений используется запрос:
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let assets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
PHFetchResult – ленивый контейнер, который не загружает сами изображения, а лишь хранит ссылки на них. Это позволяет работать с миллионами записей, не занимая память.
2. Lazy‑загрузка превью
Главный узел производительности – отрисовка превью ячеек в UICollectionView. Вместо синхронного вызова requestImage для каждой ячейки используется кэширование и предзагрузка:
let imageManager = PHCachingImageManager()
imageManager.startCachingImages(
for: assets.objects(at: indexPaths.map { $0.item }),
targetSize: thumbnailSize,
contentMode: .aspectFill,
options: nil
)
При скролле UICollectionView запрашивает только те ячейки, которые попали в зону видимости. Превью запрашиваются асинхронно, а полученный UIImage передаётся в ячейку через замыкание. Если пользователь быстро прокручивает список, запросы, которые уже не нужны, отменяются через PHImageRequestOptions.isNetworkAccessAllowed = false.
3. Интеграция с Combine
Для того чтобы UI реагировал на изменения в медиатеке (добавление новых фото, удаление, изменение метаданных), Photos предлагает наблюдателя PHPhotoLibraryChangeObserver. Его можно обернуть в Publisher:
final class PhotoLibraryPublisher: NSObject, PHPhotoLibraryChangeObserver {
private let subject = PassthroughSubject<PHChange, Never>()
var publisher: AnyPublisher<PHChange, Never> { subject.eraseToAnyPublisher() }
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
subject.send(changeInstance)
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
}
Подписка на publisher в ViewModel позволяет автоматически обновлять UICollectionView через diffable data source. При получении изменений вычисляется новый набор IndexPath, который применяется к UI без полной перезагрузки.
4. Управление выбором и ограничениями
Для мультиселекта часто требуется ограничить количество выбранных элементов (например, не более 10 фото). Хранилище выбранных PHAsset реализуется в виде Set<PHAsset> внутри ViewModel. При каждом тапе на ячейку:
func toggleSelection(of asset: PHAsset) {
if selectedAssets.contains(asset) {
selectedAssets.remove(asset)
} else if selectedAssets.count < maxSelection {
selectedAssets.insert(asset)
}
selectionSubject.send(selectedAssets)
}
selectionSubject – CurrentValueSubject<Set<PHAsset>, Never>, который публикует изменения выбранных элементов. UI‑ячейка подписывается на этот поток и меняет своё состояние (отображает галочку, меняет opacity).
Оптимизация под 60 000+ фото
Профилирование памяти
При работе с огромными библиотеками важно следить за тем, сколько изображений одновременно находится в памяти. PHCachingImageManager поддерживает автоматическое управление кэшем, но можно задать размер кэша вручную:
imageManager.maximumCacheSize = 100 * 1024 * 1024 // 100 МБ
Предзагрузка соседних ячеек
Алгоритм предзагрузки учитывает текущий visibleCells и добавляет кэширующие запросы для диапазона ±2 экранов. Это уменьшает задержку появления превью при быстрой прокрутке.
Обработка iCloud‑медиа
Не все фото находятся локально. При запросе превью необходимо включить сетевой доступ:
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
options.deliveryMode = .highQualityFormat
Если изображение загружается из iCloud, в ячейке отображается индикатор прогресса, который обновляется по мере получения данных.
Тестирование и отладка
- Unit‑тесты покрывают логику выбора, ограничения и реакцию на изменения медиатеки. Для
PHAssetиспользуетсяPHAssetMock, позволяющий имитировать изменения без доступа к реальному хранилищу. - UI‑тесты (XCUITest) проверяют скроллинг, корректность отображения превью и стабильность при добавлении новых фото в фоновом режиме.
- Performance‑инструменты (Instruments → Time Profiler, Allocations) помогают выявить утечки кэша и избыточные запросы к
PHImageManager.
Итоги реализации
Создание кастомной галереи в iOS требует от разработчика глубокого понимания Photos‑фреймворка, асинхронных паттернов загрузки и реактивных подходов к управлению состоянием. Правильное использование PHCachingImageManager, ленивой загрузки превью и Combine позволяет построить UI, способный без задержек обслуживать библиотеки из десятков тысяч элементов, поддерживать динамические изменения и интегрироваться в современную архитектуру приложения. Такой подход не только решает проблему «лага на 60 000 фото», но и открывает гибкость для дальнейших улучшений: добавление фильтров, поддержки видео, кастомных анимаций и адаптации под новые версии iOS.