I have a UITableView that is performing an animation on scroll, basically a sticky header, but instead it adjusts the height constraint of a UIView. I am getting really high CPU usage on scroll. Is there a better way to perform this animation?
Edit: I am not doing anything in the cellForRow at, function that would be causing this. It is definitely the scrollViewDidScroll function.
DidScroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y + tableViewContentInsets
if (lastPoint != nil) {
if (lastPoint! < offset) {
if (portfolioSummaryInitialHeight - offset < portfolioSummaryInitialHeight){
if (portfolioSummaryInitialHeight - offset > 0 ){
portfolioSummaryHeightConstraint.constant = portfolioSummaryInitialHeight - offset
} else {
portfolioSummaryHeightConstraint.constant = 0
}
}
} else {
if (portfolioSummaryInitialHeight - offset < portfolioSummaryInitialHeight){
if (portfolioSummaryInitialHeight - offset > 0 ){
portfolioSummaryHeightConstraint.constant = portfolioSummaryInitialHeight - offset
}
} else {
portfolioSummaryHeightConstraint.constant = portfolioSummaryInitialHeight
}
}
}
lastPoint = offset
}
Additional Variables:
var tableViewContentInsets: CGFloat = 80
var portfolioSummaryInitialHeight: CGFloat = 0 // equals portfolioSummaryInitialHeight at viewDidLoad
var lastPoint: CGFloat?
first of all, I'd advise you to check if you perform any other heavy operations on the main thread e.g data fetch or something similar. I've created a simple test project with a plain table view and a header. Here is the code which does the resizes header. According to the CPU report, it utilizes 8% of CPU in high peaks.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var headerHeightConstr: NSLayoutConstraint! //header's height constraint
#IBOutlet weak var tableView: UITableView!
private var headerHeight: CGFloat = 128.0 ////header's height constraint initial value
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "reuseID") else {
return UITableViewCell(style: .default, reuseIdentifier: "reuseID")
}
return cell
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
self.headerHeightConstr.constant += abs(scrollView.contentOffset.y)
} else if scrollView.contentOffset.y > 0 && self.headerHeightConstr.constant >= headerHeight {
self.headerHeightConstr.constant -= scrollView.contentOffset.y
if self.headerHeightConstr.constant < headerHeight {
self.headerHeightConstr.constant = headerHeight
}
}
}
}
This is kinda naive approach but it works, will try to answer your questions if needed.
Check your WillLayoutSubViews and DidLayoutSubViews functions. If you are doing lots of work in those functions then you need to add something like this:
var isScrolling: Bool = false
override func viewDidLayoutSubviews() {
if (isScrolling) {
return
} else {
super.viewDidLayoutSubviews()
}
}
While your tableView is scrolling set isScrolling = true in scrollViewDidEndDragging set isScrolling = false this will ensure that your class will not do excessive work while the tableView is scrolling but insure that it will do the work in other instances. Might not be the best way to do this but it certainly works.
Related
I have a question about UITableView.
I want to let the tableView height according to my cells content height.
So, I use the following code successfully.
DispatchQueue.main.async {
var frame = self.tableView.frame
frame.size.height = self.tableView.contentSize.height
self.tableView.frame = frame
}
But when I have much data to show, my contents will out of screen.
And the tableView also out of screen.
Have any ideas to set it's constrain and don't make it out of screen.
I want to set 15 between the tableView bottom layout and superView bottom layout.
I use SnapKit to set autolayout.
//I want to set this one is the biggest frame size. Other contents I can scroll the tableView to show the data.
tableView.snp.makeConstraints { (make) in
make.top.equalTo(self.topLayoutGuide.snp.bottom)
make.left.equalTo(10)
make.right.equalTo(-10)
make.bottom.equalTo(-15)
}
You could create a custom UITableView :
class AutomaticHeightTableView: UITableView {
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height + 20)
}
}
And then set your UITableView Class to AutomaticHeightTableView.
This solution is inspired from an answer found on stackoverflow.
I have solved a similar problem using the following method. You only need few lines of code.
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
private func setupTableView() {
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
}
Simply set an estimated row height for the UITableView and then set the rowHeight as UITableViewAutomaticDimension.
Maybe you can limit the height of the table's frame, making sure is not longer than its superView, something like this modifying your code:
DispatchQueue.main.async {
if let superViewHeight = self.tableView.superView?.bounds.maxY {
let maxHeight = superViewHeight - self.tableView.frame.minY
var frame = self.tableView.frame
frame.size.height = min(self.tableView.contentSize.height, maxHeight)
self.tableView.frame = frame
}
}
Code Work :
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tblHeightConstraint: NSLayoutConstraint! // tableView Height Constraint
#IBOutlet weak var tblView: UITableView!
var tblMaxHeight : CGFloat = 50
override func viewDidLoad() {
super.viewDidLoad()
let navHeight = (self.navigationController?.navigationBar.frame.size.height)! + UIApplication.shared.statusBarFrame.size.height
tblMaxHeight = self.view.frame.size.height - 40 - navHeight
}
override func viewDidLayoutSubviews(){
tblHeightConstraint.constant = min(tblMaxHeight, tblView.contentSize.height)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 24
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = "Row: #\(indexPath.row)"
return cell!
}
}
Constraints to tableView :
Output :
Put your UITableView inside a UIScrollView and add constraints like this:
Constraints to Table View
Create an IBOutlet of the height constraint and then override viewWillLayoutSubviews in your UIViewController
override func viewWillLayoutSubviews() {
super.updateViewConstraints()
self.tableViewHeightConstraint.constant = tableView.contentSize.height
}
Have you tried
self.tableView.invalidateIntrinsicContentSize()
https://developer.apple.com/documentation/uikit/uiview/1622457-invalidateintrinsiccontentsize
I am using pagination to show manage multiple data. Pagination works on both sides top and bottom. For this, I am using below code to call API. But I have faced the issue when data is not greater then tableView height. In this case scrollViewDidEndDragging method not called. So please tell me how to solve this problem. below code is working fine when data is greater then tableView height.
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if(scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0) {
print("up")
if workerInfo.count > 0 {
let topVisibleIndexPath:IndexPath = self.tableView.indexPathsForVisibleRows![0]
if topVisibleIndexPath.row == 0 && startCount != 0 && !isDataLoading {
isDataLoading = true
startCount = startCount - requiredCount
self.callAPI(isCallFromPagination: true)
}
}
}
else {
print("down")
if workerInfo.count > 0 {
let arrayOfVisibleItems = tableView.indexPathsForVisibleRows?.sorted()
let lastIndexPath = arrayOfVisibleItems!.last
// print("Array: ", arrayOfVisibleItems)
print("Last IndexPath: ", lastIndexPath as Any)
if lastIndexPath?.row == workerInfo.count - 1 && !isDataLoading {
isDataLoading = true
startCount = startCount + requiredCount
self.callAPI(isCallFromPagination: true)
}
}
}
}
Can you please check this properties.In my code its works perfectly.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
print("Called")
}
}
// Support Pagination
extension TableViewController: UIScrollViewDelegate {
// Set Pagination Trigger before DataSoruce remaining 6 items display
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Load More
setupPaginationAt(indexPath)
}
// If we reached at the end, check again if anything to load
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
let bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height
if (bottomEdge >= scrollView.contentSize.height) {
// We are at the end
setupPaginationAt(nil)
}
}
func setupPaginationAt(_ indexPath: IndexPath?) {
// If any network calls on the way, we must leave - Declare a Bool variable and manipulate the value of it
if isLoading {
return
}
// Validation
guard let _dataSource = YourDataSource else {
print("DataSource found empty")
return
}
// Config Pagination Call
func execute() {
// Get Current Pagination - Hope you have page index's from API
if let currentPage = dataSource.pageIndex, let lastPage = dataSource.totalPages {
if currentPage < lastPage {
let nextPage = currentPage + 1
loadData(at: nextPage)
}
}
}
// Check InBetween or End
if let _indexPath = indexPath {
if _indexPath.row == _dataSource.count - 6 {
execute()
}
} else {
// Assume End
execute()
}
}
}
This solution should work.
You can keep your table view bounce and scrollview bounce True. then scrollViewDidEndDragging method called
I'm using swift4,I have outlet for table view and table view height:
#IBOutlet var commentsTableView: UITableView!
#IBOutlet var commentTableHeight: NSLayoutConstraint!
the table return dynamic height for each cell
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
I want to change table view height when get data from API:
//// Consum APIs
func getProductDetails() {
API.getProductDetails(category_id: String(self.passed_category_id)) {( error: Error?, success: Bool, products: ProductDetails) in
if success {
self.product = products
self.setProductDetailsValue()
} else {
}
}
}
func setProductDetailsValue() {
if(self.product.comments.count > 0) {
self.commentsTableView.isHidden = false
self.commentsTableView.reloadData()
self.commentsTableView.layoutIfNeeded()
self.commentTableHeight.constant = self.getTableViewHeight(tableView: self.commentsTableView)
print("comment table height: ", self.commentTableHeight.constant, self.getTableViewHeight(tableView: self.commentsTableView))
} else {
self.commentsTableView.isHidden = true
}
}
func getTableViewHeight(tableView: UITableView)-> CGFloat {
tableView.layoutIfNeeded()
return tableView.contentSize.height
}
the print statement prints values like:
comment table height: 352.0 624.333324432373
which mean that the self.commentTableHeight.constant take different value. I find that every time the app give 44 pt to every cell, but I don't know what is the problem with my code.
any help?
You have to re-layout the view after changing the constant
self.commentTableHeight.constant = self.getTableViewHeight(tableView: self.commentsTableView)
self.view.layoutIfNeeded()
print("comment table height: ", self.commentTableHeight.constant, self.getTableViewHeight(tableView: self.commentsTableView))
//
you have to put this in viewDidLoad
tableView.estimatedRowHeight = 200; // your estimated height
tableView.rowHeight = UITableViewAutomaticDimension;
I have a UITableView with automatic sizing cells. The contents of each cell revolve around the counting of time, so the size of the cell could change every second. I am currently using a timer scheduled every 1 second to tell the table view to update the cell sizes:
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timeUpdateNotification), userInfo: nil, repeats: true)
#objc func timeUpdateNotification() {
// ... cells get notified to update contents here ...
tableView.beginUpdates()
tableView.endUpdates()
}
This works fairly well but has issues when the user taps to scroll to the top of the list. The animation is a bit janky and often times doesn't make it to the top. Is there a better way to handle this?
If you set a flag when the table is scrolling you can detect it in your timer function and not update while it is set. UITableView is a descendant of UIScrollView, so you can use some of the scroll view delegates to do this. If you override scrollViewShouldScrollToTop() and scrollViewDidScrollToTop() you will know when the scroll view is scrolling to the top and when it has finished.
override func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
NSLog("scrollViewShouldScrollToTop")
isScrolling = true
return true
}
override func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
NSLog("scrollViewDidScrollToTop")
isScrolling = false
}
You could also extend this to detect when the user is dragging/scrolling the view, to prevent the timer function from updating at these occasions too.
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
NSLog("scrollViewWillBeginDragging")
isScrolling = true
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if (!decelerate) {
// Only catch if scrolling stopped
NSLog("scrollViewDidEndDragging")
isScrolling = false
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
NSLog("scrollViewDidEndDecelerating")
isScrolling = false
}
I have added some logging to the functions, so that you can see what is going on. You can remove these, of course.
Invalidate the timer when they click to scroll to top, then as soon as it reaches top start it again.
Edit: That way, it won't update content which may move content higher without updating the "top" of the table view.
I had a similar problem.
I solved it with the help of a manual height calculation for each cell.
var cellHeights: [Int: CGFloat] = [:]
In the cellForRowAtIndexPath method, calculate the height:
cellHeights[byRow] = cell.getCellHeight()
In the cell itself
func getCellHeight() -> CGFloat {
let userInfoHeight: CGFloat = userInfoHeightConstraint.constant
let actionHeight: CGFloat = actionViewHeightConstraint.constant
let descriptionBottom: CGFloat = descriptionBottomConstraint.constant
let descriptionWidth = self.frame.width - leftAvatarConstraint.constant - descriptionRightConstraint.constant
let descriptionHeight: CGFloat = descriptionLabel.textHeight(width: descriptionWidth)
let height = userInfoHeight + actionHeight + descriptionBottom + descriptionHeight
return height
}
extension UILabel {
func textHeight(width: CGFloat) -> CGFloat {
var textHeight: CGFloat = 0
if let text = self.text {
let customLabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude))
customLabel.numberOfLines = self.numberOfLines
customLabel.text = text
customLabel.font = self.font
customLabel.sizeToFit()
textHeight = customLabel.frame.height
}
return textHeight
}}
I'm using UITableViewAutomaticDimension.
I additionally have the functionality to change the size of the cell at various clicked on it.
Therefore, in my task, this works fine:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return presenter.cellHeights[indexPath.row] ?? 0
}
But I think that you can immediately set the height:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return presenter.cellHeights[indexPath.row] ?? 0
}
I am using Swift 3. I have expandable table view cells but is it possible to get a different row height depending on which cell was clicked? For example, if the first cell is clicked, I want it to return 420 for height and if other cells are clicked, I want it to return 300 for height.
Here is my cell class.
class ResultsCell: UITableViewCell {
#IBOutlet weak var introPara : UITextView!
#IBOutlet weak var section_heading : UILabel!
class var expandedHeight : CGFloat = { get { return 420.0 } }
class var defaultHeight : CGFloat { get { return 44.0 } }
var frameAdded = false
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
section_heading.translatesAutoresizingMaskIntoConstraints = false
}
func checkHeight() {
introPara.isHidden = (frame.size.height < ResultsCell.expandedHeight)
}
func watchFrameChanges() {
if(!frameAdded) {
addObserver(self, forKeyPath: "frame", options: .new, context: nil)
checkHeight()
}
}
func ignoreFrameChanges() {
if(frameAdded){
removeObserver(self, forKeyPath: "frame")
}
}
deinit {
print("deinit called");
ignoreFrameChanges()
}
// when our frame changes, check if the frame height is appropriate and make it smaller or bigger depending
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "frame" {
checkHeight()
}
}
}
What I have tried is something like this.
var _expandedHeight : CGFloat = 420.0
class var expandedHeight : CGFloat { get { return _expandedHeight } set (newHeight) { _expandedHeight = newHeight } }
var isRow0 = false
override func awakeFromNib() {
super.awakeFromNib()
section_heading.translatesAutoresizingMaskIntoConstraints = false
if isRow0 {
ResultsCell.expandedHeight = 300.0
}
else {
ResultsCell.expandedHeight = 420.0
}
}
And then in the TableViewController...
// return the actual view for the cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let resultcell = tableView.dequeueReusableCell(withIdentifier: "resultCellTemplate", for: indexPath) as! ResultsCell
if indexPath.row == 0 {
resultcell.isRow0 = true
} else {
resultcell.isRow0 = false
}
return resultcell
}
But I'm getting errors on the getter/setter line: instance member _expandedHeight cannot be used on type ResultsCell
How can I achieve the behavior that I want?
Use tableview update methods to reevaluate each cell's height
self.tableView.beginUpdates()
self.tableView.endUpdates()
Changes to the tableview cells' heights will automatically be calculated with heightForRowAtIndexPath
try setting your height w.r.t indexpath
make user you mention the heightForRowAtIndexPath to calculate the right height.
You should do this in the delegate of your TableView.
Your TableView has a delegate private instance variable, whose value should implement protocol UITableViewDelegate. This protocol contains many functions which the UITableView calls in order to set its own appearance.
For more info:
The UITableViewDelegate protocol:
https://developer.apple.com/reference/uikit/uitableviewdelegate
The UITableView delegate instance variable:
https://developer.apple.com/reference/uikit/uitableview#delegate
What you want to do is to implement the tableView(_:heightForRowAt:) method on your UITableViewDelegate object. This method gets two parameters: The first is the UITableView itself, and the second is the IndexPath of the row being prepared for rendering. An IndexPath is an object which has section and row properties corresponding to the cell being rendered.
What you want to do is have some way of computing whether a cell is expanded or not based on its IndexPath.
Swift IndexPath reference: https://developer.apple.com/reference/foundation/indexpath
Trying to do this based on the UITableViewCell itself is an incorrect approach, as you usually should not analyze the cell before it is rendered.
A way to do this would be to have your UITableView-inheriting class keep a (possibly nested) array of booleans indicating which cells should be expanded or not. Then you could use the IndexPath's row and possibly section as indices into this array in order to determine whether the cell is expanded or not. Something like this:
class MyTableView: UITableView, UITableViewDelegate {
/* ... */
private var expanded: [Bool] = []
init() {
super.init()
self.delegate = self
}
func setData(data: [SomeType]) {
//set the data for the table here
expanded = [Bool](repeating: false, count: data.count)
}
/* ...logic to expand and collapse cells... */
func tableView(_ view: UITableView,
heightForRowAt indexPath: IndexPath) {
if expanded[indexPath.row] {
return expandedHeight
} else {
return collapsedHeight
}
}
/* ... */
}
The proper way to set your data would be to call a method on your UITableViewDataSource, but I'm assuming you'd set data on the TableView itself for the sake of simplicity.
Don't try to set the UITableView's properties by hand. The delegate and dataSource-based workflow of UITableViews is set up so that your table view works efficiently and only updates itself when it needs to.