Adding header to a scope title in TableViewController (Swift - Xcode) - swift

It's quite hard to explain but how can i add a header only to a scope button title instead of to the whole tableviewcontroller? For example, in the screenshots below, i only want the header "Most Interesting Booth" to appear when i tap on "Fun" A.K.A case 2 and not on "All" & "Top 10". Do comment if you are still unsure of what I'm saying. Thank you!
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
switch selectedScope {
case 0:
postData.removeAll()
postData2.removeAll()
for data in tableDataArray {
postData.append(data.boothName)
let value = String(describing: data.boothRating)
postData2.append(value)
}
self.tableView.reloadData()
case 1:
postData.removeAll()
postData2.removeAll()
let sortedTableData = tableDataArray.sorted(by: { $0.boothRating > $1.boothRating })
for data in sortedTableData {
postData.append(data.boothName)
let value = String(describing: data.boothRating)
postData2.append(value)
}
self.tableView.reloadData()
case 2:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> UIView? {
let view = UIView()
view.backgroundColor = UIColor.orange
return view
}
postData.removeAll()
postData2.removeAll()
let sortedTableData = tableDataArray.sorted(by: { $0.boothRating > $1.boothRating })
for data in sortedTableData {
postData.append(data.boothName)
let value = String(describing: data.boothRating)
postData2.append(value)
}
self.tableView.reloadData()
default:
break
}
tableView.reloadData()
}

I suggest to do it in this way, you can use an enum to have a cleaner reference when you update your table view.
Create an enum, for example:
enum BoothScope: Int {
case all
case top
case fun
}
Add a BoothScope var:
var selectedBoothScope: BoothScope = .all
And you can manage it in this way:
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
guard selectedBoothScope == BoothScope(rawValue: selectedScope) else { return }
switch selectedBoothScope {
case .all:
//...
case .top:
//...
case .fun:
//remove this:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> UIView? {
let view = UIView()
view.backgroundColor = UIColor.orange
return view
}
//...
And finally:
(if you prefer you can use the delegate func tableView(_ tableView: UITableView,
viewForHeaderInSection section: Int) -> UIView?)
func tableView(_ tableView: UITableView,titleForHeaderInSection section: Int) -> String? {
return "yourTitle"
}
func tableView(_ tableView: UITableView,heightForHeaderInSection section: Int) -> CGFloat {
if selectedBoothScope == .fun {
return 30
}
return 0
}

Related

Filter items to section

I want to filter items with property isCompleted = true to section with name Completed and non completed items to ToDo. How to render items?
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return manager.tasks.filter({$0.isCompleted == false}).count
} else {
return manager.tasks.filter({$0.isCompleted}).count
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
return "ToDo"
case 1:
return "Completed"
default:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
let currentItem = manager.tasks[indexPath.row]
cell.titleLabel.text = currentItem.taskName
cell.descriptionLabel.text = currentItem.description
if manager.tasks[indexPath.row].description?.isEmpty ?? false {
cell.descLabelBottomConstraint.constant = 0
}
let accessoryType: UITableViewCell.AccessoryType = currentItem.isCompleted ? .checkmark : .none
cell.accessoryType = accessoryType
return cell
}
I guess I need to filter items into two different arrays? But which way is the most correct?
Never filter things in numberOfRowsInSection. Don't do that, this method is called very often.
Create a model
struct Section {
let title : String
var items : [Task]
}
Declare the data source array
var sections = [Section]()
In viewDidLoad populate the array and reload the table view
sections = [Section(title: "ToDo", items: manager.tasks.filter{!$0.isCompleted}),
Section(title: "Completed", items: manager.tasks.filter{$0.isCompleted})]
tableView.reloadData()
Now the datasource methods become very clean (and fast)
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].items.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section].title
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
let currentItem = sections[indexPath.section].items[indexPath.row]
cell.titleLabel.text = currentItem.taskName
cell.descriptionLabel.text = currentItem.description
if currentItem.description?.isEmpty ?? false {
cell.descLabelBottomConstraint.constant = 0
} // you have to add an else clause to set the constraint to the default value
cell.accessoryType = currentItem.isCompleted ? .checkmark : .none
return cell
}
It would be still more efficient to filter the items O(n) with a partition algorithm
let p = manager.tasks.partition(by: { $0.completed })
sections = [Section(title: "ToDo", items: Array(manager.tasks[p...])),
Section(title: "Completed", items: Array(manager.tasks[..<p]))]
tableView.reloadData()
You can create 2 properties completed and notCompleted in the Manager and use them as dataSource of the tableView.
class Manager {
lazy var completed: [Task] = {
return tasks.filter({ !$0.isCompleted })
}()
lazy var notCompleted: [Task] = {
return tasks.filter({ $0.isCompleted })
}()
}
UITableViewDataSource and UITableViewDelegate methods,
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? manager.notCompleted.count : manager.completed.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return section == 0 ? "Todo" : "Completed"
}
You want your original dataSource to be an array of the 2 different arrays (one with completed and one that is not completed.) [[]]
I found This one that seems pretty solid. However, it returns an dictionary, but i rewrote it slightly for you:
extension Sequence {
func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [[Iterator.Element]] {
return Dictionary.init(grouping: self, by: key).map({$0.value})
}
}
This way when you are in title header or cellForRowAt you can call it by manager.task[indexPath.section][indexPath.item]

