setup custom UITableViewHeaderFooterView for reusability - ios

I have a custom section header view defined and registered like this:
class MySectionHeaderView : UITableViewHeaderFooterView {
var section : Int?
var button : UIButton?
}
class MyTableViewController : UITableViewController {
override func loadView() {
super.loadView()
self.tableView.register(MySectionHeaderView.self,
forHeaderFooterViewReuseIdentifier: "reuseIdentifier")
}
override func tableView(_ tableView: UITableView,
viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(
withIdentifier: "reuseIdentifier")! as! MySectionHeaderView
header.textLabel?.text = titleForHeader(section: section)
header.section = section
if header.button == nil {
let button = UIButton(type: .system)
// ... configure button ... //
header.button = button
}
return header
}
}
This works. However it is very strange to put the button and other intializers inside the function tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView. as it breaks the separation of concerns principile. This functions should be about only to set the labels, etc.
Is there a way to initialize the header view, creating sub elements somewhere inside the class MySectionHeaderView?

Set only the data source dependent information of your header in viewForHeaderInSection. Move all the setup code inside the custom header class.
class MySectionHeaderView: UITableViewHeaderFooterView {
var section: Int?
lazy var button: UIButton = {
let button = UIButton(type: .system)
// ... configure button ... //
return button
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
//Add subviews and set up constraints
}
}
Now in your delegate method,
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(
withIdentifier: "reuseIdentifier")! as! MySectionHeaderView
header.textLabel?.text = titleForHeader(section: section)
header.section = section
return header
}

Related

How to handle multiple buttons in a custom UITableViewCell?

I have a custom UITableViewCell which has 2 buttons (for incrementing and decrementing) and a count label in it. What I want to achieve is to update countLabel appropriately when subtractButton or addButton is tapped.
My custom cell class:
class ItemOptionCell: UITableViewCell {
private var count = 0
private var countLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 14)
label.numberOfLines = 0
label.text = "0"
label.adjustsFontSizeToFitWidth = true
return label
}()
private let subtractButton: UIButton = {
let subButton = UIButton(type: .system)
subButton.setTitle("-", for: .normal)
subButton.addTarget(self, action: #selector(decreaseItemCount), for: .touchUpInside)
return subButton
}()
private let addButton: UIButton = {
let addButton = UIButton(type: .system)
addButton.setTitle("+", for: .normal)
addButton.addTarget(self, action: #selector(increaseItemCount), for: .touchUpInside)
return addButton
}()
// contains subtract, add buttons and item count
private var operationsStackView = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .white
configureOperationsStackView()
}
func set(itemOption: ItemOption) {
itemLabel.text = itemOption.title
}
private func configureOperationsStackView() {
addSubview(operationsStackView)
// code for autolayout
}
#objc private func decreaseItemCount() {
if count > 0 {
count -= 1
}
updateCountLabel()
}
#objc private func increaseItemCount() {
count += 1
updateCountLabel()
}
private func updateCountLabel() {
countLabel.text = String(count)
}
Part of ViewController for handling table view delegates:
extension ItemOptionsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.text = "Header #1"
label.backgroundColor = .orange
return label
}
func numberOfSections(in tableView: UITableView) -> Int {
return options.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId) as! ItemOptionCell
let option = options[indexPath.section][indexPath.row]
cell.set(itemOption: ItemOption(title: option))
cell.selectionStyle = .none
return cell
}
}
All the forums I looked up only describe how to handle single buttons.
P.s. I read about assigning tags to buttons but found out that it's not a recommended way as when row count changes managing tags becomes problematic. Therefore, if possible, recommend a way with delegates or closures.
Use delegate for this purpose
protocol ItemOptionCellDelegate: AnyObject {
func didDecreaseItemTapped(in cell: ItemOptionCell)
func didIncreaseItemCount(in cell: ItemOptionCell)
}
class ItemOptionCell: UITableViewCell {
weak var delagate: ItemOptionCellDelegate?
...
private func decreaseItemCount() {
delegate?.didDecreaseItemTapped(in: self)
}
private func increaseItemCount() {
delegate?.didIncreaseItemCount(in: self)
}
in your ViewController
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId) as! ItemOptionCell
let option = options[indexPath.section][indexPath.row]
cell.set(itemOption: ItemOption(title: option))
cell.selectionStyle = .none
cell.delegate = self
cell.countLabel.text = //some value
return cell
}
extension ItemOptionsViewController: ItemOptionCellDelegate {
func didDecreaseItemTapped(in cell: ItemOptionCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
let option = options[indexPath.section][indexPath.row]
//do some stuff with your data, then reload table and set need value for count label.
}
func didIncreaseItemCount(in cell: ItemOptionCell) { ... }
Also, DON'T update countLabel inside cell implementation, basically you have to set value count from your model, for example in ViewController in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell func (in case you use MVC approach)
Forget about tags.
Create gesture recognisers in your cellForRowAt and assign them to the buttons:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
//Create gesture
//Create two of these: one will be for the add button and one for the minus one. Attach to eachh gesture a different function.
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
//Add the two different gestures to both of the buttons
cell.myButton.addGestureRecognizer(tap)
...
}
//Create two of these: one to add and one to subtract
#objc func tapped(){
print("ok")
//Perform your action here
}
Plus, when it comes to adding the buttons in the tableViewCell custom class, call contentView.addSubview(myButton) instead of addSubview(myButton).
Oh and obviously, remove all the stuff in your custom class where you add targets to buttons and stuff like that, only create and add the objects to the cell as I said before.
Edit:
Set your buttons as follows:
//Do this for both buttons
private let subtractButton: UIButton = {
let subButton = UIButton(type: .system)
subButton.setTitle("-", for: .normal)
return subButton
}()

