Ios10 UICollectionView prefetching enabled causes selectItemAtIndexPath behave incorrectly - ios

i was trying to create a collection view with a list of cells. And when user hits a button it will random select one of the cell.
Now suppose there are only 10 cells. i.e numberOfItemsInSection delegate return 10.
The view controller is the data source for the collectionView. The collection view is called myCollectionView. And it has a property called selectedIndexPath
So ViewController:
#interface ViewController () < UICollectionViewDataSource>
#property (nonatomic, strong) NSIndexPath * selectedIndexPath;
#property (strong, nonatomic) IBOutlet UICollectionView *myCollectionView;
#end
Here's my random select code in the view controller:
-(void)chooseRandom{
NSInteger randomShadeIndex = arc4random_uniform((uint32_t)10);
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:randomShadeIndex inSection:0];
self.selectedIndexPath = indexPath;
[self.myCollectionView selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
}
And here's my cellForItemAtIndexPath in my view controller
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
MyCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
cell.selected = self.selectedIndexPath == indexPath;
return cell;
}
And here's setSelected: method in MyCell:
- (void)setSelected:(BOOL)selected{
[super setSelected:selected];
if (selected){
self.backgroundColor = [UIColor redColor];
} else {
self.backgroundColor = [UIColor greenColor];
}
}
So now when i call chooseRandom from the button press. If the random cell to be selected is not visible(not in current screen), then there's a high chance that it doesn't end up having setSelected:YES called during the selectItemAtIndexPath:.(Or it gets called but sets the cell.selected to NO instead of YES). Meaning the resulting screen has none of the cell selected.
And the interesting thing is, when i tried touching the screen (without selecting any cell). It will called the setSelected: on the cell to be selected. So i think selectItemAtIndexPath: is bugged.
And this only happens, when prefetching enabled in interface builder is set to be enabled.(which is the default for ios 10).
And i've tried following ways to solve this, but none of them works:
Add [self.myCollectionView cellForItemAtIndexPath:indexPath].selected = YES; at the end of chooseRandom.
Use scrollToItemAtIndexPath along with method 1 instead of selectItemAtIndexPath:
I think either this is a bug or I ignore something completely. I've been stuck on this for hours and couldn't figure out why. Now i think it is most likely a bug for selectItemAtIndexPath with prefetching enabled is set.
Please help me and tell me if you encounter the same issue. Thanks!
EDIT:
not sure if the same question. this link has similar issue but with deselect

A UICollectionView, just like a UITableView, caches its cells. Meaning, that if you have 100 cells, and only 10 of them are visible, it will only create 10 cells and reuse them as you scroll, setting the model/data of the visible cells as you are scrolling, reusing the ones that are not visible anymore (to save memory). So the reason you are not able to select a cell that is not visible, might be because it hasn't been created.

Please check this if it helps you
Can download sample project
https://www.dropbox.com/s/oeo9p1h3e8xfpfj/CollectionView.zip?dl=0?
-(void)chooseRandom{
NSInteger randomShadeIndex = arc4random_uniform((uint32_t)50);
NSLog(#"%ld",(long)randomShadeIndex);
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:randomShadeIndex inSection:0];
self.selectedIndexPath = indexPath;
[self.myCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:true];
[_myCollectionView reloadData];
}

Related

Get UICollectionViewCell from indexPath when UIBarButtonItem is tapped

