Работа с большим количеством картинок


#1

Доброго времени суток! Господа, такой вопрос: работаю с большим количеством картинок, порядка 100-200, в перспективе ещё больше. И вот когда загружаю их все в CollectionView, происходит колоссальный жор оперативной памяти, доходит до 3 Гб. При закрытии окна все становится нормально. Загружаю из галереи, используя путь до картинки, который хранится в CoreData, т.к. я беру только то, что мне нужно. Есть ли какие-нибудь способы обработки такого количества картинок? Спасибо.


#2

Может быть по мере перезаписи ячеек догружать следующую партию? Ну и для этого нужно использовать очередь.


#3

Я делал подгрузку фоток с измененным качеством и кол-во ячеек в строке было 3.

class UIViewController {
    @IBOutlet weak var uiPhotoCollectionView: UICollectionView!
    var allPhotos: PHFetchResult<PHAsset>!

    override func viewDidLoad() {
            super.viewDidLoad()
            
            PHPhotoLibrary.shared().register(self)
            uiPhotoCollectionView.register(CustomViewCell.nib,
                                   forCellWithReuseIdentifier: CustomViewCell.cellIdentifier)
    }

    deinit {
            PHPhotoLibrary.shared().unregisterChangeObserver(self)
        }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        Permissions.authorisationStatus(attachmentTypeEnum: .photoLibrary, vc: self) { (status) in
            if status {
                if self.allPhotos == nil {
                    // Create a PHFetchResult object for each section in the table view.
                    DispatchQueue.global().async {
                        let allPhotosOptions = PHFetchOptions()
                        allPhotosOptions.predicate = NSPredicate(format: "mediaType = %d",
                                                                 PHAssetMediaType.image.rawValue)
                        allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
                        self.allPhotos = PHAsset.fetchAssets(with: .image, options: allPhotosOptions)
                        
                        DispatchQueue.main.async {
                            self.uiPhotoCollectionView.reloadData()
                        }
                    }
                }
            }
        }
    }
}

extension UIViewController: UICollectionViewDataSource {

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return allPhotos == nil ? 0 : allPhotos.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let asset = allPhotos.object(at: indexPath.item)
    
    guard let layoutAttributes = collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath),
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomViewCell.cellIdentifier, for: indexPath) as? CustomViewCellelse { return UICollectionViewCell() }
    
    cell.indexPath = indexPath
    cell.photoAsset = asset
    cell.size = layoutAttributes.frame.size
    
    return cell
}
}

extension UIViewController: UICollectionViewDelegateFlowLayout {

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 0.5
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 0.5
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let width = collectionView.frame.width
    let photosCountInRow = 3

    return CGSize(width: width/photosCountInRow - 0.5, height: width/photosCountInRow - 0.5)
}
}

extension UIViewController: PHPhotoLibraryChangeObserver {

func photoLibraryDidChange(_ changeInstance: PHChange) {
    guard let changes = changeInstance.changeDetails(for: allPhotos) else { return }
    
    // Change notifications may be made on a background queue. Re-dispatch to the
    // main queue before acting on the change as we'll be updating the UI.
    DispatchQueue.main.sync {
        // Hang on to the new fetch result.
        allPhotos = changes.fetchResultAfterChanges
        
        // to avoid such errors we should just call reloadData()
        // Fatal Exception: NSInternalInconsistencyException
        // attempt to delete and reload the same index path
        // (<NSIndexPath: 0x1d0431720> {length = 2, path = 0 - 1})
        self.uiPhotoCollectionView?.reloadData()
        
        /*
        if changes.hasIncrementalChanges {
            // If we have incremental diffs, animate them in the collection view.
            guard let collectionView = self.uiPhotoCollectionView else { fatalError() }
            
            collectionView.performBatchUpdates({
                // For indexes to make sense, updates must be in this order:
                // delete, insert, reload, move
                if let removed = changes.removedIndexes, removed.count > 0 {
                    collectionView.deleteItems(at: removed.map({ IndexPath(item: $0, section: 0) }))
                }
                if let inserted = changes.insertedIndexes, inserted.count > 0 {
                    collectionView.insertItems(at: inserted.map({ IndexPath(item: $0, section: 0) }))
                }
                if let changed = changes.changedIndexes, changed.count > 0 {
                    collectionView.reloadItems(at: changed.map({ IndexPath(item: $0, section: 0) }))
                }
                changes.enumerateMoves { fromIndex, toIndex in
                    collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
                                            to: IndexPath(item: toIndex, section: 0))
                }
            })
        } else {
            // Reload the collection view if incremental diffs are not available.
            uiPhotoCollectionView!.reloadData()
        }
        */
    }
}
}