Dynamic section header height on runtime

I have UITableView in view controller with a section header and in the header, I have one UITextView with scroll disabled and pinned all UITextView edges to its super view.
Here is the code for automatic height change
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "CreatePostHeaderView") as? CreatePostHeaderView else {
return nil
}
return view
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableView.automaticDimension
}
And also set the estimated Height with this code
tableView.estimatedSectionHeaderHeight = 75
But on runtime when the text of UITextView exceeds height of 75, it doesn't change after that regardless of UITextView content. So, Do I need to add anything to make sure the table section header height changed according to UITextView content? Am I missing anything here?
When performing some action that changes the height of a cell (including header / footer cells), you have to inform the table view that the height has changed.
This is commonly done with either:
tableView.beginUpdates()
tableView.endUpdates()
or:
tableView.performBatchUpdates(_:completion:)
In this case, you want to call this when the text in your text view changes - easily done with a "callback" closure.
Here is an example of using a UITextView in a reusable UITableViewHeaderFooterView.
This will apply to loading a complex view from a XIB, but since this view is simple (only contains a UITextView), we'll do it all from code. This example uses 3 sections, each with 12 rows (default table view cells).
First, the table view controller class - no #IBOutlet or #IBAction connections, so just create a new UITableViewController and set its custom class to MyTestSectionHeaderTableViewController:
class MyTestSectionHeaderTableViewController: UITableViewController {
var myHeaderData: [String] = [
"Section 0",
"Section 1",
"Section 2",
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 50
tableView.keyboardDismissMode = .onDrag
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 75
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "defCell")
tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: MySectionHeaderView.reuseIdentifier)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return myHeaderData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 12
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "defCell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: MySectionHeaderView.reuseIdentifier) as! MySectionHeaderView
v.myTextView.text = myHeaderData[section]
v.textChangedCallback = { txt in
self.myHeaderData[section] = txt
tableView.performBatchUpdates(nil, completion: nil)
}
return v
}
}
and this is the UITableViewHeaderFooterView class. Note that it needs to conform to UITextViewDelegate so we can tell the controller the text has changed (so it can update the height when needed), and we pass back the newly edited text to update our data source:
class MySectionHeaderView: UITableViewHeaderFooterView, UITextViewDelegate {
static let reuseIdentifier: String = String(describing: self)
var myTextView: UITextView = {
let v = UITextView()
v.isScrollEnabled = false
return v
}()
var textChangedCallback: ((String) -> ())?
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(myTextView)
myTextView.translatesAutoresizingMaskIntoConstraints = false
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
myTextView.topAnchor.constraint(equalTo: g.topAnchor),
myTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myTextView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
myTextView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
])
myTextView.delegate = self
}
func textViewDidChange(_ textView: UITextView) {
guard let str = textView.text else {
return
}
textChangedCallback?(str)
}
}
The result:

UITableView Table Header pulled down when collapsing collapsible section headers

