Search code examples
iosphpickerviewcontroller

Push view controller over PHPickerViewController


Is that possible to push vc over the PHPickerViewController?

I'm trying to do that like this with no luck:

var configuration = PHPickerConfiguration()
configuration.filter = .any(of: [.images, .livePhotos])
photoPickerController = PHPickerViewController(configuration: configuration)
photoPickerController.delegate = self
present(self.photoPickerController, animated: true)

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
  //Push segue
  performSegue(withIdentifier: "showAddPost", sender: self)
}

Solution

  • Update - Using CocoaPods

    I created a simple pod PhotoCropController that includes a basic photo crop controller to be presented from the PHPickerViewControllerDelegate. It provides transitions to push to a modally presented controller and to pop or dismiss. The aspect ratio of the crop view can be edited as well. To use conform your view controller to the PhotoCropDelegate protocol and present the PhotoCropController from your PHPickerViewControllerDelegate. Implementation would look something like the following:

    extension ViewController: PHPickerViewControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, PhotoCropDelegate {
        
        func browsePhotoLibrary() {
            if #available(iOS 14, *) {
                var config = PHPickerConfiguration()
                config.filter = PHPickerFilter.images
                config.selectionLimit = 1
                config.preferredAssetRepresentationMode = .compatible
                let picker = PHPickerViewController(configuration: config)
                picker.delegate = self
                let nav = UINavigationController(rootViewController: picker)
                nav.setNavigationBarHidden(true, animated: false)
                nav.setToolbarHidden(true, animated: true)
                present(nav, animated: true)            } else {
                let picker = UIImagePickerController()
                picker.delegate = self
                picker.allowsEditing = true
                present(picker, animated: true)
            }
        }
        
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let edited = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
                image = edited
            } else if let selected = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
                image = selected
            }
            presentedViewController?.dismiss(animated: true)
        }
        
        @available(iOS 14, *)
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            if let provider = results.last?.itemProvider,
               provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { [weak self] result, error in
                    if let image = result as? UIImage {
                        DispatchQueue.main.async { self?.select(image: image) }
                    } else if let error = error {
                        NSLog("Error picking image: %@", error.localizedDescription)
                        DispatchQueue.main.async { picker.dismiss(animated: true) }
                    }
                }
            } else { DispatchQueue.main.async { picker.dismiss(animated: true) } }
        }
        
        func select(image: UIImage) {
            let destinationSize = AVMakeRect(aspectRatio: image.size, insideRect: view.frame).integral.size
            //Best Performance
            let resizedImage = UIGraphicsImageRenderer(size: destinationSize).image { (context) in
                image.draw(in: CGRect(origin: .zero, size: destinationSize))
            }
            let cropController = PhotoCropController()
            cropController.delegate = self
            cropController.image = resizedImage
            presentedViewController?.present(cropController, animated: true)
        }
        
        @objc func cropViewDidCrop(image: UIImage?) {
            self.image = image
            presentedViewController?.dismiss(animated: true) { [weak self] in
                self?.presentedViewController?.dismiss(animated: true)
            }
        }
    }
    

    To present another controller modally in front of the PHPickerViewController:

    The presentedViewController property of your view controller will be your photoPickerController so you can present another controller in front of it as follows:

    present(photoPickerController, animated: true)
    presentedViewController?.present(yourViewController, animated: true)
    

    If dismissing from the presentedViewController, you will need to call twice, once to dismiss yourViewController and then again to dismiss the photoPickerController, if they are both presented:

    presentedViewController?.dismiss(animated: true)
    presentedViewController?.dismiss(animated: true)
    

    Pushing a custom controller on top of the navigation stack

    To create the appearance of pushing to the PHPickerViewController stack you can use custom UIPresentationController and UIViewControllerAnimatedTransitioning classes to present your view. The following classes will mimic a push to a modally presented navigation controller:

    import UIKit
    import PhotosUI
    
    class SlideInModalPresentationController: UIPresentationController {
                
        var offset: CGFloat = 0.0
        
        override func containerViewWillLayoutSubviews() {
            super.containerViewWillLayoutSubviews()
            presentedView?.frame.origin.y = offset
            presentedView?.frame.size.height -= offset
            presentedView?.layer.cornerRadius = 10
            presentedView?.clipsToBounds = true
        }
    }
    
    class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
        
        private let duration = 0.3
        var isPresenting: Bool = true
        var dismissModally: Bool = false
        
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return duration
        }
        
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            guard let toController = transitionContext.viewController(forKey: .to),
                let fromController = transitionContext.viewController(forKey: .from)
            else { return}
            if isPresenting {
                toController.view.frame.origin.x = fromController.view.frame.width
                transitionContext.containerView.addSubview(toController.view)
                UIView.animate(withDuration: duration, animations: {
                    toController.view.frame.origin.x = 0
                }, completion: { _ in
                    transitionContext.completeTransition(true)
                })
            } else if dismissModally {
                var stack: UIView? = nil
                if #available(iOS 14, *), toController is PHPickerViewController {
                    stack = toController.view.superview
                    toController.dismiss(animated: false)
                } else if toController is UIImagePickerController {
                    stack = toController.view.superview
                    toController.dismiss(animated: false)
                }
                UIView.animate(withDuration: duration, animations: {
                    stack?.frame.origin.y = fromController.view.frame.height
                    fromController.view.frame.origin.y = fromController.view.frame.height
                }, completion: { _ in
                    transitionContext.completeTransition(true)
                    fromController.view.removeFromSuperview()
                })
            } else {
                UIView.animate(withDuration: duration, animations: {
                    fromController.view.frame.origin.x = fromController.view.frame.width
                }, completion: { _ in
                    transitionContext.completeTransition(true)
                    fromController.view.removeFromSuperview()
                })
            }
        }
    }
    

    To implement in your view controller:

    class ViewController: UIViewController {
    
        let slidInTransition = SlideInTransition()
    }
    
    extension ViewController: UIViewControllerTransitioningDelegate {
        
        private func presentYourController(_ image: UIImage) {
            let yourController = YourController()
            yourController.image = image
            yourController.modalPresentationStyle = .custom
            yourController.transitioningDelegate = self
            slidInTransition.dismissModally = false
            presentedViewController?.present(yourController, animated: true)
        }
        
        func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
            let presentationController = SlideInModalPresentationController(presentedViewController: presented, presenting: presenting)
            presentationController.offset = view.convert(source.view.frame, to: nil).origin.y + 10
            return presentationController
        }
        
        func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            slidInTransition.isPresenting = true
            return slidInTransition
        }
        
        func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            slidInTransition.isPresenting = false
            return slidInTransition
        }
    
        private func dismissPhotoStack() {
            slidInTransition.dismissModally = true
            presentedViewController?.dismiss(animated: true)
        }
    }
    

    When you are ready to dismiss the whole stack you can call dismissPhotoStack.