Reputation: 2080
I've added a UISearchBar
to a UICollectionView
and in the delegate searchBar:textDidChange:
filter my model and call [collectionView reloadData]
. reloadData
(as well as reloadSection, etc) wants to take away firstResponder from the searchbar's textfield, thus dismissing the keyboard.
I am trying to build a "live updating" filter and so it's annoying to have the keyboard go away after each character typed.
Any ideas?
Upvotes: 25
Views: 10975
Reputation: 527
This is due to reloadData() being called inside of UISearchBarDelegate methods. A way around this is to have an extra empty section specifically for your header view that contains the search bar. Then instead of reloadData() call:
self.collectionView.reloadSections([1])
If you do not have multiple sections of data you can eliminate the second header by loading a faux header.
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if indexPath.section == 1 {
let fakeView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "fakeHeader", for: indexPath)
return fakeView
}
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: searchHeader, for: indexPath) as! SearchHeader
headerView.initializeHeader(with: self)
return headerView
}
Next you can set the height of the faux header to 0
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return section == 0 ? CGSize(width: collectionView.frame.width, height: 260) : CGSize(width: collectionView.frame.width, height: 0)
}
The next problem you will run into will be the gap between the cells and the first header which is caused by the edge insets. So eliminate the top and bottom insets for the first section.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if section == 1 {
return UIEdgeInsets(top: 20, left: 20, bottom: 30, right: 20)
}
return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
}
Hope this saves someone some time.
Upvotes: 0
Reputation: 500
I faced this issue when dealing with UITextFields inside an UICollectionViewCell. I think search bar may follow the same path.
I have a bunch of collection view cells which populate from;
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let question = questions[indexPath.row]
let cell: QuestionBaseCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellName, for: indexPath) as! QuestionBaseCollectionViewCell
cell.arrangeCell(question: question) // Here cell populates from server data
cell.delegate = self
return cell
}
This QuestionBaseCollectionViewCell has a delegate method, which updates the answer of the question to its delegate. I need to update the server side data.
In the delegate method; If I put reloadData method the textfield resigns and keyboard is disappearing.
Also I do not have direct access to the UITextField which yields to I cannot use cell.textField.becomeFirstResponder() etc.
So, I comment out the reloadData()
method.
func questionCollectionViewCell(_ cell: QuestionBaseCollectionViewCell, didChange choice: Choice) {
guard let indexPath = collectionView.indexPath(for: cell) else { return }
var question = questions[indexPath.row]
guard let choiceIndex = question.choices?.firstIndex(where: { $0.id == choice.id }) else { return }
question.choices?[choiceIndex] = choice
questions[indexPath.row] = question
// collectionView.reloadData()
}
Here is what I did;
I made call to reloadData
() after keyboard dismiss. See below.
Just add a keyboardDidHideNotification to the corresponding view controller in viewWillAppear method (note: you may put wherever you want depends on your needs) as follows:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(handleKeyboardDidHide(_ :)),
name: UIResponder.keyboardDidHideNotification,
object: nil)
}
and the observer method;
@objc func handleKeyboardDidHide(_ notification: Notification) {
collectionView.reloadData()
}
That's it. After the keyboard will hide, you may save your data.
Hope it helps!
Upvotes: 1
Reputation: 11686
The answer is to not reload the section which has the text field. I solved this problem by placing all items into a single search so it was possible to reload that single section.
The existing screen that I was modifying was showing the index on the right with the letters to navigate to each section which means there were many section which makes it harder to know how to reload each of those sections without messing up internal state. To make it work when the text field becomes the first responder the organization of the collection view changes to place all items into a single section which is reloaded when the search is run. Now the top section is not reloaded so it does not lose focus.
This way no undefined behavior is necessary like the other answers listed for this question.
https://github.com/brennanMKE/PullDownSearch
Upvotes: 2
Reputation: 191
In searchBar delegate function , I use performBatchUpdates, first,reload collectionView then call [self.searchBar becomeFirstResponder] to display keyboard
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar{
[self setEditing:NO animated:YES];
[searchBar setShowsCancelButton:YES animated:YES];
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadData];
} completion:^(BOOL finished) {
[self.searchBar becomeFirstResponder];
}];
}
Upvotes: 10
Reputation: 61
I run into the same problem recently and it took a while to get it fixed. The problem is that when text field is nested in ReusableCollectionView the following doesn't work.
[self.collectionView reloadData];
[self.textField becomeFirstResponder];
Furthermore, for me it did work fine on simulator but didn't work on the device.
Setting view controller as text field delegate and implementing
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
return NO;
}
didn't work because as the result collection view did not refresh. My guess is - before reloading its views collection tries to remove focus from all nested controls but it can't - we return NO from text field delegate method.
So the solution for me was to let the system remove focus from text field but then get it back after reload. The question was when actually to do that.
First, I've tried to do it in the
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
but when there was no items in collection (which is normal case during filtering) this method didn’t get called.
Eventually I solved this in the following way. I created UICollectionReusableView subclass implementing two methods: -prepareForReuse and -drawRect:. The first one contains -setNeedsDesplay call which schedules drawRect call on next drawing cycle. The second one restores focus by calling [self.textField becomeFirstResponder] if corresponding flag is set to YES. So the idea was to call becomeFirstResponder "later" but not too late, otherwise weird "jumping" of keyboard happens.
My custom reusable collection view looks like this:
@interface MyCollectionReusableView : UICollectionReusableView
@property (nonatomic, assign) BOOL restoreFocus;
@property (nonatomic, strong) UITextField *textField;
@end
@implementation MyCollectionReusableView
@synthesize restoreFocus;
@synthesize textField;
- (void)setTextField:(UITextField *)value {
textField = value;
[self addSubview:textField];
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
if (self.restoreFocus) {
[self.textField becomeFirstResponder];
}
}
- (void)prepareForReuse {
[self setNeedsDisplay];
}
Then in my View Controller:
- (void)viewDidLoad {
[super viewDidLoad];
[self.collectionView registerClass:[MyCollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:HeaderReuseIdentifier];
[self.textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
if (UICollectionElementKindSectionHeader == kind) {
MyCollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:HeaderReuseIdentifier forIndexPath:indexPath];
//add textField to the reusable view
if (nil == view.textField) {
view.textField = self.textField;
}
//set restore focus flag to the reusable view
view.restoreFocus = self.restoreFocus;
self.restoreFocus = NO;
return view;
}
return nil;
}
- (void)textFieldDidChange:(UITextField *)textField {
self.restoreFocus = YES;
[self.collectionView reloadData];
}
Probably not the most elegant solution, but it works. :)
Upvotes: 6
Reputation: 10045
Solved it:
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0.0, 0.0, 726.0f, 44.0f)];
[_collectionView addSubview:searchBar];
searchBar.delegate = self;
[(UICollectionViewFlowLayout *)_collectionView.collectionViewLayout setSectionInset:UIEdgeInsetsMake(64, 20, 20, 20)];
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
[_collectionView reloadData];
[searchBar becomeFirstResponder];
}
Upvotes: 2
Reputation: 693
I just added
[searchBar becomeFirstResponder];
after calling
[collectionView reloadData];
Upvotes: 0
Reputation: 423
Here's how I managed it. I had my UISearchBar as the header supplementary view in my collection view. On text didchange for the searchbar delegate I set a flag that the search is active (or not) and reloaded the collectionview
- (void)searchBar:(UISearchBar *)_searchBar textDidChange:(NSString *)searchText
{
if([searchText isEqualToString:@""])
{
activeSearch = FALSE;
}
else
{
activeSearch = TRUE;
}
[self filterContentForSearchText:searchText scope:nil];
[self.collectionView reloadData];
}
Then in the cellForItemAtIndexPath I check if the keyboard is first responder and search is active, if not, I make it first responder:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
...
if(activeSearch && ![searchBar isFirstResponder])
{
[searchBar becomeFirstResponder];
}
}
Upvotes: 0
Reputation: 28179
If you've added the UISearchBar
as a header to the UICollectionView
the reloadData
call will "refresh" the header by removing and re-adding the UISearchBar
causing it to lose firstResponder
.
You can either add your UISearchBar
in another way, not using header, or override reloadData
, check if the search bar is currently firstResponder, and if so after calling super
, do:
// If we've lost first-responder, restore it.
if ((wasFirstResponder) && (![self.searchBar isFirstResponder])) {
[self.searchBar becomeFirstResponder];
}
Upvotes: 3
Reputation: 20021
I think you are calling the -resignFirstResponder
method in the searchbar delegate which cause every time you enter the value it gets dismissed
You are in the right path about the datasource methods and reload data.Use an array to store all values another array for loading the collectionview and on every letter entered on the search bar filter and load the datasource array and then reload
Well i think it is better to keep the keyboard .That is the right UI style after you are done entering the text to search you can dismiss it manually.I think it is the better option for a live updating filter.
If you are using a button to search then you can include the keyboard hide option there.but the live update cannot be implemented when using such an option
Upvotes: 0
Reputation: 4899
I've just created a small test application. The following code does not hide the keyboard on entering characters in the searchbar. That said, [UITableView reloadData] does not resign the responder.
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return arc4random() % 10;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
return cell;
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
[self.collectionView reloadData];
}
Are you sure, you're not resigning it somewhere?
Upvotes: 0