I have a collapsible header for uitableview sections based on another stack overflow post (no idea where now, as that was months ago). As it happens, the testers found a weird bug where collapsing all of the sections pulls the table header view down.
*edit the table view header is just a UI view I dropped into the storyboard, inside the tableview, above the prototype cell. No significant constraints. Just height for the cells and the header. The tableview is pinned to the safe area.
Everything looks fine until you expand one of the sections off screen, then scroll it up so the rows start to slide under the floating section header at the top. Then you tap to collapse it. It collapses, but the header view is pulled down. It looks like it happens when the sections fit on one screen, and the rows were scrolled slightly before the collapse.
Any help would be appreciated.
In my demo project (happy to share), when the four sections are collapsed, it looks like this:
When the user expands some of the sections, scrolls so a section header is sticky at the top and the contents are scrolled under it, then collapses the sticky section header, it can look like this:
I have a protocol for the delegate:
protocol CollapsibleHeaderViewDelegate: class {
func toggleSection(header: CollapsibleSectionHeader, section: Int)
}
protocol SectionHeaderCollapsible {
var isCollapsed: Bool { get }
var rowCount: Int { get }
}
And the subclass of UITableVieHeaderFooterView:
class CollapsibleHeader: UITableViewHeaderFooterView {
#IBOutlet var sectionHeaderLabel: UILabel!
var collapsed = false
weak var delegate: CollapsibleHeaderViewDelegate?
var sectionItem: SectionHeaderCollapsible?
static let reuseIdentifer = "CollapsibleHeader"
func configure(headerText: String) {
textLabel?.text = headerText
}
override func awakeFromNib() {
super.awakeFromNib()
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapHeader)))
}
#objc private func didTapHeader(gestureRecognizer: UITapGestureRecognizer) {
guard let header = gestureRecognizer.view as? CollapsibleHeader else { return }
delegate?.toggleSection(header: self, section: header.tag)
}
}
Then the delegate does something like. this:
struct CollapsibleSection: SectionHeaderCollapsible {
var isCollapsed: Bool = false
var rowCount: Int {
get {
return isCollapsed ? 0 : dataContents.count
}
}
var dataContents: [String]
}
class ViewController: UIViewController {
#IBOutlet var tableView: UITableView!
#IBOutlet var headerView: UITableView!
var sections = [CollapsibleSection(isCollapsed: false, dataContents: ["first", "second"]),
CollapsibleSection(isCollapsed: false, dataContents: ["red", "blue"]),
CollapsibleSection(isCollapsed: false, dataContents: ["seven", "five"]),
CollapsibleSection(isCollapsed: false, dataContents: ["Josephine", "Edward"])]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
let nib = UINib(nibName: "CollapsibleHeader", bundle: nil)
tableView.register(nib, forHeaderFooterViewReuseIdentifier: "CollapsibleHeader")
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].rowCount
}
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else { fatalError() }
cell.textLabel?.text = sections[indexPath.section].dataContents[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let header = self.tableView.dequeueReusableHeaderFooterView(withIdentifier: "CollapsibleHeader") as? CollapsibleHeader else { fatalError() }
header.sectionHeaderLabel.text = "Section \(section + 1)"
header.delegate = self
header.tag = section
return header
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 100
}
}
extension ViewController: CollapsibleHeaderViewDelegate {
func toggleSection(header: CollapsibleHeader, section: Int) {
sections[section].isCollapsed = !sections[section].isCollapsed
tableView.reloadSections([section], with: .fade)
}
}
EDIT:
Looks like my coworkers created a work around based on (or at least similar to) your answer:
if tableView.contentOffset.y < 0 {
var offset = tableView.contentOffset
offset.y = tableView.contentSize.height - tableView.bounds.height
tableView.setContentOffset(offset, animated: true)
} else {
tableView.setContentOffset(tableView.contentOffset, animated: true)
}
Faced same problem, apparently right after "reloadSections", tableView's contentOffset.y has some strange value (you can see it when print "tableView.contentOffset.y" before and after "reloadSections"). So I just set contentOffset after it uncollapse to 0 offset value:
let offset = tableView.contentOffset.y
// Reload section
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
if !sections[section].isCollapsed {
tableView.contentOffset.y = offset - offset
}

How do you change the colour of a section title in a tableview?

