Segmented Control with a UITableView - swift

I have a segmented control that is filled using an enum. I have a table view to show the data of each case. What is the proper way to handle this use-case rather than hardcoding switch-cases for numberOfRowsInSection and cellForRowAt?
let segmentedControl = UISegmentedControl(items: SegmentedControlItems.allCases.map {$0.rawValue})
private enum SegmentedControlItems: String, CaseIterable {
case case1 = "Case1"
case case2 = "Case2"
case case3 = "Case3"
}
private var arr1 = [Type1]()
private var arr2 = [Type2]()
private var arr3 = [Type3]()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch view?.segmentedControl.selectedSegmentIndex {
case 0:
return case1.count
case 1:
return case2.count
case 2:
return case3.count
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: AssetView.AssetCell.reuseIdentifier, for: indexPath) as? AssetView.AssetCell else {
fatalError("Error trying to dequeue cell")
}
switch view?.segmentedControl.selectedSegmentIndex {
case 0:
setupCell(case1)
case 1:
setupCell(case2)
case 2:
setupCell(case3)
default:
return UITableViewCell()
}
return cell
}

put case1, case2, and case3 in an array. Let's call it cases:
let cases = [case1, case2, case3]
Then index into that using your segmented control's index:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let selectedItem = view?.segmentedControl.selectedSegmentIndex,
selectedItem < cases.count else { fatalError("splat!" }
return cases[selectedItem]
}

Perfect article about how to use table view with different datasources. It really helped me.
https://betterprogramming.pub/uitableview-sections-with-nested-types-27eabb833ad9

Related

Issue displaying search results

Essentially I have tableview with JSON data and a search controller for the user to quickly find values from the response. While the tableview data loads initially when attempting to search I get the error index out of range. The error occurs inside the cellForRowAt function. The following is what I am currently doing:
var sections = [customerListSection]()
var structure = [customerList]()
var searchsections = [customerListSection]()
var searchstruct = [customerList]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
let searchsection = searchsections[section]
if isFiltering() {
return searchsection.count
}
return section.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = sections[indexPath.section].items[indexPath.row]
let customerList: customerList
if isFiltering() {
customerList = searchstruct[indexPath.row]
} else {
customerList = item
}
return cell
}
struct customerListSection {
let customerType : String
var items : [customerList]
}
struct customerList: Decodable {
let customerid: Int
let customer: String
let type: String
}
From your code, it seems that there are multiple sections, so you need to implement numberOfSections(in:)method.
If you do not implement this method, the table configures the table with one section.)
https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614860-numberofsections
And since the number of rows is the number of items, not the number of sections, the tableView(_:numberOfRowsInSection:) method should return items.count.
The following is the modified code:
override func numberOfSections(in tableView: UITableView) -> Int {
if isFiltering() {
return searchsections.count
}
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return searchsections[section].items.count
}
return sections[section].items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let customerList: customerList
if isFiltering() {
customerList = searchsections[indexPath.section].items[indexPath.row]
} else {
customerList = sections[indexPath.section].items[indexPath.row]
}
cell.textLabel?.textAlignment = .left
cell.selectionStyle = .none
cell.textLabel?.font = .boldSystemFont(ofSize: 20)
cell.textLabel?.numberOfLines = 0
cell.textLabel?.lineBreakMode = .byWordWrapping
cell.textLabel!.text = customerList.customer
return cell
}

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]

getting index out of range in numberOfRowsInSection

