UITableView performance problem with HeaderCell - swift

I have a performance issue when HeaderCell becomes visible, it takes up to 40% of CPU on iPhone X and I see lags. This UITableView displays static data (TV guide). I see lags every time when I scroll up and down, "tableView.dequeueReusableCell()" doesn't help. If I use my ExpandableHeaderView() to create HeaderCell for supporting tapping and expanding section, I have the same issue. If expand section and scrolls inside elements, I do not see lags. Performance problem occurs only when HeaderCell appears on the screen. When user opens View Controller, he sees 3 programs for each channel (current and next two), if he taps on section name, he sees all programs for section (channel). HeaderCell contains only Label. I display the same data in the same way on my Android app and it works without any problems on simple Android device.
func numberOfSections(in tableView: UITableView) -> Int {
return Channels.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Channels[section].Programs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier)! as! TVGuideTableViewCell
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
let text = Channels[indexPath.section].Programs[indexPath.row].Name
let dateString = dateFormatter.string(from:Channels[indexPath.section].Programs[indexPath.row].StartDate!)
cell.label.text = text
cell.timeLabel.text = dateString
if (Channels[indexPath.section].Programs[indexPath.row].EndDate! < currentDate) {
cell.label.textColor = UIColor.gray
} else {
cell.label.textColor = UIColor.white
}
if (Channels[indexPath.section].Programs[indexPath.row].StartDate! < currentDate) {
cell.bell.isHidden = true
} else {
cell.bell.isHidden = false
}
if (Channels[indexPath.section].Programs[indexPath.row].EndDate! < currentDate) {
Channels[indexPath.section].Programs[indexPath.row].expandedRow = false
} else {
if Channels[indexPath.section].expandedCount < 3 {
Channels[indexPath.section].expandedCount += 1
Channels[indexPath.section].Programs[indexPath.row].expandedRow = true
}
}
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifierHeader) as! CustomHeaderTableViewCell
headerCell.headerLabel.text = Channels[section].Name
return headerCell
// let header = ExpandableHeaderView()
// header.customInit(title: Channels[section].Name, section: section, delegate: self as ExpandableHeaderViewDelegate)
// return header
}
func toggleSection(header: ExpandableHeaderView, section: Int) {
Channels[section].expanded = !Channels[section].expanded
Channels[section].expandedCount = 0
tableView.beginUpdates()
for i in 0 ..< Channels[section].Programs.count {
tableView.reloadRows(at: [IndexPath(row: i, section: section)], with: .automatic)
}
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 38
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if(Channels[indexPath.section].expanded) {
return 38
} else {
if (Channels[indexPath.section].Programs[indexPath.row].EndDate! < currentDate) {
return 0
} else {
if Channels[indexPath.section].Programs[indexPath.row].expandedRow {
return 38.0
} else {
return 0.0
}
}
}
}
class CustomHeaderTableViewCell: UITableViewCell {
#IBOutlet weak var headerLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}

From your code, what I can see that the problem is not HeaderCell, the problem is DateFormatter object. Because everytime you scroll and cell appears a new object is created and just imagine that if there are 10 - 15 cells at a time then 10 - 15 reused / created and as you scroll or reloadData it again create a new object.
So, my advise is to create the DateFormatter object outside cellforRow and then just use it to get to get a dateString by calling it.
This is just one assumption, try this and project still appears do let me know!

Related

Disable users to scroll up in tableView?

Similar to tinder, if a user goes down, I don't want them to be able to go back upwards. How do I do this?
I tried the code below, but it keeps running an error: Index out of range.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! FeedTableViewCell
cell.collectionView.delegate = self
cell.collectionView.dataSource = self
//reloading the tableview
let artist: UserModel
artist = self.userList[indexPath.row]
cell.nameLabel.text = artist.name
cell.passionLabel.text = artist.passion
cell.collectionView.reloadData()
if indexPath.row == 1 {
userList.removeFirst()
}
return cell
}
Refer below code,
You need to store last scroll Content Offset of tableview, when user scroll down, refer scrollViewDidScroll method.
#IBOutlet weak var tableView: UITableView!
var lastContentOffsetY:CGFloat = 0.0
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.bounces = false
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuse")
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuse")
return cell!
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(scrollView.contentOffset.y < self.lastContentOffsetY){
self.tableView.setContentOffset(CGPoint(x:scrollView.contentOffset.x, y: self.lastContentOffsetY), animated: false)
}else{
self.lastContentOffsetY = scrollView.contentOffset.y
}
}

Swift UITableView numberOfRows Error When Added or Removing Rows in Section