I have created a View Controller with a Navigation bar and UiCollectionView. UI Collection View contains custom UICollectionViewCell. Navigation bar contains two UIBarButton items, one is on the left corner - prepared segue to previous page and other item is on the right corner - arranged to delete cell(s) in the UI CollectionView as show in the picture below:
Main Screen
Now I want to remove the selected UICollectionViewCell when UIBarButtonItem in the right corner, is tapped.
This how my cellForItemAtIndexPath method look like:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath{
self.GlobalIndexPath = indexPath;
MessagesCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"messagesCell" forIndexPath:indexPath];
cell.MessageHeading.text = [self.Message_Heading objectAtIndex:indexPath.row];
cell.MessageSubject.text = [self.Message_Subject objectAtIndex:indexPath.row];
cell.MessageContent.text = [self.Message_Details objectAtIndex:indexPath.row];
[cell.Checkbox setHidden:YES];
[cell.Checkbox setChecked:NO];
}
I have tried a solution like Declaring Indexpath as global variable and use it in the button event as below:
#property (strong,nonatomic) NSIndexPath *GlobalIndexPath;
some other code .......
//When Bin Icon(UIBarButtonItem) Clicked
- (IBAction)DeleteMessages:(id)sender {
[self.view makeToast:#"You clicked delete button !"];
NSIndexPath *indexPath = [self.MessageCollectionView.indexPathsForVisibleItems objectAtIndex:0] ;
BOOL created = YES;
// how to get desired selected cell here to delete
MessagesCollectionViewCell *cell = [self.MessageCollectionView cellForItemAtIndexPath:self.GlobalIndexPath];
if([cell.Checkbox isHidden])
{
[cell setHidden:YES];
}
else{
[cell.Checkbox setChecked:NO];
[cell.Checkbox setHidden:YES];
}
}
It's not worked.
For showing the UICollectionViewCell selected as checked, i'm using #Chris Chris Vasselli's solution
Please help me with this. Thanks in Advance.
There are a few steps. First, determine the selected indexPath, but don't assume there is a selection when the method is run....
// in your button method
NSArray *selection = [self.MessageCollectionView indexPathsForSelectedItems];
if (selection.count) {
NSIndexPath *indexPath = selection[0];
[self removeItemAtIndexPath:indexPath];
}
There are two more steps to remove items from a collection view: remove them from your datasource, and tell the view it has changed.
- (void)removeItemAtIndexPath:(NSIndexPath *)indexPath {
// if your arrays are mutable...
[self.Message_Heading removeObjectAtIndex:indexPath.row];
// OR, if the arrays are immutable
NSMutableArray *tempMsgHeading = [self.Message_Heading mutableCopy];
[tempMsgHeading removeObjectAtIndex:indexPath.row];
self.Message_Heading = tempMsgHeading;
// ...
Do one or the other above for each datasource array. The last step is to inform the collection view that the datasource has changed, and it must update itself. There are a few ways to do this. The simplest is:
// ...
[self.MessageCollectionView reloadData];
OR, a little more elegantly:
[self.MessageCollectionView deleteItemsAtIndexPaths:#[indexPath]];
} // end of removeItemAtIndexPath

Which method should I use to do something between UICollectionViewCells?

I have implemented an UICollectionView image gallery. Each cell has some views and I would like to hide or show that views when I change the current cell, at least when the event starts. is there any method? or should I do something by delegate? I have paging enabled and custom cell and FlowLayout.
I have done everything almost like this tutorial
One way would be to keep the current selected cell index in a local variable in your viewController, and use that index to perform any actions when selecting another cell:
#property (nonatomic) NSIndexPath *selectedCellIndexPath;
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
if (![self.selectedCellIndexPath isEqual:indexPath]) {
UICollectionViewCell *lastSelectedCell = [collectionView cellForItemAtIndexPath:selectedIndexPath];
// Perform any change to lastSelectedCell before deselecting it
[collectionView deselectItemAtIndexPath:lastSelectedIndexPath animated:YES];
}
self.selectedCellIndexPath = indexPath;
UICollectionViewCell *selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
// Change what you want in the newly selected cell;
}

iOS: Storyboard CollectionView segue not being triggered

I have a UICollectionView controller embedded inside a navigation controller. The collectionView lists projects and each cell is supposed to segue to a ProjectDetail screen.
I simply cannot get the segue to trigger. If I simply drop a button on the nav bar and hook up a segue to the detail, it works. But triggering from my CollectionView cell doesn't.
Here is what the storyboard looks like: http://cl.ly/RfcM I do have a segue hooked up from the CollectionViewCell to the ProjectDetailViewController
Here's the relevant code inside my ProjectDetailViewController:
#interface ProjectCollectionViewController () {
NSArray *feedPhotos;
Projects *projects;
}
#end
#implementation ProjectCollectionViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.collectionView registerClass:[FeedViewCell class] forCellWithReuseIdentifier:#"cell"];
[self loadData];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"selected %d", indexPath.row);
Project *project = [projects getProject:indexPath.row];
NSLog(#"project = %#", project);
}
- (void)loadData {
[self.projectLoader loadFeed:self.username
onSuccess:^(Projects *loadedProjects) {
NSLog(#"view did load on success : projects %#", loadedProjects);
projects = loadedProjects;
[self.collectionView reloadData];
}
onFailure:^(NSError *error) {
[self handleConnectionError:error];
}];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return projects.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = #"cell";
FeedViewCell *cell = (FeedViewCell *) [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
cell.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:1.0];
UIImageView *cellImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
Project *project = [projects getProject:indexPath.row];
NSString *imageUrl = [project coverPhotoUrl:200 forHeight:200];
NSLog(#"imageurl =>%#", imageUrl);
if (imageUrl) {
[cellImageView setImageWithURL:[NSURL URLWithString:imageUrl]];
}
[cell addSubview:cellImageView];
cell.imageView = cellImageView;
return cell;
}
I'm guessing the problem is somewhere in how I'm hooking up the Cells to the CollectionView.
Any help would be greatly appreciated!
You cannot create segues directly from cells in a storyboard because the collectionview is populated dynamically through the data source. You should use the collectionView:didSelectItemAtIndexPath: and perform the segue programatically using performSegueWithIdentifier:sender:. Something like this:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[self performSegueWithIdentifier:#"MySegueIdentifier" sender:self];
}
where MySegueIdentifier is the identifier of the segue defined in storyboard.
TLDR: FOR A STORYBOARD, do not call registerClass:forCellWithReuseIdentifier:. It overrides what the storyboard sets up for the cell (including how segues are handled):
How to set a UILabel in UICollectionViewCell
Brief setup
Used a storyboard
Created a new collection view controller using the Xcode template,
setting it as a subclass of UICollectionViewController.
Initially used the default UICollectionViewCell, adding a UILabel
programmatically.
The generated UICollectionViewController code registered the cell in viewDidLoad:
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];
First Issue:
The prepareForSegue:sender: event was not firing, which brought me to this answer .
I implemented the UICollectionViewDelegate and collectionView:didSelectItemAtIndexPath: event, then called the segue programmatically.
This fixed my first issue.
Second Issue: I switched to a custom cell containing one label. After hooking everything up, the cell label was not displaying.
After some digging, I found a solution contained in the link at the top of my answer.
Third Issue and Solution: I removed the registerClass:forCellWithReuseIdentifier: line. When I ran my app, the label appeared correctly, but when I tapped a cell, it called the prepareForSegue:sender event twice. By removing the registerClass:forCellWithReuseIdentifier line, the cell was processing cell touches directly, without the need of the delegate method. This is how I expected the storyboard to work. I deleted the collectionView:didSelectItemAtIndexPath: event, which resolved the double-firing of prepareForSegue:sender:. If you are using a storyboard, do not register the cell class. It overwrites what storyboard sets up.
Have you made your CollectionView Cell's connection in Triggered Segues on selection?
You can also trigger a segue programatically using
[self performSegueWithIdentifier:#"segueIdentifier" sender:nil]
in
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
Equivalent Swift code for similar question.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier(#"TargetSegway", sender: self)
}
Make sure, in case if your cell has other overlapping views, "User Interaction Enabled" is unchecked (you can find this option, under attribute inspector View/Interaction). Otherwise, your Tap Gesture is consumed by the overlapping view, didSelectItemAtIndexPath may not be called.

Use a button on a UICollectionViewCell to display data from an array

I have an array of NSStrings, one UILabel & a UICollectionView.
My Question:
I want the array's count to determine how many UICollectionViewCell's there are.
Each UICollectionViewCell contains a button. Upon click, I want this button to cause the data in the array that corresponds to the UICollectionViewCell's number to be displayed in the label.
For example, if the user clicks on the 13th UICollectionViewCell's button, then the 13th NSString in the array would become the UILabel's text.
What I have done:
I have made my own subclass of UICollectionViewCell for the nib file that I use for all of the UICollectionViewCells, & connected the button to the .h file as a IBAction. I have also imported the MainViewController.h, which is the one that contains the array property that stores the NSStrings.
When I edit the code in the UICollectionViewCell's action, I cannot access the array property. The button does work - I placed an NSLog in the IBAction's method, which does work.
I have searched through tens of other answers on SO, but none answer my specific question. I can update this with samples of my code if requested.
I have made my own subclass of UICollectionViewCell for the nib file
that I use for all of the UICollectionViewCells, and connected the
button to the .h file as a IBAction.
If you connect the IBAction to the subclass of collectionViewCell you would need to create a delegate to make the touch event available in the viewController where you are displaying the data.
One easy tweak is to add the button the collectionViewCell, connect it's IBOutlet to the cell. But not IBAction. In the cellForRowAtIndexPath: add an eventHandler for button in that viewController containing collectionView.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//Dequeue your cell
[cell.button addTarget:self
action:#selector(collectionViewCellButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
return cell;
}
- (IBAction)collectionViewCellButtonPressed:(UIButton *)button{
//Acccess the cell
UICollectionViewCell *cell = button.superView.superView;
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
NSString *title = self.strings[indexPath.row];
self.someLabel.text = title;
}
Please try like this..
In YourCollectionViewCell.h
Create an IBOutlet not IBAction called button for the UIButton that you added to the xib. Remember you should connect the outlet to the cell object not to the file owner in the xib.
MainViewController.m
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
cell.button.tag = indexPath.row;
[cell.button addTarget:self action:#selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
-(void)buttonPressed:(UIButton*)sender
{
NSLog(#"%d : %#",sender.tag,[array objectAtIndex:sender.tag]);
self.textLabel.text = [array objectAtIndex:sender.tag];
}
Edit- Handle multiple sections
-(void)buttonPressed:(UIButton*)sender
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell: (UICollectionViewCell *)sender.superview.superview];
NSLog(#"Section : %d Row: %d",indexPath.section,indexPath.row);
if (0 == indexPath.section) {
self.textLabel.text = [firstArray objectAtIndex:indexPath.row];
}
else if(1 == indexPath.section)
{
self.textLabel.text = [secondArray objectAtIndex:indexPath.row];
}
}
When I edit the code in the UICollectionViewCell's action, I cannot access the array property.
That's because you connected the button action to the "wrong" object. It needs to be connected to the MainViewController (or whoever it is that does have access to the array property).
You are going to have several tasks to perform:
Receive the button action message.
Access the array (the model for the data).
Throw a switch saying which cell should now have its label showing.
Tell the collection view to reloadData, thus refreshing the cells.
All those tasks should most conveniently belong to one object. I am presuming that this is MainViewController (and thus I am presuming that MainViewController is the delegate/datasource of the collection view).

Select UITableViewCell text when a new cell is added to a UITableView

I have a basic UITableView which contains basic UITableViewCells. When I add a new cell to the table I'd like to:
1) scroll to the new row
2) select all of the text in the new UITableViewCell so that the keyboard becomes visible and the user can immediately edit the cell's text.
The next time through the event loop (after calling -reloadData on the table view) I do:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:wordIndex inSection:0];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:(UITableViewScrollPositionNone)];
UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell.textLabel becomeFirstResponder];
cell.highlighted = YES;
The scrolling is correct but the text in the cell is not selected.
A UILabel can't become a first responder because it returns NO from canBecomeFirstResponder. To give the illusion of a label that is edited, you could try using a UITextField with a borderStyle of UITextBorderStyleNone.
Be careful with the timing of the becomeFirstResponder call, since a control can't be first responder if it is not a subview of a window. This can happen if you are scrolling to a row that is very far off screen and immediately trying to call becomeFirstResponder before it is added as a visible row.
As in previous answer you can create custom child of UITableViewCell, add UITextField on that.
#interface MyTableViewCell : UITableViewCell
#property(nonatomic, retain) IBOutlet UITextField *editableTextField;
#end
Than you can catch didSelectRowAtIndexPath in your controller and set it's textfield to become first responder
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
[cell.editableTextField becomeFirstResponder];
}
UPD: as a trouble resolve for Not-in-window-cell trouble, described by upper answer, you must call selectCellAtIndexPath in method
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView

Resources