How to determine indexPath of currently visible UICollectionView cell - objective-c

I want to display some cell-specific text, but I can’t extricate the visible cell from the collection view.
Most helpful ones were these:
how to access from UICollectionViewCell the indexPath of the Cell in UICollectionView
detecting when an iOS UICollectionCell is going off screen
A lower collection view (self addSubview) contains possible items. An upper one (self addSubview) contains selections from the lower. The user presses a button (self addSubview again), the lower view scrolls off to oblivion and the upper one expands such that one cell fills the screen. All the array-building and displaying works fine. No *.nibs.
What doesn’t work is figuring out which cell the app is presenting to the user.
TRY #1: First I tried populating the UILabel in parallel with normal ops. Press the button and cellForItemAtIndexPath:
if (cv.tag == UPPER_CV_TAG) {
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
NSIndexPath *tempIndexPath;
tempIndexPath = itemAttributes.indexPath;
NSLog(#"cellForItem indexPath %#",indexPath);
NSLog(#"layout indexPath %#",tempIndexPath);
}
responds as you’d expect:
cellForItem indexPath <blah blah> 2 indexes [0, 0]
layout indexPath < > 2 indexes [0, 0]
cellForItem indexPath < > 2 indexes [0, 1]
layout indexPath < > 2 indexes [0, 1]
cellForItem indexPath < > 2 indexes [0, 2]
layout indexPath < > 2 indexes [0, 2]
When the gyrations conclude, Cell 0 correctly occupies center stage. Unfortunately, so does the description for cell 2. Scroll left / right, cells change accordingly, the corresponding text does not. It remains constant: that which maps to cell 2.
Obviously I’m passing the wrong index to the description array.
TRY #2: OK, fine. I’ll interrogate a point I know the currently visible cell occupies and reason backward from that.
CGPoint p = CGPointMake(512, 456);
NSIndexPath *theCellsIndexPath = [self.upper_collectionView indexPathForItemAtPoint:p];
NSLog(#"theCellsIndexPath=%d",theCellsIndexPath.item);
No cigar:
cellForItem indexPath < > 2 indexes [0, 0]
theCellsIndexPath=0
cellForItem indexPath < > 2 indexes [0, 1]
theCellsIndexPath=0
cellForItem indexPath < > 2 indexes [0, 2]
theCellsIndexPath=0
No change from “0” when I change 512,456 to other likely points. No change from “0” when I use layoutAttributes to determine where in the view the cell lies, just in case. Nope, that cell is a really, really big target. I’m not missing it.
TRY #3: Maybe I’m working too hard. I’ll trap the goings-on inside
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
}
and roll with that.
Nope. It never gets called.
TRY #4: Since I don’t know the indexPath of the single cell on the screen (but the collectionView does) I suppose I could iterate through all the cells with didEndDisplayingCell until I hit “Bingo!” That’s crude and indicative of programmer error.
I’m glad the collectionView logic buffers three cells. Probably improves user experience or something. But where it ends up – on 0 – is not where my array index last knew it was – at 2.
What part of the docs didn’t I read?

What part of the docs didn’t I read?
UICollectionView has a method called indexPathsForVisibleItems . Make sure you read it through to see what else you can use from the API.

Try collectionView.visibleCells
NSArray *ary = collectionView.visibleCells;
for (UICollectionViewCell *cell in ary) {
NSIndexPath *path = [collectionView indexPathForCell:cell];
NSLog(#"indexPath of cell: Section: %d , Row: %d", (int)path.section, (int)path.row);
}

Related

Custom focus engine behaviour for UICollectionView

I use a standard UICollectionView with sections. My cells are laid out like a grid. The next cell in a chosen direction is correctly focused if the user moves the focus around with the Apple TV remote. But if there is a "gap" in the grid, the default focus engine jumps over sections. It does this to focus a cell which could be several sections away but is in the same column.
Simple Example:
There are 3 sections. The first section has 3 cells. The second has 2 cells and the last one has 3 cells again. See the following image:
If the green cell is focused and the user touches the down direction, the yellow cell gets focused and section two is skipped by the focus engine.
I would like to force it that no sections can get jumped over. So instead of focusing the yellow cell I would like to focus the blue cell.
I learned that the Apple TV Focus engine internally works like a grid system and that the described behaviour is the default one. To allow other movements (e.g. diagonal) we need to help the focus engine by placing invisible UIFocusGuides which can redirect the focus engine to a preferredFocusedView.
So in the following image there is one invisible red focus guide placed into the empty space of a UICollectionView section which would redirect the down focus to the desired blue cell. I think that would be the perfect solution, in theorie.
But how would I add UIFocusGuides to all empty spaces of UICollectionView sections? I have tried several things but nothing worked. Maybe add it as a Decorator View but that seems wrong. Or as additional cells, but that breaks the data layer and the constraints anchors do not work.
Has anyone an idea on how to add UIFocusGuides to a UICollectionView?
Two ways to achieve your goal. At least what I tried and made it work. =). There might be other ways too.
1:
The easiest way to achieve your goal is add vertical collection view with cells that contains horizontal collection view. Each horizontal collection is your section.
Make sure you added the following code to the vertical collection view:
func collectionView(collectionView: UICollectionView, canFocusItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return false
}
2:
If you want to use UIFocusGuide I think a good place to add focus guide is section header. Make sure your focus guide in section header and update preferredFocusedView as switch to each section. In my case I assign the first cell of each section when I leave the section.
One solution is to subclass UICollectionViewFlowLayout with a layout that adds Supplementary Views with UIFocusGuides on top for all empty areas.
Basically the custom flow layout calculates the needed layout attributes in the prepareLayout like this:
self.supplementaryViewAttributeList = [NSMutableArray array];
if(self.collectionView != nil) {
// calculate layout values
CGFloat contentWidth = self.collectionViewContentSize.width - self.sectionInset.left - self.sectionInset.right;
CGFloat cellSizeWithSpacing = self.itemSize.width + self.minimumInteritemSpacing;
NSInteger numberOfItemsPerLine = floor(contentWidth / cellSizeWithSpacing);
CGFloat realInterItemSpacing = (contentWidth - (numberOfItemsPerLine * self.itemSize.width)) / (numberOfItemsPerLine - 1);
// add supplementary attributes
for (NSInteger numberOfSection = 0; numberOfSection < self.collectionView.numberOfSections; numberOfSection++) {
NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:numberOfSection];
NSInteger numberOfSupplementaryViews = numberOfItemsPerLine - (numberOfItems % numberOfItemsPerLine);
if (numberOfSupplementaryViews > 0 && numberOfSupplementaryViews < 6) {
NSIndexPath *indexPathOfLastCellOfSection = [NSIndexPath indexPathForItem:(numberOfItems - 1) inSection:numberOfSection];
UICollectionViewLayoutAttributes *layoutAttributesOfLastCellOfSection = [self layoutAttributesForItemAtIndexPath:indexPathOfLastCellOfSection];
for (NSInteger numberOfSupplementor = 0; numberOfSupplementor < numberOfSupplementaryViews; numberOfSupplementor++) {
NSIndexPath *indexPathOfSupplementor = [NSIndexPath indexPathForItem:(numberOfItems + numberOfSupplementor) inSection:numberOfSection];
UICollectionViewLayoutAttributes *supplementaryLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:ARNCollectionElementKindFocusGuide withIndexPath:indexPathOfSupplementor];
supplementaryLayoutAttributes.frame = CGRectMake(layoutAttributesOfLastCellOfSection.frame.origin.x + ((numberOfSupplementor + 1) * (self.itemSize.width + realInterItemSpacing)), layoutAttributesOfLastCellOfSection.frame.origin.y, self.itemSize.width, self.itemSize.height);
supplementaryLayoutAttributes.zIndex = -1;
[self.supplementaryViewAttributeList addObject:supplementaryLayoutAttributes];
}
}
}
}
and then returns the needed layouts in the layoutAttributesForSupplementaryViewOfKind: method like this:
if ([elementKind isEqualToString:ARNCollectionElementKindFocusGuide]) {
for (UICollectionViewLayoutAttributes *supplementaryLayoutAttributes in self.supplementaryViewAttributeList) {
if ([indexPath isEqual:supplementaryLayoutAttributes.indexPath]) {
layoutAttributes = supplementaryLayoutAttributes;
}
}
}
Now your Supplementary Views just need a UIFocusGuide the same size as the supplementary view itself. That's it.
A full implementation of the method described can be found here on gitHub
Maybe you could try to implement optional func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? method, which is part of UICollectionViewDelegate?
Implements this method for your Section 1, 2, 3... UICollectionView and returns, let's say IndexPath(item: 0, section: 0) if you want the focus engine to automatically focus the first item.
Note that, if you would want to also set sectionCollectionView.remembersLastFocusedIndexPath= true