I've been experiencing a very annoying issue this week. I have a UITableView that generates a number of sections (with section headers), each section always having 3 rows.
I've implemented a hiding/showing section functionality when a section header is tapped, where all the rows in that section are either deleted or inserted accordingly. Everything works perfectly fine and as expected... 70% of the time. I find that once I start dragging around the UITableView a bit while tapping on headers and just being all around random, it crashes unexpectedly with the error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (3) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (3 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Below is my relevant code:
#IBOutlet var pastWinnersTableView: UITableView!
var previousWeeks : [PreviousWeekData] = []
var hiddenSections = Set<Int>()
var firstTimeLoading = true ///This is needed to allow all the past week cells to start off as minimized
//This function runs in response to a section header being tapped in the table view.
#objc func expandCollapseSection(section: UIButton){
pastWinnersTableView.isUserInteractionEnabled = false
func indexPathsForSection() -> [IndexPath] {
var indexPaths = [IndexPath]()
for row in 0..<3 {
indexPaths.append(IndexPath(row: row, section: section.tag))
}
return indexPaths
}
//If the section was previously hidden...
if hiddenSections.contains(section.tag) {
hiddenSections.remove(section.tag)
pastWinnersTableView.insertRows(at: indexPathsForSection(), with: .fade)
//If the section was previously showing...
} else {
hiddenSections.insert(section.tag)
pastWinnersTableView.deleteRows(at: indexPathsForSection(), with: .fade)
}
}
//MARK: - REQUIRED METHODS
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if firstTimeLoading == true || self.hiddenSections.contains(section) { return 0 }
else { return 3 }
}
func numberOfSections(in tableView: UITableView) -> Int {
let numberOfPreviousWeeks = previousWeeks.count
if numberOfPreviousWeeks > 0 {return numberOfPreviousWeeks} else {return 0}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let weekNumber = previousWeeks[section].weekNumber
let headerButton = UIButton(type: .custom)
headerButton.layer.borderWidth = 1
headerButton.layer.borderColor = UIColor.black.cgColor
headerButton.layer.cornerRadius = 10
headerButton.layer.masksToBounds = true
headerButton.titleLabel?.font = UIFont(name: "Mouse Memoirs", size: 30)
headerButton.backgroundColor = task.hexStringToUIColor(hex: "5B3F22")
headerButton.setTitleColor(.white, for: .normal)
headerButton.titleLabel?.shadowColor = .black
headerButton.titleLabel?.shadowOffset = CGSize(width: 1, height: 1)
headerButton.addTarget(self, action: #selector(expandCollapseSection), for: .touchUpInside)
headerButton.tag = section
hiddenSections.insert(section)
headerButton.setTitle("Week \(String(weekNumber))", for: .normal)
return headerButton
}
//This footer is essentially just a small blank space.
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footer = tableView.dequeueReusableCell(withIdentifier: "pastWinnersWeekFooterCell") as! PastWinnersWeekFooterCell
return footer
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 45 }
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { return 5 }
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension }
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { return 44 }
//PreviousWeekData[(weekNumber, firstPlacePlayers[(username, score)], secondPlacePlayers[(username, score)], thirdPlacePlayers[(username, score)]), (weekNumber,...)]
//This defines the tableViewCell that holds all 3 placement sections with all the winners for a given week
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let weekSection = indexPath.section
let weekData = previousWeeks[weekSection]
let cell = tableView.dequeueReusableCell(withIdentifier: "pastWinnersTableViewCell", for: indexPath) as! PastWinnersTableViewCell
cell.thisWeeksData = weekData
cell.winnersCollectionView.tag = indexPath.row
cell.frame = tableView.bounds;
cell.layoutIfNeeded()
cell.winnersCollectionView.reloadData()
cell.collectionViewHeight.constant = cell.winnersCollectionView.collectionViewLayout.collectionViewContentSize.height;
return cell
}
Any ideas why it would be crashing on me with that error only sometimes? (ie. once I start dragging it around a bit and THEN tapping on headers? Tapping on my section headers is always a 3 in, 3 out deal so I'm not sure why it's getting confused.
Thanks for the help!

Grouped UITableView with Shadow and Corner Radius?

I was wondering how one might go about creating a grouped UITableView with sections that have a drop shadow and rounded corners. Also, the section header is selectable, so when the user clicks the header the cells in that section should collapse; therefore, the shadow and rounded corners will need to change to the new size of the section, just the header. Does anybody know how to do what I am talking about? I have attached a UI mockup for reference as well as some code.
Here is a UI Mockup of what I wish to design:
This is the code I have to set up the tableView:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
return nil
} else if settings[section - 1].isEmpty == false {
let headerView = Bundle.main.loadNibNamed("HeaderTableViewCell", owner: self, options: nil)?.first as! HeaderTableViewCell
let settingCategories = ["Account", "Notifications", "Personalized Feed", "Color Theme"]
headerView.settingCategoryTitle.text = settingCategories[section - 1]
let totalCellHeight = CGFloat(settings[section - 1].count * 63)
let height = headerView.frame.height + totalCellHeight
print("Needed Height: \(height)")
headerView.clipsToBounds = false
headerView.headerBackground.frame.size.height = height
print("HeaderView Height: \(headerView.headerBackground.frame.size.height)")
headerView.headerBackground.layer.shadowRadius = 10
headerView.headerBackground.layer.shadowOffset = CGSize(width: 0, height: 1)
headerView.headerBackground.layer.shadowColor = UIColor.black.cgColor
headerView.headerBackground.layer.shadowOpacity = 0.1
headerView.headerBackground.backgroundColor = UIColor(named: "Background")
headerView.headerBackground.layer.cornerRadius = 10
headerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDropDown)))
return headerView
} else {
return nil
}
}
#objc func handleDropDown() {
print("Handle Drop Down")
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 {
return 0.0001
} else {
return 54
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
if section == 0 {
return 10
} else {
return 20
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return settings.count + 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1
} else {
return settings[section - 1].count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingTitleCell", for: indexPath) as! SettingsTitleTableViewCell
cell.titleLabel.text = "Settings"
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingCell", for: indexPath) as! SettingTableViewCell
cell.setSetting(setting: settings[indexPath.section - 1][indexPath.row])
print("Index Path: \(indexPath.row) SettingCount: \(settings[indexPath.section - 1].count)")
return cell
}
}
That code produces this result:
I would love some help on how to make this work the way I have shown in the design. Thanks ahead of time for any help.
For collapse your header you can use this: https://github.com/jeantimex/ios-swift-collapsible-table-section
i used nested tableview to resolve it uibezierpath may be render incorrect layouts.

