I have a static grouped table view that has 5 sections (all the sections have headers, no footers). I created all of this using a Storyboard. Now, how can I hide the first/top UITableViewSection (including the header). I tried making an outlet to the UITableViewSection but it tells me that it is not valid (undeclared type):
#IBOutlet var section: UITableViewSection!
I did it this way because I was planning on doing:
section.hidden = true
Can it not be done this way?
My delegates and data sources are set up 100% correctly.
Swift 5:
You can use the delegate method heightForHeaderInSection
:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if (section == 0) {
return 0.0
}
return UITableView.automaticDimension
}
Earlier than Swift 5: Use UITableViewAutomaticDimension instead of UITableView.automaticDimension
If it's not working with height 0.0, use height 0.1
If you want no cells in a particular section, use the delegate method:
func numberOfRowsInSection(section: Int) -> Int {
if (section == 0) {
return 0
}
else {
// return the number of rows you want
}
}
Or to a neater switch-case syntax:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
switch section {
case 0:
return 0.0
default:
return UITableView.automaticDimension
}
}
I tested both and they are working fine.
I also wish you could just make an #IBOutlet to a section and hide it, but sadly it seems not, so...
Based on various suggestions here, I've established the following, which doesn't require any interfering with explicit size values, and preserves whatever you may have set on a storyboard/XIB already. It just makes the header nil and row count 0 for any section you want to hide (which results in a size of 0.0).
Obviously, you can configure sectionShouldBeHidden to work however you need; hiding #1 & #3 are just arbitrary examples.
Swift v5
private func sectionShouldBeHidden(_ section: Int) -> Bool {
switch section {
case 1, 3: return true
default: return false
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if sectionShouldBeHidden(section) {
return nil // Show nothing for the header of hidden sections
} else {
return super.tableView(tableView, titleForHeaderInSection: section) // Use the default header for other sections
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if sectionShouldBeHidden(section) {
return 0 // Don't show any rows for hidden sections
} else {
return super.tableView(tableView, numberOfRowsInSection: section) // Use the default number of rows for other sections
}
}
Update: Unfortunately, the above is only enough if the style of the table view is Plain. When it's Grouped, there's also additional space added between each section, which needs taking care of too.
This extra space is the section's footer, so can be handled like so:
override public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
if sectionShouldBeHidden(section) {
return CGFloat.leastNormalMagnitude // Use the smallest possible value for hidden sections
} else {
return super.tableView(tableView, heightForFooterInSection: section) // Use the default footer height for other sections
}
}
I tried all the solutions here with no success. At the end, adding these delegate methods this one worked:
Swift 5:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 4 ? 0 : return super.tableView(tableView, numberOfRowsInSection: section)
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 4 ? 0.1 : super.tableView(tableView, heightForHeaderInSection: section)
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return section == 4 ? 0.1 super.tableView(tableView, heightForFooterInSection: section)
}
Note that you need to return 0.1 in height, returning 0 won't do it.
0.0 did not work for me. I had to do this in order to make it work.
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
switch section {
case 0:
return 0.01
default:
return UITableViewAutomaticDimension
}
}
For group UITableView with static cells only this solution works:
self.tableView.sectionHeaderHeight = 0;
self.tableView.sectionFooterHeight = 0;
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
let count = self.tableView(tableView, numberOfRowsInSection: section)
if count == 0 {
return CGFloat(Double.leastNormalMagnitude)
}
return 44.0
}
For anyone wanting to hide sections because they are using a static grouped tableView with a dynamic number of sections, the solution below may be of help. In my case, each section with data to display needed to have a custom header. Any section that did not have data, needed to be hidden fully.
The answer above was of great help in my scenario. However, for those who don't always know which section(s) will need to be hidden here is a solution for you extending on the above.
In my scenario, I have up to 12 entries in an array that I want to show in up to 12 sections (amongst other sections in a grouped tableView). If there are less than 12 entries to display, I want to hide the unnecessary sections by giving them 0 height and 0 rows. I also wanted to hide the headerView.
To do this, I did the following:
Set up your tableView as per the excellent answer #sasquatch gave
above.
In the numberOfRowsInSection(section: Int) and tableView(_
tableView: UITableView, heightForHeaderInSection section: Int) functions, check whether the rows/height should be 0.
In my case, I was using sections 1 - 12 for my dynamic data so I used code as below:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//For section 0 and 13, just 1 row is ok
if section == 0 || section == 13 {
return 1
}
//For sections 1 - 12, determine if we have data to populate it, or if we should hide it
if section <= dynamicDataToDisplay.count {
return 2
}
//If it's section 1 - 12, but we don't have a corresponding data entry in dynamicDataToDisplay, then just return 0 rows
return 0
}
The code for the heightForHeader function is similar in logic:
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
switch section {
case 0:
return 45.0
case 13:
return UITableViewAutomaticDimension
default:
if dynamicDataToDisplay.count >= section {
return 25.0
} else {
return 0.0
}
} //end switch
}
Even after setting up these functions, I found that I was still getting headers appearing for the sections I wanted to hide. I guess I thought that viewForHeaderInSection would not be called if the numberOfRows was 0 and heightOfHeader was also 0, but it was still being called.
Adding the following helped ensure that the header wasn't unnecessarily created:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//set up header for dynamic data sections
if 1 ... 12 ~= section {
if tableView.numberOfRows(inSection: section) == 0 {
return nil
}
//Continue with the header set up for valid sections with rows to display data
......
}
}
This solution might help anyone who is still getting a header being created despite its height and rows being set to 0.
Related
I am trying to create a single table in Xcode and I want 3 sections in that table each with different headers.
I want to stick only the header of last section when it scrolls on top not the other sections.
Is there a way to do so please suggest me..
1- Yes, you can do that. In numberOfSections method of UITableViewDelegate, specify how many sections you want. And in viewForHeaderInSection method of UITableviewDelegate, provide a custom view for each section.
func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// Make custom header view for each section
if section == 0 {
let header1 = UIView()
return header1
} else if section == 1 {
let header2 = UIView()
return header2
} else if section == 2 {
let header3 = UIView()
return header3
}
}
2- No, you can not do that. UITableView does not allow you to specify that which particular header should stick to the top and which one should not.
You can set the UITableView style to Plain. It will stick all the section header while scrolling.
I think that is not possible to stick the particular section header.
You could achieve the same behaviour by having a custom cell that would act as the header in the first two sections. And then remove the actual header for those.
For example:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath.section == 0 || indexPath.section == 1) && indexPath.row == 0 {
return HeaderCell()
}
// return your desired cells (make sure to handle the number of
// rows properly as there is effectively 1 more than normal in
// in the first two sections now)
}
You'll also need to adjust the headers for the first two sections:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 2 { return YourHeaderView() }
return nil
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 2 { return yourHeight }
return 0
}
Also, in the numberOfRows delegate function, you should make sure you add 1 (which will be the HeaderCell).
I'm implementing collapsable section headers in a UITableViewController.
Here's how I determine how many rows to show per section:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return self.sections[section].isCollapsed ? 0 : self.sections[section].items.count
}
There is a struct that holds the section info with a bool for 'isCollapsed'.
Here's how I'm toggling their states:
private func getSectionsNeedReload(_ section: Int) -> [Int]
{
var sectionsToReload: [Int] = [section]
let toggleSelectedSection = !sections[section].isCollapsed
// Toggle collapse
self.sections[section].isCollapsed = toggleSelectedSection
if self.previouslyOpenSection != -1 && section != self.previouslyOpenSection
{
self.sections[self.previouslyOpenSection].isCollapsed = !self.sections[self.previouslyOpenSection].isCollapsed
sectionsToReload.append(self.previouslyOpenSection)
self.previouslyOpenSection = section
}
else if section == self.previouslyOpenSection
{
self.previouslyOpenSection = -1
}
else
{
self.previouslyOpenSection = section
}
return sectionsToReload
}
internal func toggleSection(_ header: CollapsibleTableViewHeader, section: Int)
{
let sectionsNeedReload = getSectionsNeedReload(section)
self.tableView.beginUpdates()
self.tableView.reloadSections(IndexSet(sectionsNeedReload), with: .automatic)
self.tableView.endUpdates()
}
Everything is working and animating nicely, however in the console when collapsing an expanded section, I get this [Assert]:
[Assert] Unable to determine new global row index for preReloadFirstVisibleRow (0)
This happens, regardless of whether it's the same opened Section, closing (collapsing), or if I'm opening another section and 'auto-closing' the previously open section.
I'm not doing anything with the data; that's persistent.
Could anyone help explain what's missing? Thanks
In order for a tableView to know where it is while it's reloading rows etc, it tries to find an "anchor row" which it uses as a reference. This is called a preReloadFirstVisibleRow. Since this tableView might not have any visible rows at some point because of all the sections being collapsed, the tableView will get confused as it can't find an anchor. It will then reset to the top.
Solution:
Add a 0 height row to every group which is collapsed. That way, even if a section is collapsed, there's a still a row present (albeit of 0px height). The tableView then always has something to hook onto as a reference. You will see this in effect by the addition of a row in numberOfRowsInSection if the rowcount is 0 and handling any further indexPath.row calls by making sure to return the phatom cell value before indexPath.row is needed if the datasource.visibleRows is 0.
It's easier to demo in code:
func numberOfSections(in tableView: UITableView) -> Int {
return datasource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datasource[section].visibleRows.count == 0 ? 1 : datasource[section].visibleRows.count
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
datasource[section].section = section
return datasource[section]
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if datasource[indexPath.section].visibleRows.count == 0 { return 0 }
return datasource[indexPath.section].visibleRows[indexPath.row].bounds.height
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if datasource[indexPath.section].visibleRows.count == 0 { return UITableViewCell() }
// I've left this stuff here to show the real contents of a cell - note how
// the phantom cell was returned before this point.
let section = datasource[indexPath.section]
let cell = TTSContentCell(withView: section.visibleRows[indexPath.row])
cell.accessibilityLabel = "cell_\(indexPath.section)_\(indexPath.row)"
cell.accessibilityIdentifier = "cell_\(indexPath.section)_\(indexPath.row)"
cell.showsReorderControl = true
return cell
}
I have implemented a tableview (4 sectional) in IOS. Problem is that
I have just added a section header in first section.Other sections don't have a header.First section does not have row (number of rows is 0).other sections have multiple rows.When I scroll , first section's header is not sticky.it is scrolling and out of screen.My table view style is plain.How can I want to make first section's header is always sticky.Code is below.Unfortunatelly tableview is so complicated and I don't want to make it only one section so that I have implemented it multi sectional.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
return tabHeaderView.contentView
}
else {
return UIView()
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 {
return UITableView.automaticDimension
}
else {
return 0 //just first section have a header (tabview)
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
func numberOfSections(in tableView: UITableView) -> Int {
if dataReady {
totalSectionCount = getSectionCount()
return totalSectionCount
}
else {
return 1 //permanent because of tabview
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
if !dataReady || ApplicationContext.instance.userAuthenticationStatus.value == .semiSecure{
return 1//for shimmer cell and semisecure view
}
else {
return getNumberOfRows(sectionNumber: section)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if ApplicationContext.instance.userAuthenticationStatus.value == .semiSecure {
semisecureViewCell = EarningsSemisecureViewCell()
setSemisecureViewTextInfo(cell: semisecureViewCell)
semisecureViewCell.delegate = self
semisecureViewCell.layoutIfNeeded()
return semisecureViewCell
}
else if !dataReady {
return getShimmerCell()
}
else {
if indexPath.row == 0 && !(selectedTabIndex == .BRANDPOINT && indexPath.section == 1){//marka puan listesinde header cell olmayacak
return getSectionHeaderViewCell(tableView: tableView,sectionNumber: indexPath.section)
}
else {
let cell = getTableViewCell(tableView: tableView, indexPath: indexPath)
cell.layoutIfNeeded()
return cell
}
}
}
Each section header will stick to the top until that section has some rows to display. Once you scroll all the rows for the section up. section header will be replaced by the next section header.
Here you can use one of the two solutions.
Instead of table view section header. Put your view on top of UITableView.
You can use only one section and combine all the rows in it.
From how you are describing your setup, I tend to believe that what you are looking for is the tableHeaderView of the tableView, not the section header.
See either this question, or official documentation for more info.
If that does not meet your requirements, you might wanna consider a custom view on top of the tableView, as is described here.
If the first section must always be sticky when any section is displaying, I will consider to put a custom view on top of tableView. Use autoLayout or UIStackView to let it work as a table header view.
I have a tableview and tabview (3 different tab).I want to show 4 section for first tab , 3 section for second tab and . 2 section for third tab.
Just first section's header must be sticky top of the view.Because of this I have implemented headerview just first section but header scrolls and be hidden like a tableview cell .it is not stick on top of the screen.What is the problem here?.Must I implement or override a specific function of tableview?
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0{
return UITableView.automaticDimension
}else {
return 0 //sadece 1. sectionda tablar header olarak olacak diğerlerinde header olmayacak
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
func numberOfSections(in tableView: UITableView) -> Int {
if dataReady {
totalSectionCount = getSectionCount()
return totalSectionCount
}else {
return 1 //permanent because of tabview
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
if !dataReady || ApplicationContext.instance.userAuthenticationStatus.value == .semiSecure{
return 1//for shimmer cell and semisecure view
}else {
return getNumberOfRows(sectionNumber: section)
}
}
There is not any function which restricts Specific Header To Stick and Specific To Scroll. If you defined a header for a section, it will scroll up and will be hidden when next section header comes up.
In your case, you must define first header view/cell as Section Header and manage other headers in cellForRowAt() method. Because you want them to scroll up and not stick at top.
In iOS 11, my section headers always appear, regardless of whether the items are 0 or more.
On iOS 10 devices, my code works and sections disappear when item count is 0. But on iOS 11, the same code has no affect.
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if sections[section].items.count > 0{
return sections[section].title
}else{
return nil
}
}
In iOS 11 if you implement only titleForHeaderInSection and return nil, you will not see a header view. But if you also implement viewForHeaderInSection, regardless of what you return, there will be a section.
This alone will not show a section header:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return nil
}
This will show a section header with no title:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return nil
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
So both methods may return nil and the header will be visible. If only titleForHeaderInSection is implemented, no header shows up. That does seem to be a case only in iOS 11. Not sure if it's a bug or a way to force developers chose one method of the two. But the docs confirm this behaviour about titleForHeaderInSection:
"Return Value: A string to use as the title of the section header. If you return nil , the section will have no title."
So nothing about showing or not showing, this method only returns the string for the title. Which makes sense. But what does look like a bug is that returning nil in viewForHeaderInSection will show the section header.
To hide a section header for, say, section 0, implement the following method like so:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if (section == 0) {
return CGFloat.leastNormalMagnitude //Now section 0's header is hidden regardless of the new behaviour in iOS11.
}
return tableView.sectionHeaderHeight
}
This solution also works for grouped UITableViews, as discussed below.
Update: If you execute reloadSections(..), this solution causes an
NSException of type 'CALayerInvalidGeometry'
If you return 0 in the if statement however, this crash doesn't occur! :)
Therefore, I would say the best solution I have found (atleast for plain UITableViews) is:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if (section == 0) {
return 0
}
return tableView.sectionHeaderHeight
}
Implement tableView(_:heightForHeaderInSection:) to return UITableViewAutomaticDimension.
This will suppress the section header in exactly the case where titleForHeaderInSection returns nil (and otherwise it will use the default header height from the table).
If you explicitly tell iOS 11 to use a height of 0 in heightForHeaderInSection it will hide the section header. You can still use automatic header sizing by returning UITableViewAutomaticDimension for non-zero height headers. Here's an example of a solution to workaround the iOS 11 bug:
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard shouldShowSectionHeader(section: section) else { return 0 }
return UITableViewAutomaticDimension
}
You'll need to implement shouldShowSectionHeader to determine whether or not to show the section header.
iOS 11.3, works in production
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return sections[section].headerViewModel?.makeView(bindImmediately: true)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return sections[section].headerViewModel == nil ? 0 : UITableViewAutomaticDimension
}
return empty view from viewForHeaderInSection
swift 3, 4 and 4.2
To hide your tableView header
tableView.tableHeaderView?.frame = CGRect.zero
and to show it back
tableView.tableHeaderView?.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: 66)
What worked for me:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0.01
}