3

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.

  • I found a workaround for now: update each visible cell manually when the popover is visible, so I don't need to call the reloadItems neither the reloadData. – Diego Dos Santos May 18 '20 at 21:52

2 Answers2

3

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)
}
clawesome
  • 1,223
  • 1
  • 5
  • 10
0

When you call reloadData this causes the collection view to discard any currently visible items (including placeholders) and recreate items based on the current state of the data source object.

Se the difference between the cells frame before and after the reload data:

  - some : <UICollectionViewCell: 0x7fd8abd02190; frame = (73.5 10; 50 50); clipsToBounds = YES; hidden = YES; opaque = NO; layer = <CALayer: 0x600001b50e20>>
  - some : <UICollectionViewCell: 0x7fd8abd02190; frame = (519.5 10; 50 50); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x600001b50e20>>

if you need to change only one or more itens item on the collection try to use:

func reloadItems(at: [IndexPath])
  • In my app I need to reload all visible cells in a collection view, so I had to get all indexes of the visible cells and call the reloadItems, but the result is the same, the popover moves to another cell. I want to understand why code works in iOS 12 and not in iOS 13. – Diego Dos Santos May 18 '20 at 21:45