class CustomViewCell: UICollectionViewCell {

static let nib = UINib(nibName: "CustomViewCell", bundle: Bundle.main)
static let cellIdentifier = "CustomViewCell"
private let scale = UIScreen.main.scale

var photoAsset: PHAsset? {
    didSet {
        loadPhotoAssetIfNeeded()
    }
}

var size: CGSize? {
    didSet {
        loadPhotoAssetIfNeeded()
    }
}

var indexPath: IndexPath?
private var imageRequestID: PHImageRequestID?

@IBOutlet weak var uiPhotoView: UIImageView!

override func prepareForReuse() {
    super.prepareForReuse()
    uiPhotoView.image = nil
    
    //Cancel requests if needed
    let manager = PHImageManager.default()
    guard let imageRequestID = self.imageRequestID else { return }
    manager.cancelImageRequest(imageRequestID)
    self.imageRequestID = nil
}

override func awakeFromNib() {
    uiPhotoLabel.layer.cornerRadius = uiPhotoLabel.frame.height / 2
    uiPhotoLabel.layer.borderWidth = 1
    uiPhotoLabel.layer.borderColor = UIColor.white.cgColor
    uiPhotoLabel.layer.masksToBounds = true
}

private func loadPhotoAssetIfNeeded() {
    guard let indexPath = self.indexPath,
        let asset = photoAsset, let size = self.size else { return }
    
    let options = PHImageRequestOptions()
    options.deliveryMode = .highQualityFormat
    options.resizeMode = .fast
    options.isSynchronous = false
    options.isNetworkAccessAllowed = true
    
    let manager = PHImageManager.default()
    let newSize = CGSize(width: size.width * scale,
                         height: size.height * scale)
    
    imageRequestID = manager.requestImage(for: asset, targetSize: newSize, contentMode: .aspectFill, options: options, resultHandler: { [unowned self] (result, _) in
        guard self.indexPath?.item == indexPath.item else { return }
        self.imageRequestID = nil
        UIView.transition(with: self.uiPhotoView,
                          duration: 0.3,
                          options: .transitionCrossDissolve,
                          animations: { self.uiPhotoView.set(image: result, focusOnFaces: false) },
                          completion: nil)
    })
}
}

#4

Спасибо большое за ответы, возникла небольшая проблема с идентификацией в двух местах, а именно в месте Permissions.authorisationStatus… и с переменной allPhotos, пишет что используются неизвестные идентификаторы. Буду благодарен за помощь в устранении проблем.


#5

Пардон, класс Permissions был кастомный, забыл про него.

enum AttachmentType: String {
    case camera, photoLibrary
}

public class Permissions {
    
    static let alertForPhotoLibraryMessage = LocalizedString("msg_alert_for_photo_library")
    static let alertForCameraAccessMessage = LocalizedString("msg_alert_for_camera_access")
    
    static func authorisationStatus(attachmentTypeEnum: AttachmentType, vc: UIViewController, completion: @escaping (_ status: Bool) -> ()) {
        switch attachmentTypeEnum {
        case .camera:
            cameraPermissionCheck(vc) { status in
                DispatchQueue.main.async {
                    completion(status)
                }
            }
        case .photoLibrary:
            photoLibraryPermissionCheck(vc) { status in
                DispatchQueue.main.async {
                    completion(status)
                }
            }
        }
    }
    
    private static func photoLibraryPermissionCheck(_ vc: UIViewController, completion: @escaping (_ status: Bool) -> ()) {
        let status = PHPhotoLibrary.authorizationStatus()
        
        switch status {
        case .authorized:
            completion(true)
            
        case .denied, .notDetermined, .restricted:
            PHPhotoLibrary.requestAuthorization { status in
                if status == PHAuthorizationStatus.authorized {
                    completion(true)
                }
                else {
                    completion(false)
                    addAlertForSettings(.photoLibrary, vc: vc)
                }
            }
        }
    }
    
    private static func cameraPermissionCheck(_ vc: UIViewController, completion: @escaping (_ status: Bool) -> ()) {
        let cameraMediaType = AVMediaTypeVideo
        let status = AVCaptureDevice.authorizationStatus(forMediaType: cameraMediaType)
        
        switch status {
        case .authorized:
            completion(true)
            
        case .denied, .notDetermined, .restricted:
            AVCaptureDevice.requestAccess(forMediaType: cameraMediaType) { granted in
                if granted {
                    completion(true)
                }
                else {
                    completion(false)
                    addAlertForSettings(.camera, vc: vc)
                }
            }
        }
    }
    
    private static func addAlertForSettings(_ attachmentTypeEnum: AttachmentType, vc: UIViewController) {
        var alertTitle: String = ""
        if attachmentTypeEnum == AttachmentType.camera{
            alertTitle = alertForCameraAccessMessage
        }
        if attachmentTypeEnum == AttachmentType.photoLibrary{
            alertTitle = alertForPhotoLibraryMessage
        }
        
        let cameraUnavailableAlertController = UIAlertController (title: nil , message: alertTitle, preferredStyle: .alert)
        
        let settingsAction = UIAlertAction(title: LocalizedString("action_settings"), style: .destructive) { (_) -> Void in
            let settingsUrl = NSURL(string: UIApplicationOpenSettingsURLString)
            if let url = settingsUrl {
                UIApplication.shared.open(url as URL)
            }
        }
        
        let cancelAction = UIAlertAction(title: LocalizedString("action_cancel"), style: .default, handler: nil)
        cameraUnavailableAlertController.addAction(cancelAction)
        cameraUnavailableAlertController.addAction(settingsAction)
        
        DispatchQueue.main.async {
            vc.present(cameraUnavailableAlertController , animated: true, completion: nil)
        }
    }
}

LocalizedString замените на обычные сообщения.

А по поводу переменной allPhotos смотрите что у вас не так. Она определена в ViewController’e. Может вы не там поместили ее.


#6

Спасибо. Я так понимаю, проблема с allPhotos в том, что она определена во VC, но мы же дописывали extension. AllPhoto будет видна в нем?


#7

Extension ведь для того же VC. Я надеюсь вы его в одном файлу с VC прописали.


#8

Я понимаю, конечно, все как у вас.


#9

Ладно, спасибо за помощь, буду искать проблему)


#10

В итоге разобрался, но делал немножко другим способом. Теперь можно доставать картинки по их номеру в галерее (indexpath.row).
Такой вопрос: можно ли доставать из галереи картинки не по номеру, а по названию или их пути, и если да то как? Спасибо.