Object/Item out of UICollectionView cells instead of index?

I've been trying to save the state or order of cells in a UICollectionView after they have changed locations in the UICollectionView. When I use indexPathsForVisibleItems I get indexes that are always in the same order. They do not reflect order change. If I could get the object at each indexPath I could at least make an attempt to iterate through them to get the resulting order.
I used this code. And found that it returned a refreshed state of indexPaths in [0,0]. The ordering is always the same because the it tells me what cells have something in them not what is in each cell. I just don't know enough or where to start to get the object/item in each cell.
NSArray *visible = [self.collectionView indexPathsForVisibleItems];
NSMutableArray *rowsArray = [NSMutableArray arrayWithCapacity:[visible count]];
[visible enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
[rowsArray addObject:#(indexPath.item)];
I need the items or objects at each location. How do I iterate through all the cell locations to retrieve the item there. I plan on using the count of an array used to populate the UICollectionView to loop through all the locations and get the object stored there. There is only one section and cells seem to be storing objects like this <Slide: 0x7585200>
The function above returns NSLogs in this format "<NSIndexPath 0x8490390> 2 indexes [0, 0]"
I need to get what is in that location.
I'm not familiar with NSIndexPath or it's formatting. Even just the command/function would help for me to get an object out. Thanks.
(Addendum) I'm think of using something like [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; Not sure what I can store it in.
CollectionView is a collection of UIViews (call UICollectionViewCell). Each cell has an indexPath (section,row) mean the layout position in collectionView content. In the case you has 1 section, section always = 0.
visibleCells mean all cells visible on screen at that time (visible in scroll frame or collectionView frame).
indexPathsForVisibleItems = indexPath array of visibleCells
cellForItemAtIndexPath is a method to get a collectionViewCell at any indexPath you want, often use to get cell when select:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
NSLog(#"%#",cell);
}
Investigate more from Apple UICollectionView video and example:
https://developer.apple.com/library/ios/#samplecode/CollectionView-Simple/Introduction/Intro.html
https://developer.apple.com/videos/wwdc/2012/

UITableView: load all cells

Is it possible to load all cells of an UITableView when the view is loaded so that they are not loaded when I'm scrolling?
(I would show a loading screen while doing this)
Please, it's the only way at my project (sorry too complicate to explain why ^^)
EDIT:
Okay let me explain you, what I'm definite doing:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier = [NSString stringWithFormat:#"Identifier %i/%i", indexPath.row, indexPath.section];
CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
NSDictionary *currentReading;
if (cell == nil)
{
cell = [[[CustomTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
UILabel *label;
UIView *separator;
if(indexPath.row == 0)
{
// Here I'm creating the title bar of my "table" for each section
}
else
{
int iPr = 1;
do
{
currentReading = [listData objectAtIndex:iPr-1];
iPr++;
} while (![[currentReading valueForKey:#"DeviceNo"] isEqualToString:[devicesArr objectAtIndex:indexPath.section]] ||
[readingresultsArr containsObject:[currentReading valueForKey:#"ReadingResultId"]]);
[readingresultsArr addObject:[currentReading valueForKey:#"ReadingResultId"]];
//
// ...
//
}
}
return cell;
}
My error happens in the do-while-loop:
"listData" is an array with multiple dictionaries in it.
My problem ist that when I’m scrolling my table slowly down, all is fine, but when I’m scrolling quickly to the end of the view and then I’m scrolling to the middle, I get the error that iPr is out of the array’s range. So the problem is, that the last row of the first section has already been added to the "readingresultsArr", but has not been loaded or wants to be loaded again.
That’s the reason why I want to load all cells at once.
You can cause all of the cells to be pre-allocated simply by calling:
[self tableView: self.tableView cellForRowAtIndexPath: indexPath];
for every row in your table. Put the above line in an appropriate for-loop and execute this code in viewDidAppear.
The problem however is that the tableView will not retain all of these cells. It will discard them when they are not needed.
You can get around that problem by adding an NSMutableArray to your UIViewController and then cache all the cells as they are created in cellForRowAtIndexPath. If there are dynamic updates (insertions/deletions) to your table over its lifetime, you will have to update the cache array as well.
put a uitableview on a uiscrollview
for example , you expect the height of the full list uitableview is 1000
then set the uiscrollview contentsize is 320X1000
and set the uitableview height is 1000
then all cell load their content even not visible in screen
In my case it was that I used automaticDimension for cells height and put estimatedRowHeight to small that is why tableview loaded all cells.
Some of the answers here and here suggest using automaticDimension for cells height and put mytable.estimatedRowHeight to a very low value (such as 1).
Starting with iOS 15 this approach seems not to work anymore. Hence, another way to achieve the table to "load" all cells could be by automatically scrolling to the last cell. Depending on the tables height and how many rows it can show some cells are discarded but each cell would be loaded and shown at least once.
mytable.scrollEnabled = YES;
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:cellCount - 1 inSection:0];
[mytable scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
mytable.scrollEnabled = NO;
If you want to scroll up again just scroll to the top as outlined here.
Following the comment that was made by juancazalla, I found that if you have a tableView that is using automaticDimension, loading all the cells at once can be best achieved by setting estimatedRowHeight to a low value (such as 1).

UITableView accessory added to additional cells when added to index 0

I'm having a hard time understanding some of the logic behind the UITableView. I am populating my table from a MPMediaItemCollection as a queue of songs. What I am trying to achieve is to have a now playing indicator as the accessory view of the cell at the index path that matches the currently playing song.
I originally tried this with the following:
if (indexPath.row == [mutArray indexOfObject:[mainViewController.musicPlayer.nowPlayingItem valueForProperty:MPMediaItemPropertyTitle]]) {
UIImageView *playButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"PlayButton.png"]];
[cell setAccessoryView:playButtonView];
}
This works fine for the now playing item, which is always actually objectAtIndex:0. But what I don't understand is why my table seems to define this index once every height of combined visible cells.
Let me try to explain this more clearly. Lets say that my table is 600px tall, and its content height is 1800px. This causes the indicator to be added roughly every 600px down the content height.
Now my first thought was that this was something wrong with the code for judging the index based off the name of the song, so I tried changing it to:
if (indexPath.row == 0)
But this produces the same result!
This screenshot should help explain what I'm talking about.
So, is there anything I can do to make the table treat indexPath0 as only the first cell in reference to the entire table instead of in reference to the currently visible cells?
You have to state explicitly also when the accessory should not be there:
if (indexPath.row==0) {
UIImageView *playButtonView = [[UIImageView alloc] initWithImage:
[UIImage imageNamed:#"PlayButton.png"]];
[cell setAccessoryView:playButtonView];
}
else {
[cell setAccessoryView:nil];
}
The reason is that when cell 0 gets dequeued (i.e. reused) on a different row it still has the accessory view in it.
Actually, this is a nice case study for understanding how dequeueing table view cells actually works ;-).
That's how tableview reuse cells: when cell scrolls out of screen it's added to reuse pool, so once you've added your accessory view to cell and that cell is reused - you'll see it in random places while scrolling.
You can check your cells index in -willDisplayCell:forIndexPath and add (if it's not added), hide (if it's there, but not your desired index) or show (if it's there and it's your index), or add accessory view to all cells and show/hide as needed.

Make cells appear and disappear in TableView

I making an app with a table view and a data source (core data). In this table i group several tasks ordered by date, and i have this segmented control.
I want the table to only load the tasks later or equal than today's date, when the user taps the second segment i want to show all tasks, if he taps the first segment the table must only show the later dates tasks again.
The problem is:
1 - I'm using fetchedResultsController associate with a indexPath to get the managed object.
2 - I use the insertRowsAtIndexPaths:withRowAnimation: and deleteRowsAtIndexPaths:withRowAnimation: methods to make the cells appear and disappear. And this mess with my indexPaths, if i want to go to the detail view of an specific row it is associate with a different indexPath, after delete the rows.
This problem was fixed by a method i did, but i still have other problems of indexPaths and cells, and it seems to me that is gone be me messy to each problem a fix.
There is a simple way to do that?
I tried just to hide the cells instead of delete, it works just fine, but in the place of the hidden cells was a blank space, if there is a way to hide these cells and make the non-hidden cells occupy the blank space i think that will be the simplest way.
Anyone can help me?
set the height of the cell to 0 when it hides, and set the height back to the original value when it appears.
TableViewController.h
#interface TableViewController{
CGFloat cellHeight;
}
TableViewController.m
- (void)cellHeightChange{
//if you need hide the cell then
cellHeight = 0;
cellNeedHide.hidden = YES;
//if you need hide the cell then
cellHeight = 44; // 44 is an example
cellNeedHide.hidden = NO;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
{
switch (section) {
// for example section 0 , row 0 is the cell you wanna hide.
case 0:
switch (row) {
case 0:
return cellHeight;
}
}
}
When the user taps on a segment execute a new fetch request on your managed object to give you an appropriate array (either an array of all dates, or the greater/equal dates). Then use reloadData on the tableView using this new array in the datasource.
or
Give the cell's you wish to hide a height of 0?