TableView - “Thread 1: Fatal error: Index out of range in Swift”

I'm pulling data from the database, I'm having no problems with data, but I'm having trouble transferring this data to the text in the Cell. I think I'm having a problem with the numberOfRowsInSection count. I want to add as much data as the sum of the two data, but I'm having trouble this way.
class ChatRoomViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var bilgiCevap = [String]()
var bilgiKullanıcı = [String]()
override func viewDidLoad() {
super.viewDidLoad()
FCevap()
chatTableView.delegate = self
chatTableView.dataSource = self
func FCevap(){
let client = SQLClient.sharedInstance()!
client.connect("...", username: "...", password: "...", database: "...") { success in
client.execute("SELECT ... FROM ... WHERE . LIKE '\(self.form_no)' AND ... LIKE '0'", completion: { (_ results: ([Any]?)) in
for table in results as! [[[String:AnyObject]]] {
for row in table {
for (_, value) in row {
if let intVal = value as? String {
self.bilgiKullanıcı.append(String(intVal))
}} }}
DispatchQueue.main.async {
self.Kullanici()
self.chatTableView.reloadData()}
client.disconnect() }) }
}
func Kullanıcı(){
let client = SQLClient.sharedInstance()!
client.connect("...", username: "...", password: "...", database: "...") { success in
client.execute("SELECT ... FROM ... WHERE ... LIKE '\(self.form_no)' AND ... LIKE '1'", completion: { (_ results: ([Any]?)) in
for table in results as! [[[String:AnyObject]]] {
for row in table {
for (_, value) in row {
if let intVal = value as? String {
self.bilgiCevap.append(String(intVal))
}} }}
DispatchQueue.main.async {self.chatTableView.reloadData()}
client.disconnect() }) }
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = chatTableView.dequeueReusableCell(withIdentifier: "chatCell") as! ChatCell
let messageGıden = self.bilgiKullanıcı[indexPath.row]
cell.chatTextView.text = messageGıden
cell.usernameLabel.text = "..."
cell.setBubbleType(type: .incoming)
let messageGelen = self.bilgiCevap[indexPath.row]
cell.chatTextView2.text = messageGelen
cell.userNameLabel2.text = "Kullanıcı"
cell.setBubbleType2(type: .outgoing)
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return bilgiCevap.count + bilgiKullanici.count
}
}
if you have data sources you need to take two Sections in number of section delegate.
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
And in numberOfRows section
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return dataSource1.count
} else {
return dataSource2.count
}
}
and then inside cellFor rowAt delegate
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let data = dataSource1[indexPath.row]
} else {
let data = dataSource2[indexPath.row]
}
}

How to adjust or remove grouped tableView Header size