I have a weird cellModel in my tableView like this
fileprivate enum CellModel {
case search
case categories
case switcher
case wishes
}
fileprivate enum HeaderModel {
typealias CellType = CellModel
case search
case categories
case switcher
case wishes
var cellModels: [MAWishesViewController.CellModel] {
switch self {
case .search: return [.search]
case .categories: return [.categories]
case .switcher: return [.switcher]
case .wishes: return [.wishes]
}
}
}
private var wishes = [Wish]()
private var categories = [Category]()
private let models:[CellModel] = [.search, .categories, .switcher, .wishes]
private let HeaderModels: [HeaderModel] = [.search, .categories, .switcher, .wishes]
and here is my delegate and dataSource methods : ) I use model with enum and I started think this is bad idea!
func numberOfSections(in tableView: UITableView) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let header = HeaderModels[section]
switch header {
case .categories:
return 1
case .search:
return 1
case .switcher:
return 1
case .wishes:
return self.wishes.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = HeaderModels[indexPath.section].cellModels[indexPath.row]
switch model {
case .search:
if let cell = tableView.dequeueReusableCell(withIdentifier: SearchTableViewCell.name, for: indexPath) as? SearchTableViewCell {
return cell
}
case .categories:
if let cell = tableView.dequeueReusableCell(withIdentifier: CategoriesTableViewCell.name, for: indexPath) as? CategoriesTableViewCell {
cell.collectionView.delegate = self
cell.collectionView.dataSource = self
return cell
}
case .switcher:
if let cell = tableView.dequeueReusableCell(withIdentifier: SwitcherTableViewCell.name, for: indexPath) as? SwitcherTableViewCell {
return cell
}
case .wishes:
if let cell = tableView.dequeueReusableCell(withIdentifier: WishTableViewCell.name, for: indexPath) as? WishTableViewCell {
let wish = self.wishes[indexPath.row]
cell.pictureImageView.image = UIImage(named: wish.image!)
cell.nameLabel.text = wish.name
cell.descriptionLabel.text = wish.description
return cell
}
}
return UITableViewCell.init()
}
And I got "INDEX OUT Of RANGE. All I want is to return wishes.count in section called .wishes
Also I get out of index in cellForRowAt method. So what the problem could be?

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

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
}

How can I populate multiple tableview sections when the contents of the arrays are of different types?

I'm having a bit of a polymorphism issue. I've got three arrays aList, bList and cList that contain different types of equipment "A", "B", "C".
I want to populate a tableviewcontroller with 3 sections for aList, bList, cList. But i'm struggling a bit when I get to:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
The problem i'm having is that ideally i'd have three arrays within one array eg. section = [aList, bList, cList).
In my singleton "A", "B", "C" are structs and represent a type of equipment and so I changed them into classes and subclassed them from a "Equipment" superclass and then tried to create an array of the superclass "Equipment" but that didn't work either.
Advice would be great.
class EquipmentListController: UITableViewController {
var model = SingletonModel.sharedInstance
var aList:[SingletonModel.A] = []
var bList:[SingletonModel.B] = []
var cList:[SingletonModel.C] = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
aList = model.AList
bList = model.BList
cList = model.CList
sections.append(aList)
sections.append(bList)
sections.append(cList)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section{
case 1:
self.tableView.rowHeight = 70
return aList.count
case 2:
return bList.count
case 3:
return cList.count
default:
return 0
}
}
My problem is below with the line let equipment: SingletonModel.A (or B or C) - basically i've got three separate arrays and each is a different type.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
guard let cell = tableView.dequeueReusableCell(withIdentifier: "equipmentcell", for: indexPath) as? EquipmentCell else
{
}
let equipment: SingletonModel.A (or B or C) = changeDataSource(indexPath: indexPath as NSIndexPath)
cell.equipmentTitle.text = equipment.equipmentName
cell.accessoryType = .disclosureIndicator
return cell
}
func changeDataSource(indexPath: NSIndexPath) -> SingletonModel.A, B or C
{
var equipment:SingletonModel.A (B or C)
equipment = A, B or C [indexPath.row]
return equipment
}
func tableView(tableView: UITableView!, titleForHeaderInSection section: Int) -> String!
{
switch section
{
case 1:
return "A"
case 2:
return "B"
case 3:
return "C"
default:
return nil
}
}
override func tableView(_ tableView: UITableView,
heightForHeaderInSection section: Int) -> CGFloat
{
return 40
}
}
Thanks
First you need to fix section numbering: sections are numbered from zero, not from one:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return aList.count
case 1:
return bList.count
case 2:
return cList.count
default:
return -1 // Fail fast
}
}
Next, change the way you set row height:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Rows in section 0 are taller
return indexPath.section == 0 ? 70.0 : 50.0;
}
Now you can set your cell up using A, B, or C:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "equipmentcell", for: indexPath) as? EquipmentCell else
{
return nil
}
var equipment : BaseOfABC = nil
switch indexPath.section {
case 0:
equipment = SingletonModel.A
case 1:
equipment = SingletonModel.B
case 2:
equipment = SingletonModel.C
default:
assert(indexPath.section < 3, "Unknown section")
}
cell.equipmentTitle.text = equipment.equipmentName
cell.accessoryType = .disclosureIndicator
return cell
}
In indexPath you have the row and the section of the cell :
indexPath.row
indexPath.section
You can switch on indexPath.section
For each (equipment) type you can create your own cell type.
In
tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
Switch on indexPath.section, create the matching cell type and set its attributes from the matching (equipment) array.