Change UITableView section header when it's on top - swift

I am using a custom header from Xib file for my table view using this code:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let result: Result = (self.responseData?.result![section])!
let headerCell = Bundle.main.loadNibNamed("HeaderViewTableViewCell", owner: self, options: nil)?.first as! HeaderViewTableViewCell
headerCell.sectionName.text = "Title"
if (section == 0) {
headerCell.sectionName.textColor = UIColor(red: 49/255.0, green: 149/255.0, blue: 213/255.0, alpha: 1)
}
return headerCell
}
Then I want to change the header sectionName when it is scrolled to top, I have tried this code
let topSection = self.mainTable .indexPathsForVisibleRows?.first?.section
let currentHeader : HeaderViewTableViewCell = self.mainTable .headerView(forSection: topSection!) as HeaderViewTableViewCell
currentHeader.sectionName.textColor = UIColor.red
But I get this error: Cannot convert value of type 'UITableViewHeaderFooterView?' to type 'HeaderViewTableViewCell' in coercion
Is there any way to cast the headerView to my custom type?

First of all I suggest you to use UITableViewHeaderFooterView for your header view. You can make a subclass and add custom code. For this example I will use an empty subclass:
class HeaderView: UITableViewHeaderFooterView {
override func prepareForReuse() {
super.prepareForReuse()
// set you default color (other properties) here.
// when scrolling fast the view gets reused and sometimes
// the view that's on top will suddenly appear on the bottom still with the previous values
textLabel?.textColor = .black
}
}
Register your header view (I am skipping all other unrelated code):
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(HeaderView.self, forHeaderFooterViewReuseIdentifier: "Header")
// If you have a nib file for your HeaderView then register nib instead
// Make sure in our nib file you set class name to HeaderView
// And the file name is also HeaderView.xib
// tableView.register(UINib.init(nibName: "HeaderView", bundle: nil) , forHeaderFooterViewReuseIdentifier: "Header")
}
Implement delegate methods:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "Header")
view?.textLabel?.text = "Hello"
return view
}
Create a method for updating headers:
// Iterates through section header views and
// checks for positions in relation to the tableview offset
func updateHeaders() {
var sectionHeaders: [Int: HeaderView?] = [:]
var i = 0
while i < numberOfSections {
sectionHeaders[i] = tableView.headerView(forSection: i) as? HeaderView
i += 1
}
let availableHeaders = sectionHeaders.flatMap { $0.value != nil ? $0 : nil }
for (index, header) in availableHeaders {
let rect = tableView.rectForHeader(inSection: index)
if rect.origin.y <= tableView.contentOffset.y + tableView.contentInset.top || index == 0 {
header!.textLabel?.textColor = .red
} else {
header!.textLabel?.textColor = .black
}
}
}
And call your updateHeaders() from UIScrollViewDelegate method scrollViewDidScroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateHeaders()
}
Also update headers before the view will be displayed (before any scroll appeared), for that use the UITableViewDelegate method willDisplayHeaderView:
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
updateHeaders()
}

Related

How can I prevent an image from appearing in table view section header cell when it is not set