I have a grouped tableView and it has four sections , I would like to reduce the size of the header section in some section headers and I would like to remove the section header in other sections. I have tried solutions in most questions similar to mine but nothing works.
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
tableView.separatorStyle = .none
tableView.clipsToBounds = true
self.tableView.sectionHeaderHeight = UITableView.automaticDimension
self.tableView.estimatedSectionHeaderHeight = 200
self.registerTableViewCells()
}
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
(view as? UITableViewHeaderFooterView)?.textLabel?.textColor = UIColor.gray
if section == 0 {
(view as? UITableViewHeaderFooterView)?.contentView.backgroundColor = UIColor.white
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch section {
case 0:
let headerView = self.tableView.dequeueReusableHeaderFooterView(withIdentifier: "\(TestHeaderView.self)" ) as! TestHeaderView
return headerView
case 1, 2, 3 :
return nil
default:
return nil
}
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0, 1, 2:
return nil
case 3:
return " some title"
default:
return nil
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
switch section {
case 0:
return UITableView.automaticDimension
case 1:
return CGFloat(10.0)
case 2:
return CGFloat(10.0)
case 3:
return CGFloat(43.0)
default:
return 0
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNonzeroMagnitude
}
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
return 250
}
I would like to have the tableView section headers to have specific heights as highlighted in heightForHeaderInSection.
You don't seem to be setting the dataSource or tableView delegates (unless it is happening in some part of the code not shown). These will be needed for this (and most dynamic tableview functions).
For headers/footers that won't change size it is reasonably straightforward to create the them - return the view you want to use from tableView(_:viewForHeaderInSection:) and the height you want in tableView(_:heightForHeaderInSection:). Set these to nil and 0 respectively to hide a header.
A simple example that provides different size headers for section 0 & 2, no header for section 1, and hides all footers. I've set the headers background colours to allow them to stand out, and not provided the other methods such as cellForRowAt. Note the height is controlled by the height method, not the frame height.
class MyVC1: UIViewController {
var mapa: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
title = "Dummy TableView"
view.backgroundColor = .lightGray
let tableView = UITableView(frame: CGRect(x:50, y:100, width: 300, height: 680), style: .grouped)
view.addSubview(tableView)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
}
}
//add dataSource and delegate support
extension MyVC1: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
5
}
func numberOfSections(in tableView: UITableView) -> Int {
3
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch section {
case 0:
let v = UIView(frame: CGRect(x: 10, y: 10, width: tableView.frame.width - 20, height: 90))
v.backgroundColor = .purple
return v
case 1: return nil
case 2:
let v = UIView(frame: CGRect(x: 10, y: 10, width: tableView.frame.width - 20, height: 20))
v.backgroundColor = .magenta
return v
default: fatalError()
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
switch section {
case 0: return CGFloat(60)
case 1: return CGFloat(0)
case 2: return CGFloat(100)
default: fatalError()
}
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
nil
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
0
}
}
have you added UITableViewdelegate and UITableViewDataSource ?
or try this self.tableView.estimatedSectionHeaderHeight = 80

UITableView performance problem with HeaderCell

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

Swift - when is data actually loaded in TableViewController

I'm very much a Swift beginner - am populating a table view from Firebase data.
In the table footer I want to display some calculated totals under the table columns. However when calling footerCell.configure(priceLines, isPortrait: isPortrait) the priceLines dictionary is still empty.
How to remedy this?
Thanks in advance, André Hartman, Belgium
import UIKit
import FirebaseDatabase
class ListTableViewController: UITableViewController {
var priceLines = [NSDictionary]()
var isPortrait = false
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ListTableViewController.rotated), name: UIDeviceOrientationDidChangeNotification, object: nil)
loadDataFromFirebase()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return priceLines.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("profileCell", forIndexPath: indexPath) as! PriceTableViewCell
cell.configure(priceLines, row: indexPath.row, isPortrait: isPortrait, source: "intraday")
return cell
}
override func tableView(tableView: UITableView,viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("HeaderCell") as! CustomHeaderCell
headerCell.configure(isPortrait)
return headerCell
}
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footerCell = tableView.dequeueReusableCellWithIdentifier("FooterCell") as! CustomFooterCell
footerCell.configure(priceLines, isPortrait: isPortrait)
return footerCell
}
override func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 30.0
}
override func tableView (tableView:UITableView, heightForHeaderInSection section:Int) -> CGFloat
{
return 50.0;
}
// MARK:- Load data from Firebase
func loadDataFromFirebase() {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
refInter.observeEventType(.Value, withBlock: { snapshot in
var tempItems = [NSDictionary]()
for item in snapshot.children {
let child = item as! FIRDataSnapshot
let dict = child.value as! NSDictionary
tempItems.append(dict)
}
self.priceLines = tempItems
self.tableView.reloadData()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
})
}
func rotated()
{
let newDisplay = (UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation))
if(newDisplay != isPortrait){
self.tableView.reloadData()
}
isPortrait = newDisplay
}
}
The documentation clearly says that
When the table view is about to appear the first time it’s loaded, the
table-view controller reloads the table view’s data.
So, it will reload the table automatically somewhere between viewDidLoad and viewWillAppear. Your priceLines is empty at this point and will be populated with data only when the closure in the method loadDataFromFirebase is fired. I'm not sure when it happens in your case, but as you call implicitly reloadData then you should have already priceLines nonempty (of course if the results in the closure have some data)