1

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.

ferryawijayanto
  • 569
  • 4
  • 18
  • You probably forgot `self.collectionView.reloadData()` after loading the images? – burnsi Jul 12 '22 at 13:37
  • Are you trying to show a standard Gallery so user can select photos, which you then want to show in your table view? if so, where is `PHPickerViewController` - I don't see you loading it anywhere – timbre timbre Jul 12 '22 at 13:58
  • @burnsi i already di use `self.collectionView.reloadData()` in authorize status, but it still not showing – ferryawijayanto Jul 12 '22 at 20:51
  • @jjquirckart i'm not using PHPickerViewController i fetch manually from my function `fetchAllPhotos()` using `PHAsset.fetchAssets` – ferryawijayanto Jul 12 '22 at 20:53
  • 1
    After `PHPhotoLibrary.requestAuthorization { status in` in the `case .authorized:` you are clearly missing `self.collectionView.reloadData()`, at least in the code you posted. The first `self.collectionView.reloadData()` will not be called if you ask the user for persmissions only if permissions have allready been granted. This would exactly describe the behaviour you see. – burnsi Jul 12 '22 at 20:56
  • 1
    ok, because you said "show image gallery", where in fact you want to show images *from* the gallery". In that case the advice to call `self.collectionView.reloadData()` is seems right. At least from code you posted, you do not refresh collection view after loading images in `fetchAllPhotos()` – timbre timbre Jul 12 '22 at 20:59
  • @burnsi and @jjquirckart i see i should have my `fetchAllPhotos()` function inside `DispatchQueue` than `self.collectionView.reloadData()` thank you – ferryawijayanto Jul 12 '22 at 21:31
  • First, you don't need to call `reloadData()` when using RxCocoa. It will take care of the collection view for you. All you need to do is emit a new array from the source observable that is feeding the collection view's rx.items. At no point are you emitting a new value from `dataSource`. – Daniel T. Jul 13 '22 at 01:54
  • i think the problem is how i bind the value, when i try to use regular collectionView it works. but if use rxSwift it not showing the gallery after user permission. @DanielT. – ferryawijayanto Jul 13 '22 at 03:12

1 Answers1

1

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)
    }
}
Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Can you please guide me for good resources to learn RxSwift? – nirav Jul 13 '22 at 13:32
  • 1
    There's the [Ray Wenderlich book](https://www.raywenderlich.com/books/rxswift-reactive-programming-with-swift) but I learned before it came out. The best free resources are (1) the documentation in the repo, (2) [Introduction to Rx](http://introtorx.com) and (3) [ReactiveX](https://reactivex.io). Also [join the slack](http://slack.rxswift.org). – Daniel T. Jul 13 '22 at 13:38