I have a table view with section cells and one cell in each section to display an image when the section cell is clicked on. Upon first click there is no image in the section header cell (which is the expected behavior). But when I click on it a secong time the image appears in the section header cell. I am not sure why this is happening and how to prevent it.
Here is the code where I deal with any images and set the images for specific cells.[enter image description here](https://i.stack.imgur.com/uGsBA.png)
import UIKit
class expandedSection {
let title: String
var isOpened: Bool = false
var image: UIImage!
init(title: String,
isOpened: Bool = false, image: UIImage) {
self.title = title
self.isOpened = isOpened
self.image = image
}
}
class AnalyticsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
/*
Variables
*/
#IBOutlet var buttons: [UIButton]!
#IBOutlet weak var tableView: UITableView!
var sectionImages = [UIImage(named: "graph1"), UIImage(named: "graph1")]
private var sections = [expandedSection]()
/*
Constructor
*/
override func viewDidLoad() {
// set up models
sections.insert(expandedSection(title: "Title", image: sectionImages[0]!), at: 0)
view.backgroundColor = UIColor(red: 127/255, green: 204/255, blue: 204/255, alpha: 1)
super.viewDidLoad()
self.tableView.register(UINib.init(nibName: "ExercisesTableViewCell", bundle: .main), forCellReuseIdentifier: "ExercisesTableViewCell")
tableView.dataSource = self
tableView.delegate = self
self.navigationItem.title = "Progression"
}
/*
TableView Initializers - Legs
*/
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
// creates number of rows in sections based off the options array length + 1 (to account for the section header)
if section.isOpened {
return 2
}
else {
return 1
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: ExercisesTableViewCell = tableView.dequeueReusableCell(withIdentifier: "ExercisesTableViewCell", for: indexPath) as! ExercisesTableViewCell
if indexPath.row == 0 {
cell.textLabel?.text = sections[indexPath.section].title
cell.textLabel?.textColor = UIColor.init(red: 73/255, green: 72/255, blue: 178/255, alpha: 1)
}
else {
cell.selectionStyle = .none
cell.textLabel?.text = ""
cell.imageView?.image = sections[indexPath.section].image
cell.imageView?.bottomAnchor.constraint(equalTo: cell.bottomAnchor, constant: 3).isActive = true
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row != 0 {
return 400.0
}
else {
return 50
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.row == 0 {
sections[indexPath.section].isOpened = !sections[indexPath.section].isOpened
tableView.reloadSections([indexPath.section], with: .automatic)
}
}
}
I have tried specifically setting the image to hidden on those cells but that still results in the text in the section headers moveing to the right to accomodate for space.
It looks like this is a layout issue as the UITableView code you've posted, while rather messy, looks to be logically sound.
I suggest that you use the built-in iOS table view section headers as it is designed to handle what you're looking to do. As a starting point, have a look at the viewForHeaderInSection docs.

CellForRowAtIndexPath not Called when reloading table data

I've worked through a lot of answers like this one but I'm still stuck.
I have a viewcontroller with a collectionView and a searchController. When the user searches I want to display a UITableView on top of the collectionView. I'm trying to add the tableview programmatically b/c it's a simple text label I'm displaying
I am able to display the search results for the dummy array I have in viewdidload of my tableviewcontroller class. However when I get the actual results, I can only print them in the numberOfRows function.
All help would be greatly appreciated, here's my code:
This is my TableView Class:
import UIKit
class HashtagSearchList: UITableViewController {
var hashtagListTableview = UITableView()
var hashtagList = [String]()
override func viewDidLoad() {
super.viewDidLoad()
hashtagList = ["One", "Two", "Three"]
hashtagListTableview.delegate = self
hashtagListTableview.dataSource = self
}
// MARK: - Table view data source
//I don't need this, but added it based on some answers... didn't help
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//All these print statements show correct values
print("List Count = \(hashtagList.count)")
print("List of Strings: \(hashtagList)")
print("frame size = \(tableView.frame)")
return hashtagList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
//This print statement only fires on ViewdidLoad
print("cellForRowAt = \(hashtagList[indexPath.row])")
cell.textLabel?.text = hashtagList[indexPath.row]
cell.backgroundColor = .red
return cell
}
}
Here is my ViewController / SearchController code:
class ExploreVC: UIViewController {
var hashtagSearchController = HashtagSearchList()
override func viewDidLoad() {
super.viewDidLoad()
//Search Controller
navigationItem.searchController = searchController
searchController = UISearchController(searchResultsController: hashtagSearchController)
}
//......
// SEARCH CONTROLLER
extension ExploreVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let searchText = searchController.searchBar.text
if searchText == "" { return }
PostFirebase.getHashtagList(hashtag: searchText) { (hashtagList) in
self.hashtagSearchController.hashtagListTableview.frame = CGRect(x: 0.0, y: 0.0, width: self.view.bounds.width, height: self.view.bounds.height)
self.hashtagSearchController.hashtagList = hashtagList
//self.hashtagSearchController.hashtagListTableview.reloadData()
DispatchQueue.main.async {
self.hashtagSearchController.hashtagListTableview.reloadData()
}
}
}
}
}
Here is the tableview from viewDidLoad, and it never changes from here
You initialized hashtagListTableview in HashtagSearchList but you didn't add layout constraints. By default, it will have .zero frame and won't be displayed on the screen.
I guess that the table view on the screen is tableView from UITableViewController. That's why nothing happened when you call self.hashtagSearchController.hashtagListTableview.reloadData().
To fix it, try to use tableView instead of hashtagListTableview. Replace
self.hashtagSearchController.hashtagListTableview.reloadData()
with
self.hashtagSearchController.tableView.reloadData()

Can't get indexPath of cell in header