How to show number of row in section after tapping on section header in swift?

I have a tableView which contain some
But i want it to show only section header and when i tap on section header it will show all employee details contining that section only.
Below is my code:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return company.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return company[section].employees!.count
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if company[section].employees!.count < 1 {
return nil
}
return company[section].companyName
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! EmployeeTableViewCell
let employeeDetails = company[indexPath.section].employees!.allObjects as! [Employees]
cell.lblName.text = employeeDetails[indexPath.row].employeeName
cell.lblAddress.text = String(employeeDetails[indexPath.row].address!)
cell.lblAge.text = String(employeeDetails[indexPath.row].age!)
return cell
}
Thanks!
First add a button on your header view on that you can call a function for extending cell .
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let titleHeader = company[section].companyName // Also set on button
let headerCell = UIView(frame: CGRectMake(0 , 0, tableView.frame.size.width , 40 ))
headerCell.backgroundColor = UIColor.whiteColor()
let button = UIButton(frame: headerCell.frame)
button.addTarget(self, action: "selectedSectionStoredButtonClicked:", forControlEvents: UIControlEvents.TouchUpInside)
button.setTitle(titleHeader, forState: UIControlState.Normal)
button.tag = section
headerCell.addSubview(button)
return headerCell
}
Now check selected header view on buttonclicked
func selectedSectionStoredButtonClicked (sender : UIButton) {
if (selectedArray.containsObject(sender.tag)){
selectedArray.removeObject(sender.tag)
}else{
//selectedArray.removeAllObjects() // Uncomment it If you don't want to show other section cell .
selectedArray.addObject(sender.tag)
}
tableViewObj.reloadData()
}
NOTE :- Here I declared selectedArray as NSMutableArray Global "tableViewObj" is your tableview object
Now proceed for final steps . make changes in your numberOfRowsInSection
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (selectedArray.containsObject(section)){
return company[section].employees!.count
}else{
return 0
}
}
Try it , It'll work fine , If you have any confusion than leave comment.
Create a varialbe like
var hiddenSections: [Int] = []
//TableView
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return tempContact.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if hiddenSections.contains(section) {
return 0
}
return tempContact[section].count
}
//Create a headerCell add label and a button .do it in storyboard
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableCellWithIdentifier("HeaderCell")! as! HeaderCell
header._lblName.text = tempContact[section].NAME
header._btnExpand.addTarget(self, action: "hideSection:", forControlEvents: .TouchUpInside)
return header.contentView
}
//Button Click
func hideSection(sender: UIButton) {
if hiddenSections.contains(sender.tag) {
hiddenSections.removeAtIndex(hiddenSections.indexOf(sender.tag)!)
_tableView.reloadSections(NSIndexSet(index: sender.tag), withRowAnimation: .Automatic)
_tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: sender.tag), atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)
}
else {
hiddenSections.append(sender.tag)
_tableView.reloadSections(NSIndexSet(index: sender.tag), withRowAnimation: .Automatic)
}
}