Here is what I have at the moment.
How do I refer to this so that I can change the text colour to match my index list? The sectionForSectionIndexTitle worked well for adding in the correct section title but how exactly does one access the title element?
Or is it impossible and I need to redraw the view and add it with viewForHeaderInSection?
you can use the one of UITableViewDelegate's method
swift3 and above
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
if let headerView = view as? UITableViewHeaderFooterView {
headerView.contentView.backgroundColor = .white
headerView.backgroundView?.backgroundColor = .black
headerView.textLabel?.textColor = .red
}
}
objective C
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
if([view isKindOfClass:[UITableViewHeaderFooterView class]]){
UITableViewHeaderFooterView * headerView = (UITableViewHeaderFooterView *) view;
headerView.textLabel.textColor = [UIColor RedColor];
}
}
for Reference I taken the model answer from here
One liner solution (using optional chaining):
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
(view as? UITableViewHeaderFooterView)?.textLabel?.textColor = UIColor.red
}
Custom Title:
override func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let title = UILabel()
title.font = UIFont(name: "SFUIDisplay-Light", size: 13)!
title.textColor = UIColor.redColor()
let header = view as! UITableViewHeaderFooterView
header.textLabel!.font=title.font
header.textLabel!.textColor=title.textColor
header.contentView.backgroundColor = UIColor.whiteColor()
}
Swift Solution
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
view.tintColor = UIColor.red
let header = view as! UITableViewHeaderFooterView
header.textLabel?.textColor = UIColor.white
}
I would use the Appearance() proxy class. I usually add them in a function in AppDelegate and call them didFinishLaunching.
private func setupApperances() {
UILabel.appearance(whenContainedInInstancesOf: [UITableViewHeaderFooterView.self]).textColor = .red
}
You can make your own section title (header/footer) view, and it is easy.
class BlackTableViewHeaderFooterView : UITableViewHeaderFooterView {
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .black
textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
textLabel?.numberOfLines = 0
textLabel?.textColor = .white
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TableViewController : UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(BlackTableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "\(BlackTableViewHeaderFooterView.self)")
// do other setup
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "\(BlackTableViewHeaderFooterView.self)")
header.textLabel?.text = "" // set your header title
return header
}
}

When I swipe a UITableViewCell, the header view moves as well

So, I have a few Swipe actions like delete, block, etc in my UITableView. I wanted to add headers to separate my two sections. So, I added a prototype cell, named it HeaderCell and then went to the view. I added one label, named headerLabe. My problem is that when I swipe for the actions, the header cells were moving as well, which looked bad. I researched, and found a solution to just return the contentView of the cell. However, when I do this, the label has not shown up. I have tried a dozen different solutions, and nothing has worked, so I have turned to SO. Can anyone help me?
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell : CustomHeaderTableViewCell = tableView.dequeueReusableCellWithIdentifier("HeaderCell") as! CustomHeaderTableViewCell
if section == 0 {
headerCell.headerLabel.text = "Thank You's"
} else if section == 1 {
headerCell.headerLabel.text = "Conversations"
}
return headerCell.contentView
}
Thanks so much.
You can use a section Header as #ozgur suggest.If you still want to use a cell.
Refer to this datasource method
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
if indexPath = YourHeaderCellIndexPath{
return false
}
return true
}
check the following methods
In your UIViewController use the following
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 60
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("HeaderCell") as! WishListHeaderCell
headerCell.lblTitle.text = cartsData.stores_Brand_Name
let imgVw = UIImageView()
imgVw.frame = CGRectMake(8, 18, 25, 25)
imgVw.image = UIImage(named: "location.png")
let title = UILabel()
title.frame = CGRectMake(41, 10, headerCell.viwContent.frame.width - 49, 41)
title.text = cartsData.stores_Brand_Name
title.textColor = UIColor.whiteColor()
headerCell.viwContent.addSubview(imgVw)
headerCell.viwContent.addSubview(title)
return headerCell.viwContent
}
In your UITableViewCell use the following
import UIKit
class HeaderCell: UITableViewCell {
#IBOutlet weak var viwContent: UIView!
#IBOutlet weak var imgIcn: UIImageView!
#IBOutlet weak var lblTitle: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.viwContent.backgroundColor = UIColor.grayColor()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//UITableViewCell
let headerCell = tableView.dequeueReusableCellWithIdentifier("headerCell") as! SecJobCCHeaderTableViewCell
// Cell Rect
var cellRect : CGRect = headerCell.frame
cellRect.size.width = screenBounds.width
// Header Footer View
let headerFooterView = UITableViewHeaderFooterView(frame : cellRect)
//Adding Gesture
let swipeGestRight = UISwipeGestureRecognizer(target: self, action:#selector(AddSecJobCostCentreViewController.draggedViewRight(_:)))
swipeGestRight.enabled = true
swipeGestRight.direction = UISwipeGestureRecognizerDirection.Right
headerFooterView.addGestureRecognizer(swipeGestRight)
// Update Cell Rect
headerCell.frame = cellRect
// Add Cell As Subview
headerCell.tag = 1000
headerFooterView.addSubview(headerCell)
// Return Header Footer View
return headerFooterView
}
func draggedViewRight(sender:UISwipeGestureRecognizer) {
// Swipe Gesture Action
let currentHeaderView = sender.view?.viewWithTag(1000) as! SecJobCCHeaderTableViewCell
}

Resources