I have UICollection in which there will be number of Students items and each item having switch inside it used for recording attendance. I am loop through all visible cells like this.
for(attendancecollectionViewCell* cells in [[self collectionView] visibleCells]){
NSLog(#"The switch value : %c",cells.attendanceSwitchLabel.isOn);
}
But, I wanted to loop through all the cells for attendance, not just the visible cells.
You cannot loop through non-visible cells because those cells don't exist. UICollectionView, like UITableView, reuses cells as soon as they are offscreen. I.e., if you scroll down, it takes a cell that has been scrolled off and uses it for a "new" cell about to be scrolled into view.
If you wish to hold state for an entry in your collection, you'll have to store it separately from the cell itself. For example, an NSArray of structs (or custom NSObjects) that map to the indexPath.row value.
A more important question for you specifically would be: What are you trying to achieve in your for loop?
Let me know if you need more information or sample code.
Swift 4+
func getAllCells() -> [UICollectionViewCell] {
var cells = [UICollectionViewCell]()
// assuming tableView is your self.tableView defined somewhere
for i in 0...self.numberOfSections-1
{
for j in 0...self.numberOfItems(inSection: i) - 1
{
if let cell = self.cellForItem(at: NSIndexPath(row: j, section: i) as IndexPath) {
cells.append(cell)
}
}
}
return cells
}
Related
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
I've got a UIImageView to place a picture there and UITableView to place comments there. The part of the view with UIImageView should be scrolled with comment cells, that's why separate UIImageView and UITableView can't be used - the image won't be scrolled. If UIScrollView is used, than I receive scroll in scroll. Can cellForRowAtIndexPath method be used to create different cells? E.g.:
if (indexPath.row == 0) {
CellWithImage * cell = (CellWithImage *)[tableView dequeueReusableCellWithIdentifier:#"CellWithImage"];
//some code here
return cell;
}
else {
CellWithComment * cell = (CellWithComment *)[tableView dequeueReusableCellWithIdentifier:#"CellWithComment"];
//some code here
return cell;
}
For your case, its better to make the imageview part of the header view for the section
Yes, you can use different identifiers to load different cell types and use them together inside the same table. In fact, your case is somewhat simple -- you're only using a different cell for the first row. You could easily choose the cell type based on the type of data that will be displayed in the row, position of data within a hierarchy, etc.
I acknowledge that UITableview load dynamically a cell when user scrolls. I wonder if there is a way to preload all cells in order not to load each one while scrolling. I need to preload 10 cells. Is this possible?
You can initialize table view cells, put them into an array precomputedCells and in the data source delegate method tableView:cellForRowAtIndexPath: return the precomputed cells from the array instead of calling dequeueReusableCellWithIdentifier:. (Similar to what you would do in a static table view.)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self.precomputedCells objectAtIndex:indexPath.row];
}
It might also be useful to have a look at the
WWDC 2012 Session 211 "Building Concurrent User Interfaces on iOS"
where it is shown how to fill the contents of table view cells in background threads while keeping the user interface responsive.
I was looking for a solution to the original question and I thought I'd share my solution.
In my case, I only needed to preload the next cell (I won't go into the reason why, but there was a good reason).
It seems the UITableView renders as many cells as will fit into the UITableView frame assigned to it.
Therefore, I oversized the UITableView frame by the height of 1 extra cell, pushing the oversized region offscreen (or it could be into a clipped UIView if needed). Of course, this would now mean that when I scrolled the table view, the last cell wouldn't be visible (because the UITableView frame is bigger than it's superview). Therefore I added an additional UIView to the tableFooterView of the height of a cell. This means that when the table is scrolled to the bottom, the last cells sits nicely at the bottom of it's superview, while the added tableFooterView remains offscreen.
This can of course be applied to any number of cells. It should even be possible to apply it to preload ALL cells if needed by oversizing the UITableView frame to the contentSize iOS originally calculates, then adding a tableFooterView of the same size.
Hopefully this helps someone else with the same problem.
As suggested by Mark I also changed the height of my UITableView temporarily so that the table view creates enough reusable cells. Then I reset the height of my table view so that it stops creating reusable cells while scrolling.
To accomplish that I create a helper bool which is set to false by default:
var didPreloadCells = false
It is set to true when my table view first reloaded data and therefore created the first reusable cells.
resultsHandler.doSearch { (resultDict, error) -> Void in
[...]
self.tableView.reloadData()
self.didPreloadCells = true
[...]
}
The real trick happens in my viewDidLayoutSubviews Method. Here I set the frame of my table view depending on my boolean. If the reusable cells were not created yet I increase the frame of the table view. In the other case I set the normal frame
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tableView.frame = self.view.bounds
if !didPreloadCells
{
self.tableView.frame.size.height += ResultCellHeight
}
}
With the help of that the table view creates more initial reusable cells than normal and the scrolling is smooth and fluent because no additional cells need to be created.
Change the dequeueReusableCellWithIdentifier when you are reusing the table view otherwise it will load the old data
Solution for autoresizing cells. Change 'estimatedRowHeight' to lower value
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 32; //Actual is 64
}
Actual estimated height is 64. 32 is used to add more cells for reuse to avoid lagging when scrolling begins
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.
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?