I have created prototype custom header cell for a tableView with a button on it. I am trying to get the indexPath of the cell when the button is tapped, but I don't receive it. Any idea what I am doing wrong here?
protocol MediaHeaderCellDelegate: class {
func editPost(cell: MediaHeaderCell)
}
class MediaHeaderCell: UITableViewCell {
weak var delegate: MediaHeaderCellDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
class NewsfeedTableViewController:UITableViewController, MediaHeaderCellDelegate {
func editPost(cell: MediaHeaderCell) {
guard let indexPath = tableView.indexPath(for: cell) else {
print("indexpath could not be given")
return}
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaHeaderCell) as! MediaHeaderCell
cell.delegate = self
cell.media = media[section]
return cell
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
cell.delegate = self
return cell
}
}
So this is actually all about learning what section a section header belongs to?? Here’s what I do. I have a header class:
class MyHeaderView : UITableViewHeaderFooterView {
var section = 0
}
I register it:
self.tableView.register(
MyHeaderView.self, forHeaderFooterViewReuseIdentifier: self.headerID)
I use and configure it:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let h = tableView
.dequeueReusableHeaderFooterView(withIdentifier: self.headerID) as! MyHeaderView
// other stuff
h.section = section // *
return h
}
Now if the header view is tappable or contains a button or whatever, learning what section this is the header of is trivial.
Your immediate issue is that you are using a table cell as a section header view. That should not be done. Once you resolve that, your next task is to determine the table section from the header view whose button was tapped.
First, change your MediaHeaderCell to be a header view that extends UITableViewHeaderFooterView and update your protocol accordingly:
protocol MediaHeaderViewDelegate: class {
func editPost(view: MediaHeaderView)
}
class MediaHeaderView: UITableViewHeaderFooterView {
weak var delegate: MediaHeaderViewDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
Then you need to register the header view in your view controller.
Then update your viewForHeaderInSection:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: Storyboard.mediaHeaderView) as! MediaHeaderView
view.delegate = self
view.media = media[section]
view.tag = section
return view
}
And last, update your protocol method implementation:
func editPost(view: MediaHeaderView) {
let section = view.tag
// do something
}
There is one possible issue with this. If your table allows sections to be added or removed, then it is possible that a header view's tag could be wrong when the button is tapped.

How to make first table view cell as static and then load data from second cell using swift

class FiorstTableViewController: UITableViewController {
var data = ["1","2","3","4","5","6","7","8"]
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0{
return 1
}
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SecondCellfortable") as! CheckTableViewCell
cell.LabelNumber.text = data[indexPath.row]
if indexPath.row == 0 {
print("my first cell")
}
return cell
}
}
I need to load data from the second table view cell and making first cell as static.i have searched other stack overflow answers nothing works out for me..help me to do this one
You have two ways to implement that:
1) You can make two cell for UITableView first one always return when indexPath.row == 0
and for all return your SecondCellfortable
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50 //row count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("FirstCellfortable") as! CheckTableViewCellFirst
cell.LabelNumber.text = data[indexPath.row]
print("my first cell")
return cell
} else {
tableView.dequeueReusableCellWithIdentifier("SecondCellfortable") as! CheckTableViewCellSecond
return cell
}
}
OR
2) You can add the UITableViewHeader
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: tableView.sectionHeaderHeight))
view.backgroundColor = UIColor.redColor()
// Do your customization
return view
}
You cannot have static and dynamic cells in the same tableview. I recommend setting up your "first table cell" as a UIView then setting the tableheaderview to your custom view.
let headerView = Bundle.main.loadNibNamed("YourCustomView", owner: self, options: nil)?[0] as? YourCustomView
tableView.tableHeaderView = headerView

Swift: Unsatisfying constraints (auto layout) after tableView reloadSections

