On iOS 17, UICollectionView updates trigger a resignFirstResponder call even when the cell with the text input is unaffected

I have a view with search functionality implemented with a UICollectionView. In a cell in section 0 there's a UITextField used for input. Changes in it trigger an update, during which collectionView.performBatchUpdates is called, with the updates block containing insertSections, deleteSections and reloadSections calls that only affect higher section indices.

On previous iOS versions there are no issues here, but starting on iOS 17, keyboard drops after every character entered. Adding a symbolic breakpoint on -[UIResponder resignFirstResponder] results in a break on the collectionView.performBatchUpdates call with the following UIKit methods in the stack trace above it:

#0	0x00000001852e5b78 in -[UIResponder resignFirstResponder] ()
#1	0x00000001855d6bcc in -[UITextField resignFirstResponder] ()
#2	0x0000000184a38f34 in -[UICollectionView _resignOrRebaseFirstResponderViewWithIndexPathMapping:] ()
#3	0x0000000184a37394 in -[UICollectionView _updateWithItems:tentativelyForReordering:propertyAnimator:collectionViewAnimator:] ()
#4	0x0000000184a309f0 in -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:collectionViewAnimator:] ()
#5	0x0000000184a3a808 in -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:animationHandler:] ()
Post not yet marked as solved Up vote post of kosti_ Down vote post of kosti_
678 views

Replies

UICollectionView will only call resignFirstResponder like this when the first responder view (in this case, the UITextField) is contained within a section or item that has been deleted or reloaded. If the UITextField is contained within a cell in section 0 as you claim, then most likely section 0 is being reloaded as part of the batch updates.

If you don't believe this is what is happening, please distill this into a sample project and submit that as an attachment to a feedback report so that we can take a look and understand the behavior you're seeing.

Alright. On closer look I got one detail wrong and the issue was with a header supplementary view rather than a cell, though still in a section unaffected by the updates and which there were no issues with in iOS versions prior to iOS 17. I reproduced this in a sample project which I sent attached to report FB13403563.

Any ideas how to work around this?

In the original situation we have a search bar as part of the scrollable area, but remaining at the top of the screen as the user scrolls down. This sticky header is implemented through a simple UICollectionViewFlowLayout subclass. I thought I would patch the issue by replacing the header with a placeholder view with the actual header placed separately on top of it tracking the position, but I'm stuck with two different issues depending on how exactly I do this.

If I add the real header as a subview to the collection view, it again gets wiped by the content update. It seems like this iOS 17 bug is affecting any non-cell subviews with an active first responder?

If I add the real header separate from the collection view, the layout works, but the user cannot scroll touching the header view, and I can't figure out a way to pass the touches to the collection view while still allowing interaction with the view itself. The most promising avenue seemed to be overriding touchesBegan and the four other related methods, but capturing the events there and passing them to other views appears to do nothing.

The best workaround I have found is to disable UIView animations before performing the update and opening the keyboard again, but even with this, the keyboard blinks and typing isn't very smooth.

UIView.setAnimationsEnabled(false)
self.collectionView.reloadSections(myIndexSet)
self.header?.searchBar.becomeFirstResponder()

//we need to re-enable animations at some point. For a naive solute, just re-enable it after a short delay
delay(0.2) { UIView.setAnimationsEnabled(true) }