swiftuicollectionviewuikitpopoverreloaddata

Popover attached to cell in UICollectionView moving when is called the ReloadData of this collection view


I have a UICollectionView that the user can clicks and it shows a popover attached to the cell clicked. In iOS 12, the popover stayed in the same origin (x,y or cell) if the reload data was called or not, but in iOS 13, the popover moves between the cells when the reload data of the collection view is called.

Follow a video of the behavior in iOS 13: https://vimeo.com/385021234

The presentation is done using the following code:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard let cell = collectionView.cellForItem(at: indexPath) else {
        return
    }

    presentPopover(from: self, cell: cell)
}

func presentPopover(from view: UIViewController, cell: UIView) {
    let popoverView = PopoverViewController(nibName: "PopoverViewController", bundle: nil)
    let popover: UIPopoverPresentationController = popoverView.popoverPresentationController!

    popover.sourceRect = cell.bounds
    popover.sourceView = cell

    view.present(popoverView, animated: true, completion: nil)
}

And the PopoverViewController is using the modalPresentationStyle = .popover

Anyone had this issue before in iOS 13? Our application was working fine in iOS 11 and 12.

I have an example of this behavior in the following git repository: https://github.com/diegodossantos95/UICollectionViewPopoverBug

Steps to reproduce in the repository:

  1. Click on a collection cell

  2. Popover opens

  3. Wait for the reload data

Thanks,

Diego.


Solution

  • Instead of attaching the popover to a cell who you can guarantee won't be dequeued, you could use a temporary UIView created from the cell's frame and then attach the popover to that. Here's the relevant code to accomplish this.

    class ViewController: UIViewController {
        var timer = Timer()
    
        var popOverTempView = UIView()
    
        @IBOutlet weak var collectionView: UICollectionView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            timer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(reloadData), userInfo: nil, repeats: true)
            self.collectionView.addSubview(popOverTempView)
            popOverTempView.isHidden = true
            popOverTempView.backgroundColor = .clear
        }
    
    }
    
    extension ViewController: UICollectionViewDelegate {
    
        func presentPopover(from view: UIViewController, cell: UIView) {
            let popoverView = PopoverViewController(nibName: "PopoverViewController", bundle: nil)
            let popover: UIPopoverPresentationController = popoverView.popoverPresentationController!
    
            popoverView.delegate = self
    
            popOverTempView.frame = cell.frame
            popOverTempView.isHidden = false
            popover.sourceRect = popOverTempView.bounds
            popover.sourceView = popOverTempView
    
            view.present(popoverView, animated: true, completion: nil)
    
        }
    
    }
    
    extension ViewController: PopoverViewControllerDelegate {
        func willDismissPopover() {
            popOverTempView.isHidden = true
        }
    }
    
    protocol PopoverViewControllerDelegate {
        func willDismissPopover()
    }
    
    class PopoverViewController: UIViewController {
    
        var delegate: PopoverViewControllerDelegate?
    
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            delegate?.willDismissPopover()
        }
    }
    

    Update 5/20/20:

    I think I found a cleaner more direct fix. Instead of using a cell's bounds directly, convert it's contentView's frame to the collectionView's coordinate space, then use that as the sourceRect and present from the current view controller's view.

    func presentPopover(from view: UIViewController, cell: UICollectionViewCell, indexPath: IndexPath) {
        let popoverView = PopoverViewController(nibName: "PopoverViewController", bundle: nil)
        let popover: UIPopoverPresentationController = popoverView.popoverPresentationController!
    
        var rect = cell.convert(cell.contentView.frame, to: collectionView)
        rect = collectionView.convert(rect, to: self.view)
    
        popover.sourceRect = rect
        popover.sourceView = self.view
    
        view.present(popoverView, animated: true, completion: nil)
    }