is it possible to reverse the order of a tableView. I have searched a lot for a solution but all the results have not quite been a solution to what I am trying to achieve. They all suggest scrolling to the last position of a table with scrollToRowAtIndexPath and populating the data in reverse. But this doesn't work if the table content is dynamic and in some instances not all the cells have data. For example in a normal tableView the order is:
label 1
label 2
label 3
empty
empty
scroll direction
v
V
the desired result would be:
scroll direction
^
^
empty
empty
empty
label 3
label 2
label 1
in this example if I used the suggested method of scrollToRowAtIndexPath and use the length of the array of objects, I would only get the third cell from the top. And end up with something like this:
unwanted outcome:
label 3
label 2
label 1
empty
empty
scroll direction
v
V
any help would be great thank you.
To populate UITableView from the bottom:
- (void)updateTableContentInset {
NSInteger numRows = [self.tableView numberOfRowsInSection:0];
CGFloat contentInsetTop = self.tableView.bounds.size.height;
for (NSInteger i = 0; i < numRows; i++) {
contentInsetTop -= [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
if (contentInsetTop <= 0) {
contentInsetTop = 0;
break;
}
}
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0);
}
To reverse the order of elements:
dataSourceArray = dataSourceArray.reverseObjectEnumerator.allObjects;
Swift 4.2/5 version:
func updateTableContentInset() {
let numRows = self.tableView.numberOfRows(inSection: 0)
var contentInsetTop = self.tableView.bounds.size.height
for i in 0..<numRows {
let rowRect = self.tableView.rectForRow(at: IndexPath(item: i, section: 0))
contentInsetTop -= rowRect.size.height
if contentInsetTop <= 0 {
contentInsetTop = 0
break
}
}
self.tableView.contentInset = UIEdgeInsets(top: contentInsetTop,left: 0,bottom: 0,right: 0)
}
Swift 3/4.0 version:
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
first reverse uitableview
tableView.transform = CGAffineTransformMakeScale (1,-1);
then reverse cell in cell create.
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
...
cell.contentView.transform = CGAffineTransformMakeScale (1,-1);
Swift 4.0 and 4.2 version
First reverse UITableView in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
}
Then reverse the cell in cellForRowAt.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell else { fatalError() }
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
}
Here is a refined solution of KlimczakM´s solution that works with autolayouted tableview cells (as well as the fixed ones). This solution also works with sections, section headers and section footers.
Swift 3.0:
func updateTableContentInset(forTableView tv: UITableView) {
let numSections = tv.numberOfSections
var contentInsetTop = tv.bounds.size.height -
(self.navigationBar?.frame.size.height ?? 0)
for section in 0..<numSections {
let numRows = tv.numberOfRows(inSection: section)
let sectionHeaderHeight = tv.rectForHeader(inSection: section).size.height
let sectionFooterHeight = tv.rectForFooter(inSection: section).size.height
contentInsetTop -= sectionHeaderHeight + sectionFooterHeight
for i in 0..<numRows {
let rowHeight = tv.rectForRow(at: IndexPath(item: i, section: section)).size.height
contentInsetTop -= rowHeight
if contentInsetTop <= 0 {
contentInsetTop = 0
break
}
}
// Break outer loop as well if contentInsetTop == 0
if contentInsetTop == 0 {
break
}
}
tv.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
}
NOTE:
Above code is untested but should work.
Just make sure that you cope for the height of any navbar or tabbar and you'll be fine. In the code above i only do that for the navbar!
Don't bother to write the code by yourself.
Why don't you use ReverseExtension. Its very easy and will give you all required results. Please follow this url
https://github.com/marty-suzuki/ReverseExtension
Note: Whenever you need to add a new cell, please insert newly added model at zeroth index of datasource array, so new cell should add at bottom. Otherwise it would add the cell at top and you would get confused again.
The simple way is use like UILabel multiple lines + autolayout.
-> UITableView should resize it base on it content(aka intrinsic layout).
Create your tableview and set base class following:
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height)
}
}
Now set left right and bottom layout constraints for your tableview pin to it parent view.
The most important is top layout constraint should set to great than or equal 0, This condition guaranteed table will not tall than it parent view.
I did in cellForRowAt method:
let reverseIndex = myArray.count-indexPath.row-1
let currCellData = myArray.object(at: reverseIndex)
and then you continue working with currCellData
//Add these lines where you want to reload your tableView
let indexpath = IndexPath(row: self.Array.count-1, section: 0)
self.tableView.scrollToRow(at: indexpath, at: .top, animated: true)
self.updateTableContentInset()
//Add this function below
func updateTableContentInset() {
let numRows = self.tableView.numberOfRows(inSection: 0)
var contentInsetTop = self.tableView.bounds.size.height
for i in 0..<numRows {
let rowRect = self.tableView.rectForRow(at: IndexPath(item: i, section: 0))
contentInsetTop -= rowRect.size.height
if contentInsetTop <= 0 {
contentInsetTop = 0
break
}
}
self.tableView.contentInset = UIEdgeInsets(top: contentInsetTop,left: 0,bottom: 0,right: 0)
}
I you want that every new cell should appear from the bottom of the tableView, use this:-
Invert the tableView:
myTableView.transform = CGAffineTransform (scaleX: -1,y: -1)
Invert the cells also:
cell.contentView.transform = CGAffineTransform (scaleX: -1,y: -1)
Now populate your tabelViewDataSource in opposite direction, like if you are using an array, then you may do like this:
myTableViewData.insert(<Your New Array Element>), at: 0)
This solution adjust the content inset as the content size changes using KVO. It also takes the content inset into account when scrolling to top as simply scrolling to CGPointZero will scroll to the top of the content instead of scrolling to the top of the table.
-(void)startObservingContentSizeChanges
{
[self.tableView addObserver:self forKeyPath:kKeyPathContentSize options:0 context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:kKeyPathContentSize] && object == self.tableView)
{
// difference between content and table heights. +1 accounts for last row separator
CGFloat height = MAX(self.tableView.frame.size.height - self.tableView.contentSize.height, 0) + 1;
self.tableView.contentInset = UIEdgeInsetsMake(height, 0, 0, 0);
// "scroll" to top taking inset into account
[self.tableView setContentOffset:CGPointMake(0, -height) animated:NO];
}
}
Swift 3.01 - Other solution can be, rotate and flip the tableView.
self.tableView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(M_PI)))
self.tableView.transform = CGAffineTransform.init(translationX: -view.frame.width, y: view.frame.height)
UITableView anchor rows to bottom
If you have an array of object you display in cellForRowAtIndexPath:, lets say dataArray you can reverse it, or in cellForRowAtIndexPath:
you can do something like that:
NSString *yourObject = dataArray[[dataArray count] - 1 - indexPath.row];
cell.textLabel.text = yourObject
I assume you keep strings in dataArray.
Related
I downloaded this Pintrest-esc Custom Layout for my CollectionView from : https://www.raywenderlich.com/164608/uicollectionview-custom-layout-tutorial-pinterest-2 (modified it a little bit to do with cache code)
So the layout works fine, i got it set up so when you scroll to the bottom, i make another API call, create and populate additional cells.
Problem: If a User scrolls down and loads some more cells (lets say 20 for example) and then uses the "Search Feature" i have implemented at the top, it will then filter some of the data via price from MinRange - MaxRange leaving you with (lets say 5 results), thus deleting some cells and repopulating those. BUT - All that space and scrollable space that existed for the first 20 cells still exists as blank white space.... How can i remove this space ? and why can it resize when adding more cells, but not when removing them ?
Code of my API Call:
DispatchQueue.global(qos: .background).async {
WebserviceController.GetSearchPage(parameters: params) { (success, data) in
if success
{
if let products = data!["MESSAGE"] as? [[String : AnyObject]]
{
self.productList = products
}
}
DispatchQueue.main.async(execute: { () -> Void in
self.pintrestCollectionView.reloadData()
self.pintrestCollectionView.collectionViewLayout.invalidateLayout()
self.pintrestCollectionView.layoutSubviews()
})
}
}
Code Of Custom Layout Class & protocol:
protocol PinterestLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat
}
class PintrestLayout: UICollectionViewFlowLayout {
// 1
weak var delegate : PinterestLayoutDelegate!
// 2
fileprivate var numberOfColumns = 2
fileprivate var cellPadding: CGFloat = 10
// 3
fileprivate var cache = [UICollectionViewLayoutAttributes]()
// 4
fileprivate var contentHeight: CGFloat = 0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
// 5
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// 1
//You only calculate the layout attributes if cache is empty and the collection view exists.
/*guard cache.isEmpty == true, let collectionView = collectionView else {
return
}*/
cache.removeAll()
guard cache.isEmpty == true || cache.isEmpty == false, let collectionView = collectionView else {
return
}
// 2
//This declares and fills the xOffset array with the x-coordinate for every column based on the column widths. The yOffset array tracks the y-position for every column.
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0 ..< numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
// 3
//This loops through all the items in the first section, as this particular layout has only one section.
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
if collectionView.numberOfItems(inSection: 0) < 3
{
collectionView.isScrollEnabled = false
} else {
collectionView.isScrollEnabled = true
}
let indexPath = IndexPath(item: item, section: 0)
// 4
//This is where you perform the frame calculation. width is the previously calculated cellWidth, with the padding between cells removed.
let photoHeight = delegate.collectionView(collectionView, heightForPhotoAtIndexPath: indexPath)
let height = cellPadding * 2 + photoHeight
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: 7, dy: 4)
collectionView.contentInset = UIEdgeInsets.init(top: 15, left: 12, bottom: 0, right: 12)
// 5
//This creates an instance of UICollectionViewLayoutAttribute, sets its frame using insetFrame and appends the attributes to cache.
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6
//This expands contentHeight to account for the frame of the newly calculated item.
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
FYI: In MainStoryboard, i disabled "Adjust Scroll View Insets" as many of other threads and forums have suggested...
I'm guessing you've already found the answer, but I'm posting here for the next person since I ran into this issue.
The trick here is contentHeight actually never gets smaller.
contentHeight = max(contentHeight, frame.maxY)
This is a nondecreasing function.
To fix it, just set contentHeight to 0 if prepare is called:
guard cache.isEmpty == true || cache.isEmpty == false, let
collectionView = collectionView else {
return
}
contentHeight = 0
I have a UITableView with a lot of cells. On the cells that have a UITextField i scroll the table if the keyboard would hide the textfield. And to some cells i added a change of cell height to the scroll.
This works fine at the beginning and middle of the table but if the cell in question is at the bottom of the table I am getting some strange jumping around and vanishing of cells.
After the user has finished the editing the height of the cell is changed back to normal and the vanished cells reappear but the table is scrolled a bit upwards.
My question is now how can I get rid of this jumping and vanishing of cells?
Here is my code:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if fieldIndex != [-99, -99] {
if indexPath.section == fieldIndex[0] && indexPath.row == fieldIndex[1] {
fieldIndex = [-99, -99]
return 220.0
}
}
return 80.0
}
func keyboardWillBeShown(_ sender: Notification) {
let info: NSDictionary = (sender as NSNotification).userInfo! as NSDictionary
let value: NSValue = info.value(forKey: UIKeyboardFrameBeginUserInfoKey) as! NSValue
let keyboardSize: CGSize = value.cgRectValue.size
let height = toolBar.frame.height
let contentInset: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height + toolBar.frame.height + 60, 0.0)
originalIntersect = tableView.contentInset
tableView.contentInset = contentInset
tableView.scrollIndicatorInsets = contentInset
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardSize.height
}
func keyboardWillBeHidden(_ sender: Notification) {
let insets: UIEdgeInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0, 0, 0)
tableView.contentInset = originalIntersect
tableView.scrollIndicatorInsets = originalIntersect
}
If there is other code you need to see to help just ask.
Regards and Thanks
Adarkas
Hello I have working uicollectionview custom layout and i have issuesfor bottom padding and scrolling sizes. Under below picture you can see;
Also top side first cell always stays on top when scrolling ( Left side cell must be always stay right now no problem there , but top side cell must be scroll when scrolling )
I try to set in viewDidLoad under below codes but dont work
self.automaticallyAdjustsScrollViewInsets = false
self.collectionView.contentInset = UIEdgeInsetsMake(20, 20, 120, 100);
My custom collectionview layout file
import UIKit
public var CELL_HEIGHT = CGFloat(22)
protocol CustomCollectionViewDelegateLayout: NSObjectProtocol {
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, widthForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat
}
class CustomCollectionViewLayout: UICollectionViewLayout {
// Used for calculating each cells CGRect on screen.
// CGRect will define the Origin and Size of the cell.
let STATUS_BAR = UIApplication.sharedApplication().statusBarFrame.height
// Dictionary to hold the UICollectionViewLayoutAttributes for
// each cell. The layout attribtues will define the cell's size
// and position (x, y, and z index). I have found this process
// to be one of the heavier parts of the layout. I recommend
// holding onto this data after it has been calculated in either
// a dictionary or data store of some kind for a smooth performance.
var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
// Defines the size of the area the user can move around in
// within the collection view.
var contentSize = CGSize.zero
// Used to determine if a data source update has occured.
// Note: The data source would be responsible for updating
// this value if an update was performed.
var dataSourceDidUpdate = true
weak var delegate: CustomCollectionViewDelegateLayout?
override func collectionViewContentSize() -> CGSize {
return self.contentSize
}
override func prepareLayout() {
// Only update header cells.
if !dataSourceDidUpdate {
// Determine current content offsets.
let xOffset = collectionView!.contentOffset.x
let yOffset = collectionView!.contentOffset.y
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Confirm the section has items.
if collectionView?.numberOfItemsInSection(section) > 0 {
// Update all items in the first row.
if section == 0 {
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: item, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
// Also update x-position for corner cell.
if item == 0 {
frame.origin.x = xOffset
}
frame.origin.y = yOffset
attrs.frame = frame
}
}
// For all other sections, we only need to update
// the x-position for the fist item.
} else {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: 0, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
frame.origin.x = xOffset
attrs.frame = frame
}
}
}
}
}
// Do not run attribute generation code
// unless data source has been updated.
return
}
// Acknowledge data source change, and disable for next time.
dataSourceDidUpdate = false
var maxWidthInASection = CGFloat(0)
// Cycle through each section of the data source.
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Cycle through each item in the section.
if collectionView?.numberOfItemsInSection(section) > 0 {
var prevCellAttributes: UICollectionViewLayoutAttributes?
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
let cellIndex = NSIndexPath(forItem: item, inSection: section)
guard let width = delegate?.collectionView(collectionView!, layout: self, widthForItemAtIndexPath: cellIndex) else {
print("Please comform to CustomCollectionViewDelegateLayout protocol")
return
}
// Build the UICollectionVieLayoutAttributes for the cell.
var xPos = CGFloat(0)
let yPos = CGFloat(section) * CELL_HEIGHT
if let prevCellAttributes = prevCellAttributes {
xPos = CGRectGetMaxX(prevCellAttributes.frame)
}
let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex)
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: width, height: CELL_HEIGHT)
// Determine zIndex based on cell type.
if section == 0 && item == 0 {
cellAttributes.zIndex = 4
} else if section == 0 {
cellAttributes.zIndex = 3
} else if item == 0 {
cellAttributes.zIndex = 2
} else {
cellAttributes.zIndex = 1
}
// Save the attributes.
cellAttrsDictionary[cellIndex] = cellAttributes
prevCellAttributes = cellAttributes
let maxX = CGRectGetMaxX(cellAttributes.frame)
if maxWidthInASection < maxX {
maxWidthInASection = maxX
}
}
}
}
}
// Update content size.
let contentWidth = maxWidthInASection
let contentHeight = Double(collectionView!.numberOfSections())
self.contentSize = CGSize(width: Double(contentWidth), height: contentHeight)
self.contentSize.height = CGFloat(Double(collectionView!.numberOfSections()))
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create an array to hold all elements found in our current view.
var attributesInRect = [UICollectionViewLayoutAttributes]()
// Check each element to see if it should be returned.
for cellAttributes in cellAttrsDictionary.values.elements {
if CGRectIntersectsRect(rect, cellAttributes.frame) {
attributesInRect.append(cellAttributes)
}
}
// Return list of elements.
return attributesInRect
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary[indexPath]!
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
}
I try change collecionview properties in storyboard but doesnt works also i attached current collectionview object properties picture under below
Also i selected zoom 4 but zoom feature doesnt work ? why ? it must be work also zoom feature.
I know this is too late to answer, but this could be useful to other users.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
}
This what worked for me:
messagesCollectionView.contentInset.top = 100
messagesCollectionView.contentInset.bottom = 80
This can be also done on IB under section insets...
I am subclassing UICollectionViewFlowLayout in order to get two way scrolling in a UICollectionView. The scrolling works fine for smaller number of row and section count (100-200 rows and sections) but there is visible lag while scrolling when I increase row and section count over 500 i.e 250,000 or more cells in the UICollectionView. I have traced the source of the lag to be for in loop in the layoutAttributesForElementsInRect. I am using a Dictionary to hold UICollectionViewLayoutAttributes of each cell to avoid recalculating it and looping through it to return attributes of cells from layoutAttributesForElementsInRect
import UIKit
class LuckGameCollectionViewLayout: UICollectionViewFlowLayout {
// Used for calculating each cells CGRect on screen.
// CGRect will define the Origin and Size of the cell.
let CELL_HEIGHT = 70.0
let CELL_WIDTH = 70.0
// Dictionary to hold the UICollectionViewLayoutAttributes for
// each cell. The layout attribtues will define the cell's size
// and position (x, y, and z index). I have found this process
// to be one of the heavier parts of the layout. I recommend
// holding onto this data after it has been calculated in either
// a dictionary or data store of some kind for a smooth performance.
var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
// Defines the size of the area the user can move around in
// within the collection view.
var contentSize = CGSize.zero
override func collectionViewContentSize() -> CGSize {
return self.contentSize
}
override func prepareLayout() {
// Cycle through each section of the data source.
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Cycle through each item in the section.
if collectionView?.numberOfItemsInSection(section) > 0 {
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
// Build the UICollectionVieLayoutAttributes for the cell.
let cellIndex = NSIndexPath(forItem: item, inSection: section)
let xPos = Double(item) * CELL_WIDTH
let yPos = Double(section) * CELL_HEIGHT
let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex)
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)
// Save the attributes.
cellAttrsDictionary[cellIndex] = cellAttributes
}
}
}
}
// Update content size.
let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH
let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT
self.contentSize = CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create an array to hold all elements found in our current view.
var attributesInRect = [UICollectionViewLayoutAttributes]()
// Check each element to see if it should be returned.
for (_,cellAttributes) in cellAttrsDictionary {
if CGRectIntersectsRect(rect, cellAttributes.frame) {
attributesInRect.append(cellAttributes)
}
}
// Return list of elements.
return attributesInRect
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary[indexPath]!
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return false
}
}
Edit:
Following are the changes that I have come up with in the layoutAttributesForElementsInRect method.
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create an array to hold all elements found in our current view.
var attributesInRect = [UICollectionViewLayoutAttributes]()
let xOffSet = self.collectionView?.contentOffset.x
let yOffSet = self.collectionView?.contentOffset.y
let totalColumnCount = self.collectionView?.numberOfSections()
let totalRowCount = self.collectionView?.numberOfItemsInSection(0)
let startRow = Int(Double(xOffSet!)/CELL_WIDTH) - 10 //include 10 rows towards left
let endRow = Int(Double(xOffSet!)/CELL_WIDTH + Double(Utils.getScreenWidth())/CELL_WIDTH) + 10 //include 10 rows towards right
let startCol = Int(Double(yOffSet!)/CELL_HEIGHT) - 10 //include 10 rows towards top
let endCol = Int(Double(yOffSet!)/CELL_HEIGHT + Double(Utils.getScreenHeight())/CELL_HEIGHT) + 10 //include 10 rows towards bottom
for(var i = startRow ; i <= endRow; i = i + 1){
for (var j = startCol ; j <= endCol; j = j + 1){
if (i < 0 || i > (totalRowCount! - 1) || j < 0 || j > (totalColumnCount! - 1)){
continue
}
let indexPath: NSIndexPath = NSIndexPath(forRow: i, inSection: j)
attributesInRect.append(cellAttrsDictionary[indexPath]!)
}
}
// Return list of elements.
return attributesInRect
}
I have calculated the offset of the collectionView and used it to calculate the cells that will be visible on screen(using height/width of each cell). I had to add extra cells on each side so that when user scrolls there are no missing cells. I have tested this and the performance is fine.
By taking advantage of the layoutAttributesForElementsInRect(rect: CGRect) with your known cell size you don't need to cache your attributes and could just calculate them for a given rect as the collectionView requests them. You would still need to check for the boundary cases of 0 and the maximum section/row counts to avoid calculating unneeded or invalid attributes but that can be easily done in where clauses around the loops. Here's a working example that I've tested with 1000 sections x 1000 rows and it works just fine without lagging on the device:
Edit: I've added the biggerRect so that attributes can be pre-calculated for before the scrolling gets there. From your edit it looks like you're still caching the attributes which I don't think is needed for performance. Also it's going to lead to a much larger memory footprint with the more scrolling you do. Also is there a reason your don't want to use the supplied CGRect from the callback rather than manually calculate one from the contentOffset?
class LuckGameCollectionViewLayout: UICollectionViewFlowLayout {
let CELL_HEIGHT = 50.0
let CELL_WIDTH = 50.0
override func collectionViewContentSize() -> CGSize {
let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH
let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let biggerRect = rect.insetBy(dx: -2048, dy: -2048)
let startIndexY = Int(Double(biggerRect.origin.y) / CELL_HEIGHT)
let startIndexX = Int(Double(biggerRect.origin.x) / CELL_WIDTH)
let numberOfVisibleCellsInRectY = Int(Double(biggerRect.height) / CELL_HEIGHT) + startIndexY
let numberOfVisibleCellsInRectX = Int(Double(biggerRect.width) / CELL_WIDTH) + startIndexX
var attributes: [UICollectionViewLayoutAttributes] = []
for section in startIndexY..<numberOfVisibleCellsInRectY
where section >= 0 && section < self.collectionView!.numberOfSections() {
for item in startIndexX..<numberOfVisibleCellsInRectX
where item >= 0 && item < self.collectionView!.numberOfItemsInSection(section) {
let cellIndex = NSIndexPath(forItem: item, inSection: section)
if let attrs = self.layoutAttributesForItemAtIndexPath(cellIndex) {
attributes.append(attrs)
}
}
}
return attributes
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let xPos = Double(indexPath.row) * CELL_WIDTH
let yPos = Double(indexPath.section) * CELL_HEIGHT
let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)
return cellAttributes
}
}
You need to come up with a data structure for your cells which is ordered by dimension(s), in order to come up with some algorithm using those dimension(s) to narrow down the search range.
Let's take the case of a table with cells 100 pixels tall by the full width of the view, and 250_000 elements, asked for the cells intersecting { 0, top, 320, bottom }. Then your data structure would be an array ordered by top coordinate, and the accompanying algorithm would be like
let start: Int = top / 100
let end: Int = bottom / 100 + 1
return (start...end).map { cellAttributes[$0] }
Add as much complexity as your actual layout requires.
cellAttrsDictionary is inappropriate to store the UICollectionViewLayoutAttributes. It's indexed by NSIndexPath but in layoutAttributesForElementsInRect you are searching for an area. If you need cellAttrsDictionary for something else then you could leave it but store each cellAttribute additionally in another data structure which is faster to search.
For example a nested array:
var allCellAttributes = [[UICollectionViewLayoutAttributes]]()
The first level array describes an area, let's say it's in chunks of 1000 pixel high and full screen width. So all cellAttributes which intersect with the rectangle { 0, 0, screenwidth, 1000 } go into:
allCellAttributes[0].append(cellAttributes)
All cellAttributes which intersect with the rectangle { 0, 1000, screenwidth, 2000 } go into:
allCellAttributes[1].append(cellAttributes)
And so on...
Then, in layoutAttributesForElementsInRect you can search in this data-structure by jumping directly into the array depending on the given CGRect. If the rect is e.g. {0, 5500, 100, 6700} then you just need to search in this range:
allCellAttributes[5]
allCellAttributes[6]
This should give you the basic idea, I hope you understand what I mean.
I have a UICollectionView that shows several rows with one, full-width column (looks like a UITableView)
What I'd like to achieve is something similar to this:
... where the middle cell has a much greater height. As the user scrolls up and down, the cells before and after the middle cell animate back to the default height for the given cell.
Can somebody outline how I should approach this problem?
I use this Swift 3 code in my horizontal UICollectionView.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let centerX = scrollView.contentOffset.x + scrollView.frame.size.width/2
for cell in mainCollectionView.visibleCells {
var offsetX = centerX - cell.center.x
if offsetX < 0 {
offsetX *= -1
}
cell.transform = CGAffineTransform(scaleX: 1, y: 1)
if offsetX > 50 {
let offsetPercentage = (offsetX - 50) / view.bounds.width
var scaleX = 1-offsetPercentage
if scaleX < 0.8 {
scaleX = 0.8
}
cell.transform = CGAffineTransform(scaleX: scaleX, y: scaleX)
}
}
}
You need to create a custom subclass of UICollectionViewLayout.
First of all, override - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds to return yes, that way, you can change the layout attributes of your cells as your collection is being scrolled.
After that, your key methods to override are:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
and
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
I suggest reading an article about custom collection view layouts. It can be pretty heavy subject matter.
since the UICollectionView is the subclass of UIScrollView, you can solved this problem by treating your collectionView as a scrollView.
set the UIScrollViewDelegate and implemented scrollViewDidScroll, and then do something like this:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
for (int i = 0; i < [scrollView.subviews count]; i++) {
UIView *cell = [scrollView.subviews objectAtIndex:i];
float position = cell.center.y - scrollView.contentOffset.y;
float offset = 1.5 - (fabs(scrollView.center.y - position) * 1.0) / scrollView.center.y;
if (offset<1.0)
{
offset=1.0;
}
cell.transform = CGAffineTransformIdentity;
cell.transform = CGAffineTransformScale(cell.transform, offset, offset);
}
}
hope this will solve your problem.