I have a tableview header within my table view that uses a custom cell with a segment control in it.
I have added a gesture recognizer to my storyboard:
#IBAction func swipeLeftTapped(_ sender: Any) {
}
I want to swipe left and decrease the selected segment of the segment control within that storyboard header by 1. How would I do that. Here is what I have tried but obviously it doesn't work.
let header = self.mainTableView.cellForRow(at: IndexPath(row: 0, section: 0)) as! HeaderTableViewCell
also
let header2 = self.mainTableView.headerView(forSection: 0) as! HeaderTableViewCell
header.productSegmentControl.selectedSegmentIndex += 1
Here is my code for the tableview header
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "HeaderCell") as! HeaderTableViewCell
cell.productSegmentControl.selectedSegmentIndex = productIdIndex
return cell
}
How do you access a tableview header like you would a normal cell?
Why you are using a cell for the header, just use a normal view and add whatever you want to this view, and then you can access it's subViews.
check Martin's answer here
Related
I have a UITableView with custom section headers, made via the storyboard using a custom prototype cell with a Identifier of "headerCell", along with a Cocoa Touch Class called "HeaderViewCell" subclassing UITableViewCell.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCell(withIdentifier: "headerCell") as! HeaderViewCell
headerCell.sectionTitle.text = viewModel.items[section].sectionTitle
headerCell.section = section
headerCell.delegate = self
return headerCell
}
The button in the cell fires a delegate func passing in the section that was assigned to it.
Everything works great- setting the title, tapping the button I needed, etc... EXCEPT that when you tap the blank space between the section title (on the left) and button (on the right), the section header highlights as if it's a cell in the section, and then performs the segue for the first row in the section.
Selection is set to "None" in the attributes inspector. If I toggle User Interaction Enabled, then the button does not work.
I've found lots of posts where people are trying to register taps on the section headers (answer: with tap gestures), but exhausted myself in search of how to block them. In the didSelectRow at delegate method, I see the same IndexPath I would as if I clicked on the row and not the header, so I can't block it from there.
Being that using a custom prototype cell is the most widely suggested response to a custom section header, I would have expected this to have been an issue for someone else as well. ?
"HeaderViewCell" subclassing UITableViewCell.
Stop right there. That's totally wrong. Your section header should not be a UITableViewCell. It should be a UITableViewHeaderFooterView (or a subclass thereof).
As soon as you make that change (along with any needed corresponding changes to registration of the header view type), your problem will go away.
Matt's answer should would work.
Create a Subclass of type UITableViewHeaderFooterView and name it CustomHeaderView
class CustomHeaderView: UITableViewHeaderFooterView {
// programmatically add the sectionTitle and whatever else inside here. Matt said there isn’t a storyboard or nib for a HeaderFooterView so do it programmatically
}
Then inside inside viewForHeaderInSection use tableView.dequeueReusableHeaderFooterView and cast it as the CustomHeaderView
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// don't forget to rename the identifier
let customHeaderView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "customHeaderView") as! CustomHeaderView
customHeaderView.sectionTitle.text = viewModel.items[section].sectionTitle
customHeaderView.section = section
customHeaderView.delegate = self
return customHeaderView
}
If not try this.
If you don't want the cell to highlight first set the selection style to .none:
Either set .selectionStyle = .none inside the HeaderCell itself
or
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCell(withIdentifier: "headerCell") as! HeaderViewCell
headerCell.sectionTitle.text = viewModel.items[section].sectionTitle
headerCell.section = section
headerCell.delegate = self
headerCell.selectionStyle = .none // set it here
return headerCell
}
Then in didSelectRowAtIndexPath find out the type of cell that is being selected. If it's a HeaderCell then just return and the cell shouldn't push. If it's any of the other type of cells (eg PushCell) then those cells should perform the segue:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// if it's a HeaderCell then do nothing but print
if let _ = tableView.cellForRowAtIndexPath(indexPath) as? HeaderCell {
tableView.deselectRow(at: indexPath, animated: true)
print("+++++HeaderCell was tapped")
return // nothing should happen
}
// if it's a PushCell then push
if let _ = tableView.cellForRowAtIndexPath(indexPath) as? PushCell {
print("-----PushCell was tapped")
performSegue(withIdentifier...
// or if your using navigationController?.pushViewController(...
}
}
I have a table view in Swift with headers that expand or contract to show or hide cells, but for some reason there are extra row dividers in the middle of the page for no reason, and the label in the header is not showing up.
class ExpandableHeader: UITableViewHeaderFooterView {
var section: Int = 0
let expandLabel = UILabel()
}
func numberOfSections(in tableView: UITableView) -> Int {
// return number of section in table from data
return list.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return number of rows in each section from data
return list[section].items.count + 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create cell with the identifier which was set in the storyboard prototype cell
// set cell data/name from our data object
if indexPath.row < list[indexPath.section].items.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = list[indexPath.section].items[indexPath.row].name
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "custom", for: indexPath)
return cell
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// create a header of type of our subclassed header with section number
let headerView = ExpandableHeader()
headerView.expandLabel.text = "+"
headerView.expandLabel.frame.size.height = 30
headerView.expandLabel.frame.size.width = 30
headerView.expandLabel.textAlignment = NSTextAlignment.center
headerView.addSubview(headerView.expandLabel)
headerView.expandLabel.frame.origin.x = view.frame.maxX - headerView.expandLabel.frame.width
// assign selected/current section number to the header object
headerView.section = section
// create Gesture Recognizer to add ability to select header to this cutsom header with an action
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(headerClicked(sender:)))
// add Gesture Recognizer to our header
headerView.addGestureRecognizer(tapGesture)
return headerView
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// check if row's section expanded parameter is set to true or false. set height of rows accordingly to hide or show them
if list[indexPath.section].expanded == true {
return 44
} else {
return 0
}
}
I added the relevant lines of code above. When you change the row height to 0, should the cell separator lines also be changed to height 0, or hidden?
Why is the header in each section not going all the way to the right side of the screen when the storyboard has it all the way to the right edge?
Is this also why the text label for the header is not being displayed? Because the right edge is getting cut off?
Sorry for the basic questions, I'm still getting the hang of this, so any suggestions would be much appreciated.
To hide extra row divider use below lines of code
let backgroundView = UIView(frame: CGRect.zero)
self.tableView.tableFooterView = backgroundView //instead of tableView give your tableView name
View Setup:
My TableView has 3 sections with 4 or 9 cell each. Each Cell has a Label and TextField.
On Starting to edit a cell at index 2 of each section, I reload the section which will now consist of 9 cells(update model to dequeueCell so that 5 more cells will be added).
Problem:
The tableView scrolls as expected(brings textfield to visible part of the screen) for the unexpanded state of the section. But after I add cells by beginning to edit the textfield of cell at index 2 of any section, the tableView scrolls such that it hides the textfield. The weird scrolling occurs for any cells in the tableview once any section has expanded numbers of cells. Also, while weird scroll is happening, the tableView is reloaded(which is leading to lose the focus away from textfield). I have included tableView.reloadSection(_:) in the didBeginEditing(:_) custom delegate of the cell.
I have seen this problem in iOS 9 and 10
Sorry for poor explanation. Thanks
Heres the Github Repo
And Problem is here
P.S. I am using Swift 3 and Xcode 8.3.3 with deployment target iOS 10
Please do not suggest answer in Swift 4 and Xcode 9
You can try another approach: change the height of cells instead of insert / delete.
Change number of cells to always return all items:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
guard let sectionEnum = Sections(rawValue: section) else { return 0 }
return sectionEnum.getRows(forExpanded: true).count
}
Set height of 'hidden' items to 0:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let sectionEnum = Sections(rawValue: indexPath.section) else { return 0 }
let isExpanded = expandedSectionData[indexPath.section]
if (!isExpanded) {
let object = sectionEnum.getRows(forExpanded: true)[indexPath.row]
if (!sectionEnum.getRows(forExpanded: false).contains(object)) {
return 0;
}
}
return self.tableView.estimatedRowHeight
}
Set cell to clip subviews to its bounds:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
....
cell.clipsToBounds = true;
return cell
}
And change updating code to (remove tableView.reloadSections, change indexPath):
func didBeginEditing(textField: UITextField, cell: UITableViewCell) {
guard let indexPath = tableView.indexPath(for: cell), let section = Sections(rawValue: indexPath.section) else { return }
if indexPath.row == 7 && !expandedSectionData[indexPath.section] {
expandedSectionData[indexPath.section] = true
tableView.beginUpdates()
tableView.endUpdates()
tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.none, animated: true)
textField.becomeFirstResponder()
}
}
You need to make textfield as first responder again, after reloading section text field no longer remains first responder.
You might need to change something like -
func didBeginEditing(textField: UITextField, cell: UITableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
if indexPath.row == 2 && !expandedSectionData[indexPath.section] {
tableView.beginUpdates()
expandedSectionData[indexPath.section] = true
tableView.reloadSections(IndexSet(integer: indexPath.section), with: .automatic)
tableView.endUpdates()
// after tableview is reloaded, get cell again
let cell = tableView.cellForRow(at: IndexPath(row: 2, section: indexPath.section)) as? TestCell
cell?.textField.becomeFirstResponder()
}
}
I have tried running this, kind of looks fine to me.
This issue has to do with your use of self-sizing tableview cells. To fix the issue, comment out these two lines in your viewDidLoad and consider defining the height of your cells with tableView:heightForRowAtIndexPath:.
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
Since the self-sizing tableview documentation states,
To define the cell’s height, you need an unbroken chain of constraints
and views (with defined heights) to fill the area between the content
view’s top edge and its bottom edge
I also tried changing the bottomMargin = textField.bottom constraint from priority 750 to 1000, but this did not fix the issue.
I'm using a custom cell as a section header in my UITableView. In that cell there are three buttons. If any of the buttons are clicked in that section's cell, it should reload that custom section cell only, not any rows. Is this possible?
I was using the following code to reload that cell:
tableViewHome.reloadSections([1], with: UITableViewRowAnimation.none)
It's hiding my section cell and distorting my entire table.
UPDATE
I'm using UITableView and following code I'm using:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cellHeader = tableViewHome.dequeueReusableCell(withIdentifier: "header") as! HeaderTableViewCell
cellHeader.filter1btn.addTarget(self, action: #selector(filterBtnAction(_:)), for: .touchUpInside)
cellHeader.filter2Btn.addTarget(self, action: #selector(filterBtnAction(_:)), for: .touchUpInside)
cellHeader.filter3btn.addTarget(self, action: #selector(filterBtnAction(_:)), for: .touchUpInside)
return cellHeader
}
#IBAction func filterBtnAction(_ sender: UIButton) {
print(sender.tag)
tableViewHome.reloadSections([1], with: UITableViewRowAnimation.none)
}
I'm a little unclear as to what's going on here, but it sounds like there is a UITableView concepts worth explaining here:
UITableView has its own concept of a cell, implemented as UITableViewCell, and its own concept of a header/footer, implemented as UITableViewHeaderFooterView.
Depending on which of these two you meant, there are a few things you can do to get the intended effect:
The UITableViewCell Approach:
If you're using a UITableViewCell as the first row of a section to act like a "header," and you just want to reload that row to the exclusion of the rest of the section, you can call yourTableViewInstance.reloadRows(at:with:) (Apple Documentation) This method takes an array of IndexPaths, and an animation style. You can pass in the indexPath of the one you want to reload.
The UITableViewHeaderFooterView Approach:
If you're using a proper UITableViewHeaderFooterView then you need to make sure that you're providing the appropriate view when reloading the section. Zack Shapiro outlines the steps you need to take in this answer:
Create a class that's a subclass of UITableViewHeaderFooterView.
Register it with your UITableView instance.
Then in viewForHeaderInSection, you do let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderView") as! YourHeaderViewSubclass
The last thing he points out is this:
The deceptive thing is the function calls for a return of UIView? when it really needs a dequeuedReusableHeaderFooterView or reloadData will cause it to disappear.
It depends on which of these two implementation paths you're taking, but this should be enough information to point you in the right direction.
Edit:
Based on the code you added, it looks like you're calling yourTableViewInstance.dequeueReusableCell(withIdentifier:for:) instead of yourTableViewInstance.dequeueReusableHeaderFooterView(withIdentifier:) inside of viewForHeaderInSection.
You need to have a subclass of UITableViewHeaderFooterView and then call it correctly. Create that new subclass, then change this:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cellHeader = tableViewHome.dequeueReusableCell(withIdentifier: "header") as! HeaderTableViewCell
// ...
to this:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cellHeader = tableViewHome.dequeueReusableHeaderFooterView(withIdentifier: "header") as! HeaderTableView
// ...
You need to follow two steps here:
Create a new class, subclassing UITableViewHeaderFooterView instead of UITableViewCell.
Then use the appropriate class as outlined above.
Yes, It is.
Let's say that this is implementation of your method:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let customCell = .... as! YourCustomCell
customCell.someLabel.text = "Some Data"
//... filling your curstom cell
return customCell
}
You can change it in this way
func updateHeaderView(headerView:YourCustomCell, section: Int) {
customCell.someLabel.text = "Some Data"
//... filling your curstom cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let customCell = .... as! YourCustomCell
self.updateHeaderView(customCell, section)
return customCell
}
And call again self.updateHeaderView(customCell, section) whenever you want, e.g.
func buttonClicked() {
let customCell = self.tableView.headerView(forSection: 0) as! YourCustomCell
self.updateHeaderView(customCell, 0)
}
I think your header view class is extending UITableViewHeaderFooterView class. Add a function in the extension
extension UITableViewHeaderFooterView{
func reloadHeaderCell(){
preconditionFailure("This method must be overridden")
}
}
Now override this in your Header class as below
class HeaderView: UITableViewHeaderFooterView {
override func reloadHeaderCell() {
////// add your logic to reload view
}
}
Now you can simply call below line to refresh views
self.tableView?.headerView(forSection:0)?.reloadHeaderCell()
What I did and working very correctly, Please follow the given answer:
SWIFT 3.1
Step 1:
Firstly I took a view xib, designed that according to my requirement and did register in my required class.
Secondly, did sub class class HeaderView: UITableViewHeaderFooterView of UITableViewHeaderFooterView
Like following image:
In my required class(here homeclass) I did register my xib file for my tableview.
override func viewDidLoad() {
super.viewDidLoad()
tableViewHome.register(UINib(nibName: "HeaderView", bundle: nil), forHeaderFooterViewReuseIdentifier: "HeaderView")
}
Step 2:
Then in my required class i did following:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cellHeader = tableViewHome.dequeueReusableHeaderFooterView(withIdentifier: "HeaderView") as! HeaderView
cellHeader.filterAction(cellHeader.filter1Btn)
return cellHeader
}
And it started working as per my requirement, later i added custom delegate for more actions in my class, but by subviewing, its now working.
I am new to iOS Development and I just implemented a simple expandable sections UITableView. I am not able to understand why some rows disappear and sometimes change position when the row heights are recalculated on tapping the section header. I went through all the already answered questions on this topic and have not been able to find the right solution.
Following is a scenario:
Launch the app:
Tap on the section header:
Section expands
All other headers disappear
Tap again
Section collapses
The headers continue to be blank
Scrolled to the bottom and back to the top
The positions of headers changed
Scrolled to the bottom and back to the top again
The positions of headers changed again with some cells still blank
Things I have already tried:
Wrapping reloadRowsAtIndexPaths in updates block (beginUpdates() and endUpdates())
Using reloadRowsAtIndexPaths with animation set to .none
Removing reloadRowsAtIndexPaths at all while keeping the updates block
Using reloadData() instead which actually works but I lose animation
Code:
Here is the link to the project repository.
You're using cells for the header. You shouldn't do that, you need a regular UIView there, or at least a cell that's not being dequeued like that. There's a few warnings when you run it that give that away. Usually just make a standalone xib with the view and then have a static method like this in your header class. Make sure you tie your outlets to the view itself, and NOT the owner:
static func view() -> HeaderView {
return Bundle.main.loadNibNamed("HeaderView", owner: nil, options: nil)![0] as! HeaderView
}
You're reloading the cells in the section that grows, but when you change the section that's grown you'd need to at least reload the former section for it to take the changes to it's cell's height. You can reload the section by index instead of individual rows in both cases
Ok as you ask, I am changing my answer according to you.
import UIKit
class MyTableViewController: UITableViewController {
let rows = 2
var categories = [Int](repeating: 0, count: 10)
struct Constants {
static let noSelectedSection = -1
}
var selectedSection: Int = Constants.noSelectedSection
func selectedChanged(to selected: Int?) {
let oldIndex = selectedSection;
if let s = selected {
if selectedSection != s {
selectedSection = s
} else {
selectedSection = Constants.noSelectedSection
}
tableView.beginUpdates()
if(oldIndex != -1){
tableView.reloadSections([oldIndex,s], with: .automatic)
}else{
tableView.reloadSections([s], with: .automatic)
}
tableView.endUpdates()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("reloading section \(section)")
return (selectedSection == section) ? rows : 0;//rows
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "Header")
if let categoryCell = cell as? MyTableViewCell {
categoryCell.category = section + 1
let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
recognizer.numberOfTapsRequired = 1
recognizer.numberOfTouchesRequired = 1
categoryCell.contentView.tag = section;
categoryCell.contentView.addGestureRecognizer(recognizer)
}
return cell?.contentView
}
func handleTapGesture(recognizer: UITapGestureRecognizer) {
if let sindex = recognizer.view?.tag {
selectedChanged(to: sindex)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Body", for: indexPath)
if let label = cell.viewWithTag(1) as? UILabel {
label.text = "Body \(indexPath.section + 1) - \(indexPath.row + 1)"
}
return cell
}
}
As you can see now I am just reloading a particular section instead of reloading the whole table.
also, I have removed gesture recognizer from the cell & put this into the main controller.