Expandable Sections UITableView IndexPath SWIFT

Im pretty new to coding, I only know Swift. I have found several tutorials to produce drop down sections in a table. Basically it will represent a TV show, the headers will be the seasons and the drop down list of episodes from each season.
I managed to get this working perfectly for what I want from https://github.com/fawazbabu/Accordion_Menu
This all looks good, however I need to be able to select from the drop down items. I have added didSelectRowAtIndexPath with just a simple print of the rows to start with. When I select a row, section or cell, random index paths are returned, the same row can be pressed a second time and return a different value. I thought this was just something I had added to the issue. So I added didSelectRowAtIndexPath to the original code. This has the same issue.
I assume this is because a UIGestureRecogniser is being used as well as didSelectRowAtIndexPath. But I am not sure what the alternative is.
Could someone tell me where I am going wrong please?
import UIKit
import Foundation
class test: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var sectionTitleArray : NSMutableArray = NSMutableArray()
var sectionContentDict : NSMutableDictionary = NSMutableDictionary()
var arrayForBool : NSMutableArray = NSMutableArray()
override func awakeFromNib() {
super.awakeFromNib()
tableView.delegate = self
tableView.dataSource = self
arrayForBool = ["0","0"]
sectionTitleArray = ["Pool A","Pool B"]
var tmp1 : NSArray = ["New Zealand","Australia","Bangladesh","Sri Lanka"]
var string1 = sectionTitleArray .objectAtIndex(0) as? String
[sectionContentDict .setValue(tmp1, forKey:string1! )]
var tmp2 : NSArray = ["India","South Africa","UAE","Pakistan"]
string1 = sectionTitleArray .objectAtIndex(1) as? String
[sectionContentDict .setValue(tmp2, forKey:string1! )]
self.tableView.registerNib(UINib(nibName: "CategoryNameTableCell", bundle: NSBundle.mainBundle()), forCellReuseIdentifier: "CategoryNameTableCell")
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sectionTitleArray.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
if(arrayForBool .objectAtIndex(section).boolValue == true){
var tps = sectionTitleArray.objectAtIndex(section) as! String
var count1 = (sectionContentDict.valueForKey(tps)) as! NSArray
return count1.count
}
return 0;
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "ABC"
}
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if(arrayForBool .objectAtIndex(indexPath.section).boolValue == true){
return 50
}
return 2;
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = self.tableView.dequeueReusableCellWithIdentifier("CategoryNameTableCell") as! CategoryNameTableCell
cell.downArrow.hidden = false
cell.backgroundColor = UIColor.blackColor()
cell.tag = section
cell.CategoryLabel.text = sectionTitleArray.objectAtIndex(section) as? String
let cellTapped = UITapGestureRecognizer (target: self, action:"sectionHeaderTapped:")
cell .addGestureRecognizer(cellTapped)
return cell
}
func sectionHeaderTapped(recognizer: UITapGestureRecognizer) {
println("Tapping working")
println(recognizer.view?.tag)
var indexPath : NSIndexPath = NSIndexPath(forRow: 0, inSection:(recognizer.view?.tag as Int!)!)
if (indexPath.row == 0) {
var collapsed = arrayForBool .objectAtIndex(indexPath.section).boolValue
collapsed = !collapsed;
arrayForBool .replaceObjectAtIndex(indexPath.section, withObject: collapsed)
var range = NSMakeRange(indexPath.section, 1)
var sectionToReload = NSIndexSet(indexesInRange: range)
self.tableView .reloadSections(sectionToReload, withRowAnimation:UITableViewRowAnimation.Fade)
tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = self.tableView.dequeueReusableCellWithIdentifier("CategoryNameTableCell") as! CategoryNameTableCell
var manyCells : Bool = arrayForBool .objectAtIndex(indexPath.section).boolValue
if (!manyCells) {
} else {
var content = sectionContentDict .valueForKey(sectionTitleArray.objectAtIndex(indexPath.section) as! String) as! NSArray
cell.CategoryLabel.text = content .objectAtIndex(indexPath.row) as? String
cell.backgroundColor = UIColor( red: 49/255, green: 46/255, blue:47/255, alpha: 1.0 )
}
return cell
}
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
println(indexPath.row)
}
I don't know why you need sections to do it what you want, you can achieve with normal cells, no need to complicate at all. Just differentiating the parent cells of the child cells is all you need.
I have a repository in Github that achieve what you want, without using sections neither UITapGestureRecognizer. As soon as possible I'll try to update the project to better performance and more levels of depth in the dropdown.
You can reach it AccordionMenu
, and feel free to post anything you need as issue.
I hope this help you.