my problem looks like a weird behavior to me. I'm using Swift and iOS9+.
I've set up an UIViewController in storyboard with some views and a tableview. (Views are vertically above the tableview) All is set up properly with autolayout, no warnings and the UIViewController displays correctly.
In viewDidLoad() I request some table view data from an API and call tableView.reloadSections() after getting the response, the section fades correctly.
If I tap on a button in the section header, another view controller is presented where I can filter the requested data. After setting the filter, the view controller dismisses and the refreshVitalSigns(...) is called in the delegate.
Then again, I want to reload the table view section to only show the filtered data. When I call reloadSections() again, I get a lot of unsatisfying constraint warnings and the view is messed up, and I don't know why??????
With reloadData() everything works, but I only want to reload the section.
FYI: After requesting the API data, you have to scroll to see the whole table view content. If I scroll first, to see the whole content, and filter afterwards, also the reloadSections() works well! Obviously, it should also work without scrolling first...
Do you have any idea why this strange behavior happens?
I'm greatful for every hint!!!
Best
class JMProfileViewController: UIViewController {
/// Table view top spacing
#IBOutlet weak var tableViewTopSpacing: NSLayoutConstraint!
/// Table view
#IBOutlet var tableView: UITableView!
/// Attention view
#IBOutlet var attentionView: JMAttentionView?
var vitalSigns: [Items] = []
var data: [Items] = []
...
// View did load
override func viewDidLoad() {
super.viewDidLoad()
...
// Table view row height
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44.0
// Register custom tableview header/footer views and cells
...
// Get table view data
let patientId = ...
getData(patientId)
}
/**
Get data
- parameter patientId: Patient ID
*/
func getData(patientId: Int) {
// Request
APIController.sharedInstance.getData(patientId: patientId) { response in
// Result handling
switch response {
case .Success(let result):
// Update vital signs
self.vitalSigns = result
self.data = result
// Reload data
self.tableView.beginUpdates()
self.tableView.reloadSections(NSIndexSet(index: 1), withRowAnimation: .Fade)
self.tableView.endUpdates()
case .Failure(let error):
print(error)
}
}
}
override func updateViewConstraints() {
super.updateViewConstraints()
// Set constraints depending on view's visibility
if let view = attentionView {
if view.hidden {
tableViewTopSpacing.constant = 0
} else {
tableViewTopSpacing.constant = view.bounds.height
}
}
}
// MARK: - Navigation
// Preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Data segue
if segue.identifier == SegueIdentifier.JMVitalSignsSegue.rawValue {
let vsvc = segue.destinationViewController as! JMVitalSignsViewController
vsvc.delegate = self
}
}
}
// MARK: - UITableViewDataSource
extension JMProfileViewController: UITableViewDataSource {
// Number of sections in table view
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 3
}
// Height for header in section
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? 0 : UITableViewAutomaticDimension
}
// Estimated height for header in section
func tableView(tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? 0 : 27.0
}
// View for header in section
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch section {
case 0:
// First section without header
return nil
case 1:
// Configure header
let header = tableView.dequeueReusableHeaderFooterViewWithIdentifier(CellIdentifier.JMTitleButtonHeaderView.rawValue) as! JMTitleButtonHeaderView
header.configure(NSLocalizedString("vitalSigns", comment: ""), buttonTarget: self, buttonImage: UIImage(named: "ic_filter_dark"), buttonAction: #selector(parameterButtonTapped(_:)))
return header
default:
// Configure header
let header = tableView.dequeueReusableHeaderFooterViewWithIdentifier(CellIdentifier.JMTitleButtonHeaderView.rawValue) as! JMTitleButtonHeaderView
header.configure(NSLocalizedString("others", comment: ""))
return header
}
}
/**
Vital signs button tapped
*/
func parameterButtonTapped(sender: UIButton) {
// Show vital signs view controller
self.performSegueWithIdentifier(SegueIdentifier.JMVitalSignsSegue.rawValue, sender: self)
}
// Number of rows in section
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var rows = 0
switch section {
case 0:
// Diagnosis
rows = 1
break
case 1:
// Vital signs
rows = data.count > 0 ? data.count : 1
break
case 2:
// Others
rows = 3
break
default:
break
}
return rows
}
// Cell for row at indexpath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier.JMSubtitleImageRightDetailCell.rawValue, forIndexPath: indexPath) as! JMSubtitleImageRightDetailCell
// Configure cell
...
return cell
case 1:
if data.count > 0 {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier.JMTitleThreeLabelsSubtitleCell.rawValue, forIndexPath: indexPath) as! JMTitleThreeLabelsSubtitleCell
// Configure cell
let item = data[indexPath.row]
cell.configure(item.caption, unit: item.unit, values: item.values)
cell.accessoryType = .DisclosureIndicator
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier.JMBasicCell.rawValue, forIndexPath: indexPath)
// Configure cell
cell.textLabel?.text = NSLocalizedString("noData", comment: "")
cell.selectionStyle = .None
return cell
}
default:
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier.JMBasicCell.rawValue, forIndexPath: indexPath) as! JMDefaultCell
...
return cell
}
}
}
// MARK: - JMVitalSignsViewControllerDelegate
extension JMProfileViewController: JMVitalSignsViewControllerDelegate {
/**
Refresh vital signs
*/
func refreshVitalSigns(selectedItems: [Items]) {
print("Refresh vital signs")
var data: [Items] = []
for item in selectedItems {
for vitalItem in vitalSigns {
if item.match == vitalItem.match {
data.append(vitalItem)
break
}
}
}
self.data = data
// HERE IS MY PROBLEM
// tableView.beginUpdates()
// tableView.reloadSections(NSIndexSet(index: 1), withRowAnimation: .Fade)
// tableView.endUpdates()
tableView.reloadData()
}
}
Finally I've found a workaround.
I changed the UIViewController into a UITableViewController and added every custom UIView in the TableViewHeader (NOT Section header) with auto layout.
Now it works!