Are there any way to get all cells on the row from UICollectionView given the indexPath.row?
I guess you can make something like this:
var itemsInRow1: [Int] = [1,2,3]
var cellsInRow: [Cells] = []
for item in 0..<itemsInRow1 {
cellsInRow.append(collectionView.cellForItem(at: IndexPath.init(row: item,
section: 0)))
}
But i guess it depends on how you have your dataSourceModel setup
There is no such method. You have to mathematically calculate according to the collection view width and some calculation according to the row number you enter.
Related
I have a custom cell composed by 3 StackView. Each one of them has a title, a description and an image, horizontally.
I have to fill this cell with an Array, it could be made of max 3 elements, but it could have 2 or 1.
So in my viewModel I'm treating this array like this ...
let firstItem = myArray[0]
let secondItem = myArray[1]
let thirdItem = myArray[2]
And I fill the field with firstItem.name firstItem.description ... For each one of them (not the best approach I guess)
Then I made some check if index exist, if it doesn't I delete the StackView, I set manually some constraints and I fit the cell to the content ( If I have 2 elements the cell is shorter, If I have 3 elements the cell is bigger).
This is a piece of code after I check that index 3 doesn't exist:
self.stackView.removeFromSuperview()
self.ownConstraints.constant = value (20 for example)
My question is, what is the best approach to achieve this? With cell I usually append item with a for cycle one by one, but I'm not familiar with this approach on StackView inside a Cell.
This is what I have done with cell (a series of cell with 1 name and 1 description):
for (element) in myArray {
self.cellArray.append( elementName , elementDescription )
}
// hide all items in stackView
stackView.arrangedSubviews.forEach({ $0.isHidden = true })
// add or update arrangedSubviews
for (index, element) in myArray.enumerated() {
if index >= stackView.arrangedSubviews.count - 1 {
stackView.addArrangedSubview(UILabel())
}
(stackView.arrangedSubviews[index] as? UILabel)?.text = element
stackView.arrangedSubviews[index].isHidden = false
}
I would hide all subviews in stackView and only show subviews if content is available in your viewModel.
How could I load more data while scrolling to the top without losing the current offset oh the UITableView?
Here is what I am trying to achieve:
This is the whole set of data:
row 1
row 2
row 3
row 4
row 5
row 6
row 7
row 8*
row 9
row 10
row 11
row 12
row 13
row 14
row 15
Now, imagine that the user loaded the ones marked in bold and the offset is at row 8, if the user scroll up and reached row 7, I want to load and insert the rows from 1 to 5 without jumping from row 7. Keeping in mind that the user may be scrolling so when data reached the phone it is at row 6, so I can't jump it back to row 7, but keep the scroll smooth and natural (just how happen when you load more data while scrolling down, that the data is reloaded without the tableview jumping from between rows).
By the way, by offset I mean the contentOffset property of the UITableView.
Thanks for your help, I do really appreciate it!
When you are updating your data, you need to get the current offset of the tableView first. Then, you can add the height of the added data to the offset and set the tableView's offset like so:
func updateWithContentOffsset(data: [String]) {
guard let tableView = tableView else {
return
}
let currentOffset = tableView.contentOffset
let yOffset = CGFloat(data.count) * tableView.rowHeight // MAKE SURE YOU SET THE ROW HEIGHT OTHERWISE IT WILL BE ZERO!!!
let newOffset = CGPoint(x: currentOffset.x, y: currentOffset.y + yOffset)
tableView.reloadData()
tableView.setContentOffset(newOffset, animated: false)
}
You can also take a look at the gist that I created. Simply open up a new iOS Playground and copy-paste the gist.
The only thing you have to be aware of is that make sure you know your row height to add to the offset.
#Pratik Patel Bellow answer is best one.
Right down or call bellow function in the web-service response.
func updateWithScroll(data: [String]) {
guard let tableView = tableView else {
return
}
guard let currentVisibleIndexPaths = tableView.indexPathsForVisibleRows else {
return
}
var updatedVisibleIndexPaths = [IndexPath]()
for indexPath in currentVisibleIndexPaths {
let newIndexPath = IndexPath(row: indexPath.row + data.count, section: indexPath.section)
updatedVisibleIndexPaths.append(newIndexPath)
}
tableView.reloadData()
tableView.scrollToRow(at: updatedVisibleIndexPaths[0], at: .top, animated: false)
}
Now, imagine that the user loaded the ones marked in bold and the offset is at row 8, if the user scroll up and reached row 7, I want to load and insert the rows from 1 to 5 without jumping from row 7.
Loading data into the table as its needed is one of the things that UITableView does for you automatically. Don't try to insert rows into the table because they'll soon be needed as the user scrolls toward them -- the table view will request those rows from its data source as they're needed. You just need to make sure that your data source has the information it needs in order to fulfill the tables requests for cells as they arrive.
Things get a little more complicated if populating the rows requires making a network request for each row. Given your use of "load" in the question, I don't think that's what you're talking about, but in case that's your situation, here are some tips:
Request the data for as many rows as you reasonable can as early as you reasonably can. The amount of data displayed in a single row is typically small, so requesting a few hundred rows all at once shouldn't be a big deal.
If you don't have the data you need for a given row, make the necessary request, but return a cell immediately. The cell you return could use a spinner or other indication that the data is pending. When the request completes, you can tell the table to reload the appropriate row so that the proper content will display.
I have two questions :
1- Is it possible to know if a cell indexPath.row has changed in a UITableView ? for example if I have an element at index (0) and then I add another one, the first element will be at index (1). Is it possible to detect this change ?
2- Is it possible to fnd the index of an element by the cell title ? for example I have X,Y,Z element in the tableview and I want to know in which cell 'X' is placed ?
thanks guys,
Generally speaking, it's common practice to isolate model and view. What I mean by this, is separate the storage of the data and the view that's rendering it.
For example, you may be creating a table view of color names. Therefore, you could store an array of color names as a property of your view controller
class UIColorViewController: UITableViewController{
let colors = ["Black", "Blue", "Yellow"]
As I'm sure you're doing, you can assign the view controller as the data source of the table view so you can tell it how many sections you want, the number of rows you want in a section, and what to render onto a cell.
In this elementary example, the number of sections could be 1 and the number of rows in this section would be the length of the colors array. Now when rendering data onto the cell, you simply index the colors array by the index path.
fun tableView(UITableView, cellForRowAt: IndexPath){
var color = colors[indexPath.row]
cell = //dequeue cell
cell.textLabel?.text = color
return cell
}
So by separating the data from the view, if you want to find what cell a particular value is at, you can simply search through the array where the color is at and the index returned would be equivalent to what cell this color is or will be at depending on if that cell has been rendered onto the screen yet.
For example using the colors array, if I were looking for "Blue"
for (index, color) in self.colors.enumerated{
if color == 'Blue'{
print("Found Blue at index \(index)")
}
}
When you want to add a cell or delete a cell, you simply modify the colors array which is storing the model data, perform the appropriate table view related action, and the invariant remains that the index of the color in the array would be equivalent to the index of the cell in the table view.
Is there any chance to scroll in the UICollectionView to the wanted item using . scrollToItemAtIndexPath and snap not to the item itself, but to the page the item is part of? (I got paging enabled.)
Cheers
You need to create NSIndexPath than scroll to that index.
//Mark: - Move to cell when view did appear
overload viewDidAppear() {
let scrollIndex: NSIndexPath = NSIndexPath(forItem: dayIndex, inSection: monthSection)
if (// Check you are in range of UIcollectionView's count) {
//Scroll collectionview according to your requirement i.e UICollectionViewScrollPositionCenteredHorizontally or UICollectionViewScrollPosition.Left
self.YourCollectionView.scrollToItemAtIndexPath(scrollIndex, atScrollPosition: UICollectionViewScrollPositionCenteredHorizontally, animated: true)
}
}
I did end up using offsetContent property;
I knew the width of every so called page.
I wrote a code to get the indexPath for current day
I retrieved the section of the indexPath
I multiplied the section by the width of the view and named it "offset"
I set my UICollectionView.contentOffset.x to "offset" and it works fine.
I also tried using .scrollRectToVisible and the offset, and it worked amazingly, but I also wanted to updated something that was based on the contentOffset of the UICollectionView, and the .scrollRectToVisible seems not to update this property - it was updated only after I dragged the view a little.
Thanks for all your help!
Each UICollectionViewCell has its own button hooked up to the following action:
#IBAction func dropDown(sender:UIButton){
var pt = sender.bounds.origin
var ptCoords : CGPoint = sender.convertPoint(pt, toView:sender.superview);
var ptCoords2 : CGPoint = sender.convertPoint( ptCoords, toView: collectionView!);
var cellIndex: NSIndexPath = self.collectionView!.indexPathForItemAtPoint(ptCoords2)!
//var i : NSInteger = cellIndex.row;
//var i2 : NSInteger = cellIndex.section;
var selectedCell = collectionView?.cellForItemAtIndexPath(cellIndex) as CollectionViewCell!
selectedCell.button.backgroundColor = UIColor.blackColor()
for (var i = 0; i < 3; i++){
var textView : UITextView! = UITextView(frame: CGRectMake(self.view.frame.size.width - self.view.frame.size.width/1.3, CGFloat(50 + (30*(i+1))), CGRectGetWidth(self.view.frame), CGFloat(25)))
textView.backgroundColor = UIColor.whiteColor()
selectedCell.contentView.addSubview(textView)
}
}
What I want to do is add 3 subviews to only the cell that's been tapped. The subviews are added successfully, but as soon as I scroll, cells that come into view & correspond to the previously set indexPath are loaded with 3 subviews. I figure this is due to the dequeueReusableCellWithReuseIdentifier method, but I can't figure out a way around it. I considered removing the subviews on scrollViewDidScroll, but ideally I would like to keep the views present on their parent cell until the button is tapped again.
EDIT:
Okay, I ditched the whole convertPoint approach and now get the cell index based on button tags:
var selectedCellIndex : NSIndexPath = NSIndexPath(forRow: cell.button.tag, inSection: 0)
var selectedCell = collectionView?.cellForItemAtIndexPath(selectedCellIndex) as CollectionViewCell!
Regardless, when I try to add subviews to only the cell at the selected index, the subviews are duplicated.
EDIT:
I've created a dictionary with key values to track the state of each cell like so:
var cellStates = [NSIndexPath: Bool]()
for(var i = 0; i < cellImages.count; i++){
cellStates[NSIndexPath(forRow: i, inSection: 0)] = false
}
which are set by cellStates[selectedCellIndex] = true within the dropDown function. Then in the cellForItemAtIndexPath function, I do the following check:
if(selectedIndex == indexPath && cellStates[indexPath] == true){
for (var i = 0; i < 3; i++){
var textView : UITextView! = UITextView(frame: CGRectMake(cell.frame.size.width - cell.frame.size.width/1.3, CGFloat(50 + (30 * (i+1))), CGRectGetWidth(cell.frame), CGFloat(25)))
textView.backgroundColor = UIColor.whiteColor()
cell.contentView.addSubview(textView)
println("display subviews")
println(indexPath)
}
} else {
println("do not display subviews")
println(indexPath)
}
return cell
where selectedIndex, the NSIndexPath of the active cell set via the dropDown function, is compared to the indexPath of the cell being created & the cellState is checked for true.
Still no luck - the subviews are still displayed on the recycled cell. I should mention that "display subviews" and "do not display subviews" are being logged correctly while scrolling, so the conditional statement is being evaluated successfully.
MY (...hack of a...) SOLUTION!
Probably breaking a bunch of best coding practices, but I assigned tags to all the created subviews, remove them at the beginning of the cellForItemAtIndexPath method, and create them again if the cellState condition returns true.
No problem. Basically, you need to store program state OUTSIDE your UI components in what is commonly called a "model". Not sure what your app is so I am going to make up an example. Assume you want to show a grid where each cell is initially green and they toggle to red when the user taps it. You would need to store the state (I.e., whether a cell has been tapped or not) in some two dimensional array, which is going to contain a Boolean for ALL cells, and not just the ones that are currently showing (assuming you have enough cells to make the grid scroll). When the user taps a cell you set the flag in corresponding array element. Then, when the iOS calls you back to provide a cell (in the dequeue method) you check the state in the array, apply the appropriate color to the UIView of the cell, then return it. That way, iOS can reuse the cell view objects for efficiency, while at the same time you apply your model state to corresponding cells dynamically. Let me know if this clear.
One of two things:
- Disallow pooling of cells.
- Maintain sufficient info in your mode to be able to draw cells depending on the model rather than on their location on screen. That is, store a bit in your model that determines whether or not to show the three views for each "logical" cell. Then, when asked to dequeue a cell, check its model and add/remove the backgrounds dynamically.