0

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)
}
  • You can't use a segue from something that is not in your storyboard. You can initialise and present your own view controller via code. Also, PHPickerViewController has to be dismissed manually. – Oscar Apeland Jan 14 '21 at 16:46
  • @OscarApeland can I push new vc on PHPickerViewController from code? – Діма Комар Jan 14 '21 at 19:14
  • I think you _can_, it is contained in a navigation controller. I am pretty sure Apple would prefer if you didn't. I suggest you show your "Add Post" interface first, then let that view controller present the PHPicker. When the user presses Done in the picker, populate your AddPost view controller with the selected images. my rule of thumb is that if something is hard to make, make something else. hard code is usually bad design. let good code guide your user interface. – Oscar Apeland Jan 14 '21 at 21:03

1 Answers1

1

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.

Azure
  • 229
  • 2
  • 5
  • The question was about pushing new view controller into PHPickerViewController navigation stack – Діма Комар Jun 06 '21 at 17:26
  • That was my understanding. According to apple docs you cannot access or modify the view hierarchy of the [PHPickerViewController](https://developer.apple.com/documentation/photokit/phpickerviewcontroller) itself, however when you present it, it is added to your view hierarchy as the presentedViewController property of your view controller. You can present another view controller from the presentedViewController even if you can't access the hierarchy of that controller. Hope that clarifies. – Azure Jun 06 '21 at 17:40
  • Sorry, my mistake. I don't believe that you can push to the PHPickerViewController as the view hierarchy is owned by the system rather than the app. The solution here is to present your controller in front of the PHPickerViewController. You could create a custom transition controller that would mimic the appearance of pushing to the PHPickerViewController stack if the presentation format is key, but it would only appear to do so. – Azure Jun 06 '21 at 18:02
  • I've seen some apps do that kind of push over PHPickerViewController so I was wondering how they do that, maybe thats some kind of private API... – Діма Комар Jun 06 '21 at 18:49
  • I see. It is possible they used the UIImagePickerController which is now deprecated but so far still works. It allowed you to push but even that was a little funky as it would push full screed even if it was presented modally. – Azure Jun 06 '21 at 19:47
  • The thing is that's definitely PHPickerViewController (they are visually different with UIImagePickerController). And they are pushing controller that looks a lot like the one that UIImagePickerController shows when allowsEditing is set to true. I know that PHPickerViewController doesn't have allowsEditing property so thats super odd for me. – Діма Комар Jun 08 '21 at 17:10
  • I have edited my response with how to use a custom presentation controller and transition delegate to mimic pushing your custom controller to the stack. If you needed the editing/crop controller like the one presented by the UIImagePickerController when allowsEditing is set to true, let me know. – Azure Jun 13 '21 at 21:38
  • yes, I need editing/crop controller like the one presented by the UIImagePickerController when allowsEditing is set to true – Діма Комар Jun 17 '21 at 12:35
  • I created a CocoaPods pod and update my response. Pod can be found [here](https://cocoapods.org/pods/PhotoCropController). – Azure Jun 22 '21 at 21:06
  • good job, looks really cool still the editing view controller isn't a part of PHPickerViewController navigation stack. Do you thing there is a way to simulate UINavigationViewController animation completely ? – Діма Комар Jun 27 '21 at 13:34
  • thank you : ). I am unaware of any way to access the navigation stack of the PHPickerViewController itself. Logging the UIApplication.shared.windows.last?.rootViewController from the delegate method shows a UIInputWindowController which appears to have no navigation controller and is not a navigation controller also couldn't find public documentation. I did update my response above and the pod to use your own navigation controller with bars hidden to present the picker which can then be pushed to. – Azure Jun 28 '21 at 18:00