I'm working on a project that needs something I never imagined to have. The app for iOS is directed to iPad due to size. To that question, I made a small prototype to show one of the parties to detail better.
This is a tableView where the functions and actions will happen.
And this is the Swift code:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data:[String] = ["Row 0","Row 1", "Row 2","Row 3","Row 4","Row 5","Row 6"]
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:Cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! Cell
cell.labelText.text = self.data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
cell!.alpha = 0.5
}
}
What the app to do exactly?
Well, when a row is selected, the rows below it need to stay with the Alpha equal to 0.5.
Examples:
I touched the row 3
Action:
Row 1, Row 2 and Row 3 will keep the Alpha equal to 1.0
Row 4, Row 5 and Row 6 will keep the Alpha equal to 0.5
I touched in row 4
Action:
Row 1, Row 2, Row 3 and Row 4 will keep the Alpha equal to 1.0
Row 5, Row 6 will keep the Alpha equal to 0.5
.
.
.
Can someone help me?
You'd want to set the alpha value in the cellForRowAtIndexPath, then simply reload that row when its tapped. This should preserve the alpha for that cell and set alpha to 1 on every other cell, even if the the user scrolls the cell offscreen.
var selectedIndexPath:NSIndexPath?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
if let selectedIndexPath = self.selectedIndexPath where indexPath.row == selectedIndexPath.row {
cell.alpha = 0.5
} else {
cell.alpha = 1
}
cell.labelText.text = self.data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
Make an integer variable "selectedCell", and in didSelectRowAtIndexPath, set it equal to indexPath.row, then tableView.reloadData()
In cellForRowAtIndexPath simply use an if statement to determine if indexPath.row is greater than "selectedCell". If so then set the alpha value to 0.5, otherwise set it to 1.0.
It is generally a good practice to change cell properties in cellForRowAtIndexPath, because every time it is called you risk overwriting changes you make to cells elsewhere in the code. Hope this helped.
Hi I am proposing below solution, please consider
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data:[String] = ["Row 0","Row 1", "Row 2","Row 3","Row 4","Row 5","Row 6"]
var rowSelected:Int
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:Cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! Cell
cell.labelText.text = self.data[indexPath.row]
if rowSelected <= indexPath.row
cell!.alpha = 0.5;
else
cell!.alpha = 1.0;
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
rowSelected = indexPath.row
tableView.reloadData()
}
}
I'm not sure if you are describing what you want it to do or the anormal behaviour.
There are two possibilities:
1) If you want the selection related alpha to render on a single row, you may want to override didDeselectRowAtIndexPath and set the alpha to 1.0 there.
2) You need to explicitly set the alpha after getting the cell from dequeueReusableCellWithIdentifier because you are not guaranteed to get a fresh cell instance every time.
The tableview will call cellForRowAtIndexPath for a variety of reasons even after showing it for the first time. What is probably happening is that cellForRowAtIndexPath is called for rows that have been deselected and dequeueReusableCellWithIdentifier may or may not reuse an existing cell object. When it reuses a cell object, properties are not reset.
I have a static UITableView which has 12 rows and for 11 rows I know what the height needs to be.
I have a UILabel which has dynamic text which sits inside the 12 row, how do i go about making just that one cell to have a dynamic height based on the text in the UILabel.
I have tried the below code inside viewDidLoad, but it didn't work. The rows remained the same height. I have also set the lines = 0 for the UILabel
tableView.estimatedRowHeight = 100.0
tableView.rowHeight = UITableViewAutomaticDimension
Have you tried this?
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if (indexPath.row < 11) {
// Everything starts at 0, so this covers 0-10
// Whatever your cell height is
return <#fixedCellHeight#>
} else {
// This is your 12th cell (indexPath.row 11), as we start counting at 0
return UITableViewAutomaticDimension
}
}
Adding to #Adrian answer , if you are using static Cells , changing one cell to dynamic height , and others as they are you can edit it to this .
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == 11 {
// This is your 12th cell (indexPath.row 11), as we start counting at 0
return UITableViewAutomaticDimension
} else {
return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
}
}
Try this:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
You can create 2 cell prototype cells for the table view. You give them 2 different id.
And then in your code you override this fonction
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if indexPath.row < 12 {
cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
} else {
cell = tableView.dequeueReusableCellWithIdentifier("cell12", forIndexPath: indexPath)
}
return cell
}
You Can create the dynamic cell with Text height then you can set the static and dynamic cell both in on table view
Here is link to dynamic cell with text How to change cell height dynamically in UITableView static cell
More elegant might be to just implement as suggested:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Or in Objective C:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
But rather than the row specific logic, add constraints to your static cell's content in the Storyboard to keep the row heights constant. That way if you move rows or change content you don't need to change any code.
I have a tableview inside my UIViewController to display comments. The height of this tableview depends on the number and the size of comments. The cells are dynamic.
I use autolayout, so my tableview has a height constraint. I set this constraint programmatically :
override func viewDidLayoutSubviews() {
self.heightCommentsTableView.constant = self.commentsTableView.contentSize.height + 50
}
It works if I display all the comments at once.
BUT, I would like display the comments 5 per 5, using this method : load more for UITableView in swift because of performance issue to display my view
(all my comments are loaded before pushing the view)
I noticed when I set my constraint, it calls cellForRowAtIndexPath for all the comments, not only the first 5.
I don't know how to do.
EDIT
var allCommentsArray: NSMutableArray = []
var elements: NSMutableArray = []
var range = 5
var currentPage = 0
var nextpage = 0
override func viewDidLoad() {
super.viewDidLoad()
allCommentsArray = NSMutableArray(array: comments)
elements.addObjectsFromArray(allCommentsArray.subarrayWithRange(NSMakeRange(0, range)))
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return comments.count
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
self.nextpage = self.elements.count - 5
if indexPath.row == nextpage {
self.currentPage++
self.nextpage = self.elements.count - 5
self.elements.addObjectsFromArray(self.allCommentsArray.subarrayWithRange(NSMakeRange(self.currentPage, self.range)))
tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CommentCustomTableViewCell", forIndexPath: indexPath) as! CommentCustomTableViewCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
The table view will call tableView(_:cellForRowAtIndexPath:) for every visible cell it is about to create, the amount of it will determine based on how many sections and how many cells in each section. You let the table vie know these amounts by implementing the tableView(_:numberOfRowsInSection:) and numberOfSectionsInTableView(_:) methods of UITableViewDataSource. So, if you want to control how many cells could possibly be created and visible at a given time, you'd have to manage that state in your data source by adding and removing according to whatever logic you desire. In the answer that you linked, you can see that he is called elements.addObjectsFromArray to progressively add more elements in batches.
I have UITabelViewController with custom MyUITableViewCell.
MyUITableViewCell class has #IBOutlet MyTextView:UITextView and method:
func getHeight() -> CGFloat
{
let height = (self.MyTextView?.contentSize)!.height
return height
}
I need set high for cell depend textView content size.
In UITabelViewController I set high for TableViewCell:
override func tableView(_tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
let cell = tableView(_tableView, cellForRowAtIndexPath: indexPath)
println(cell.getHeight())
return cell.getHeight()
}
But println show 20.0 for all cells on output, although I see that real cell high in table is different.
Simply check the UITableView's rowHeight method, like this:
tableView.rowHeight
implement heightForRowAtIndexPath:NSIndexPath
I feel like this might be a common issue and was wondering if there was any common solution to it.
Basically, my UITableView has dynamic cell heights for every cell. If I am not at the top of the UITableView and I tableView.reloadData(), scrolling up becomes jumpy.
I believe this is due to the fact that because I reloaded data, as I'm scrolling up, the UITableView is recalculating the height for each cell coming into visibility. How do I mitigate that, or how do I only reloadData from a certain IndexPath to the end of the UITableView?
Further, when I do manage to scroll all the way to the top, I can scroll back down and then up, no problem with no jumping. This is most likely because the UITableViewCell heights were already calculated.
To prevent jumping you should save heights of cells when they loads and give exact value in tableView:estimatedHeightForRowAtIndexPath:
Swift:
var cellHeights = [IndexPath: CGFloat]()
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? UITableView.automaticDimension
}
Objective C:
// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = #{}.mutableCopy;
// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;
// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cellHeightsDictionary setObject:#(cell.frame.size.height) forKey:indexPath];
}
// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
if (height) return height.doubleValue;
return UITableViewAutomaticDimension;
}
Swift 3 version of accepted answer.
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
The jump is because of a bad estimated height. The more the estimatedRowHeight differs from the actual height the more the table may jump when it is reloaded especially the further down it has been scrolled. This is because the table's estimated size radically differs from its actual size, forcing the table to adjust its content size and offset.
So the estimated height shouldn't be a random value but close to what you think the height is going to be. I have also experienced when i set UITableViewAutomaticDimension
if your cells are same type then
func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 100//close to your cell height
}
if you have variety of cells in different sections then I think the better place is
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
//return different sizes for different cells if you need to
return 100
}
#Igor answer is working fine in this case, Swift-4 code of it.
// declaration & initialization
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]
in following methods of UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// print("Cell height: \(cell.frame.size.height)")
self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = self.cellHeightsDictionary[indexPath] {
return height
}
return UITableView.automaticDimension
}
I have tried all the workarounds above, but nothing worked.
After spending hours and going through all the possible frustrations, figured out a way to fix this. This solution is a life savior! Worked like a charm!
Swift 4
let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)
I added it as an extension, to make the code look cleaner and avoid writing all these lines every time I want to reload.
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
beginUpdates()
endUpdates()
layer.removeAllAnimations()
setContentOffset(lastScrollOffset, animated: false)
}
}
finally ..
tableView.reloadWithoutAnimation()
OR you could actually add these line in your UITableViewCell awakeFromNib() method
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
and do normal reloadData()
I use more ways how to fix it:
For view controller:
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
as the extension for UITableView
extension UITableView {
func reloadSectionWithoutAnimation(section: Int) {
UIView.performWithoutAnimation {
let offset = self.contentOffset
self.reloadSections(IndexSet(integer: section), with: .none)
self.contentOffset = offset
}
}
}
The result is
tableView.reloadSectionWithoutAnimation(section: indexPath.section)
I ran into this today and observed:
It's iOS 8 only, indeed.
Overridding cellForRowAtIndexPath doesn't help.
The fix was actually pretty simple:
Override estimatedHeightForRowAtIndexPath and make sure it returns the correct values.
With this, all weird jittering and jumping around in my UITableViews has stopped.
NOTE: I actually know the size of my cells. There are only two possible values. If your cells are truly variable-sized, then you might want to cache the cell.bounds.size.height from tableView:willDisplayCell:forRowAtIndexPath:
You can in fact reload only certain rows by using reloadRowsAtIndexPaths, ex:
tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)
But, in general, you could also animate table cell height changes like so:
tableView.beginUpdates()
tableView.endUpdates()
Overriding the estimatedHeightForRowAtIndexPath method with an high value, for example 300f
This should fix the problem :)
Here's a bit shorter version:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}
There is a bug which I believe was introduced in iOS11.
That is when you do a reload the tableView contentOffSet gets unexpectedly altered. In fact contentOffset should not change after a reload. It tends to happen due to miscalculations of UITableViewAutomaticDimension
You have to save your contentOffSet and set it back to your saved value after your reload is finished.
func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){
DispatchQueue.main.async { [weak self] () in
self?.tableView.reloadData()
self?.tableView.layoutIfNeeded()
self?.tableView.contentOffset = offset
}
}
How you use it?
someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)
This answer was derived from here
This one worked for me in Swift4:
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
reloadData()
layoutIfNeeded()
setContentOffset(lastScrollOffset, animated: false)
}
}
One of the approach to solve this problem that I found is
CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()
None of these solutions worked for me. Here's what I did with Swift 4 & Xcode 10.1...
In viewDidLoad(), declare table dynamic row height and create correct constraints in cells...
tableView.rowHeight = UITableView.automaticDimension
Also in viewDidLoad(), register all your tableView cell nibs to tableview like this:
tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")
In tableView heightForRowAt, return height equal to each cell's height at indexPath.row...
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
return cell.layer.frame.height
} else if indexPath.row == 1 {
let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
return cell.layer.frame.height
} else {
let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
return cell.layer.frame.height
}
}
Now give an estimated row height for each cell in tableView estimatedHeightForRowAt. Be accurate as you can...
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return 400 // or whatever YourTableViewCell's height is
} else if indexPath.row == 1 {
return 231 // or whatever YourSecondTableViewCell's height is
} else {
return 216 // or whatever YourThirdTableViewCell's height is
}
}
That should work...
I didn't need to save and set contentOffset when calling tableView.reloadData()
I have 2 different cell heights.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
After I added estimatedHeightForRowAt, there was no more jumping.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
For me the working solution is
UIView.setAnimationsEnabled(false)
tableView.performBatchUpdates { [weak self] in
self?.tableView.reloadRows(at: [indexPath], with: .none)
} completion: { [weak self] _ in
UIView.setAnimationsEnabled(true)
self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) // remove if you don't need to scroll
}
I have expandable cells.
Try to call cell.layoutSubviews() before returning cell in func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. It's known bug in iOS8.
You can use the following in ViewDidLoad()
tableView.estimatedRowHeight = 0 // if have just tableViewCells <br/>
// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0
I had this jumping behavior and I initially was able to mitigate it by setting the exact estimated header height (because I only had 1 possible header view), however the jumps then started to happen inside the headers specifically, not affecting the whole table anymore.
Following the answers here, I had the clue that it was related to animations, so I found that the table view was inside a stack view, and sometimes we'd call stackView.layoutIfNeeded() inside an animation block. My final solution was to make sure this call doesn't happen unless "really" needed, because layout "if needed" had visual behaviors in that context even when "not needed".
I had the same issue. I had pagination and reloading data without animation but it did not help the scroll to prevent jumping. I have different size of IPhones, the scroll was not jumpy on iphone8 but it was jumpy on iphone7+
I applied following changes on viewDidLoad function:
self.myTableView.estimatedRowHeight = 0.0
self.myTableView.estimatedSectionFooterHeight = 0
self.myTableView.estimatedSectionHeaderHeight = 0
and my problem solved. I hope it helps you too.
For me, it worked with "heightForRowAt"
extension APICallURLSessionViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
print("Inside heightForRowAt")
return 130.50
}
}
Actually I found if you use reloadRows causing a jump problem. Then you should try to use reloadSections like this:
UIView.performWithoutAnimation {
tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}