swiftrx-swiftphphotolibrary

Image gallery not show after request access permission


i try to show image gallery after user permission to allow all photos, but the gallery is not showing. but when i back to previous controller and navigate back, the gallery show up. but that's not what i want, i want after user allow the image show up.

this my setup

    private var allPhotos: [PHAsset] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        PHPhotoLibrary.shared().register(self)
        setupCollectionView()
        checkPhotoLibraryPermission()
        bindViewModel()
    }
    
    deinit {
        PHPhotoLibrary.shared().unregisterChangeObserver(self)
    }

    private func bindViewModel() {
        let dataSource = Observable.just(allPhotos)
        
        dataSource.asObservable()
            .bind(to: collectionView.rx.items(cellIdentifier: GalleryCollectionViewCell.cellId, cellType: GalleryCollectionViewCell.self)) { row, asset, cell in
                let imageRequestOptions = PHImageRequestOptions()
                imageRequestOptions.resizeMode = .exact
                
                self.imageManager.requestImageDataAndOrientation(for: asset, options: imageRequestOptions) { imageData, _, orientation, info in
                    guard let imageData = imageData else { return }
                    cell.setup(imageData: imageData)
                }
            }.disposed(by: disposedBag)
        
        collectionView.rx.itemSelected
            .subscribe(onNext: { [weak self] indexPath in
                guard let strongSelf = self else { return }
                let asset = strongSelf.allPhotos[indexPath.row]
                asset.requestContentEditingInput(with: PHContentEditingInputRequestOptions()) { editingInput, info in
                    guard let path = editingInput?.fullSizeImageURL?.path.replacingOccurrences(of: "HEIC", with: "PNG") else { return }
                    self?.imageManager.requestImageDataAndOrientation(for: asset, options: self?.imageRequestOptions) { imageData, _, orientation, info in
                        guard let imageData = imageData else { return }
                        self?.goToCropImage(from: imageData, and: path.lastPathComponent)
                    }
                }
            }).disposed(by: disposedBag)
    }

    private func fetchAllPhotos() {
        let allPhotosOptions = PHFetchOptions()
        allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        
        let fetchResult = PHAsset.fetchAssets(with: .image, options: allPhotosOptions)
        allPhotos = fetchResult.objects(at: IndexSet(0..<fetchResult.count))
    }
    
    private func checkPhotoLibraryPermission() {
        let status = PHPhotoLibrary.authorizationStatus()
        switch status {
        case .authorized:
            fetchAllPhotos()
            DispatchQueue.main.async {
            self.collectionView.reloadData()
          }
        }
        case .denied, .restricted :
            //handle denied status
            gotoAppSettings()
        case .notDetermined:
            // ask for permissions
            PHPhotoLibrary.requestAuthorization { status in
                switch status {
                case .authorized:
                    self.fetchAllPhotos()
                case .denied, .restricted:
                    // as above
                    self.gotoAppSettings()
                case .notDetermined:
                    // won't happen but still
                    break
                case .limited:
                    break
                @unknown default:
                    fatalError("Failed to get user permission to access photo")
                }
            }
        case .limited:
            fetchAllPhotos()
        @unknown default:
            fatalError("Failed to get user permission to access photo")
        }
    }

    func photoLibraryDidChange(_ changeInstance: PHChange) {
        let allPhotosOptions = PHFetchOptions()
        
        let fetchResult = PHAsset.fetchAssets(with: .image, options: allPhotosOptions)
        DispatchQueue.main.async {
            self.allPhotos = fetchResult.objects(at: IndexSet(0..<fetchResult.count))
            self.collectionView.reloadData()
        }
    }

I already try to to reload collectionView but it still not show up.


Solution

  • The way that UICollectionView.rx.items works is that it observes its dataSource. When the dataSource emits a new array, the items operator will reload the collection view and call its closure for each item.

    Since you are using just as your data source, only one array is emitted and the collection view never changes. You have to tie the source to the change observer to get it to work. Here is a working example:

    extension PhotosViewController { // a UICollectionViewController
        func connect(disposeBag: DisposeBag) {
            // initial fetch result
            let allPhotosOptions = PHFetchOptions()
            allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
            let initialFetchResult = PHAsset.fetchAssets(with: allPhotosOptions)
    
            let assets = PHPhotoLibrary.shared().rx.registerChangeObserver()
                // when a change is observed, we need to update the fetchResult
                .scan(initialFetchResult) { oldResult, change in
                    guard let changes = change.changeDetails(for: oldResult) else { return oldResult }
                    return changes.fetchResultAfterChanges
                }
                // but first send the initial asset fetch to the collection view
                .startWith(initialFetchResult)
                // and get the assets out of the fetch result.
                .map { $0.objects(at: IndexSet(0 ..< $0.count)) }
    
            collectionView.dataSource = nil
            assets
                .observe(on: MainScheduler.instance)
                .bind(to: collectionView.rx.items(cellIdentifier: "GridViewCell", cellType: GridViewCell.self)) { _, asset, cell in
                    cell.configure(asset: asset)
                }
                .disposed(by: disposeBag)
        }
    }
    
    extension Reactive where Base: PHPhotoLibrary {
        // not actually needed, but I provided it as an example.
        static func requestAuthorization() -> Observable<PHAuthorizationStatus> {
            Observable.create { observer in
                Base.requestAuthorization { status in
                    observer.onNext(status)
                    observer.onCompleted()
                }
                return Disposables.create()
            }
        }
    
        // this sets up the change observer. Note, your VC isn't the observer.
        func registerChangeObserver() -> Observable<PHChange> {
            Observable.create { [base] observer in
                let changeObserver: RxPhotoLibraryChangeObserver = .init(observer: observer)
                base.register(changeObserver)
                return Disposables.create { base.unregisterChangeObserver(changeObserver) }
            }
        }
    }
    
    // this is the change observer used in the above.
    final class RxPhotoLibraryChangeObserver: NSObject, PHPhotoLibraryChangeObserver {
        let observer: AnyObserver<PHChange>
        init(observer: AnyObserver<PHChange>) {
            self.observer = observer
        }
    
        func photoLibraryDidChange(_ changeInstance: PHChange) {
            observer.onNext(changeInstance)
        }
    }