I have a tableView with different subcategories ("Algrebra","Biology","Chemistry") who are indexed and searchable via the searchController. I want to put these subcategories inside multiple categories ("Urgent","Important","Not Important") and expand/collapse them on click. I also want to have the categories indexed (instead of the subcategories) but keep the subcategories searchable via the searchController.
I don't know how to implement it properly with my code.
Here's my code:
CategoryController
class CategoryController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UISearchResultsUpdating {
private var searchController = UISearchController()
let categories = ["Urgent", "Important", "Not Important"]
let subcategories = [
Add(category: "Algrebra", categoryImg: #imageLiteral(resourceName: "Algebra.png")),
Add(category: "Biology", categoryImg: #imageLiteral(resourceName: "Biology.png")),
Add(category: "Chemistry", categoryImg: #imageLiteral(resourceName: "Chemistry.png")),
]
private var sectionTitles = [String]()
private var filteredSectionTitles = [String]()
private var sortedCategory = [(key: String, value: [Add])]()
private var filteredCategory = [(key: String, value: [Add])]()
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return table }()
override func viewDidLoad() {
super.viewDidLoad()
//TABLEVIEW
tableView.rowHeight = 50
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.register(CategoryCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.sectionIndexColor = .black
tableView.sectionIndexBackgroundColor = .lightGray
tableView.sectionIndexTrackingBackgroundColor = .gray
tableView.allowsMultipleSelection = false
//SEARCHCONTROLLER
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.searchResultsUpdater = self
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchBar.placeholder = "Search for your category"
self.searchController.hidesNavigationBarDuringPresentation = false
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.navigationItem.title = "Tasks"
navigationController?.navigationBar.prefersLargeTitles = true
self.searchController.searchBar.searchTextField.textColor = .label
let groupedList = Dictionary(grouping: self.subcategories, by: { String($0.category.prefix(1)) })
self.sortedCategory = groupedList.sorted{$0.key < $1.key}
for tuple in self.sortedCategory {
self.sectionTitles.append(tuple.key)
}
}
//VIEWDIDLAYOUT
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
/// TABLEVIEW
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
return self.filteredSectionTitles[section]
} else {
return self.sectionTitles[section]
}
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
return self.filteredSectionTitles
} else {
return self.sectionTitles
}
}
func numberOfSections(in tableView: UITableView) -> Int {
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
return self.filteredSectionTitles.count
} else {
return self.sectionTitles.count
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.searchController.isActive && !self.filteredCategory.isEmpty {
return self.filteredCategory[section].value.count
} else {
return self.sortedCategory[section].value.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for:indexPath) as UITableViewCell
cell.imageView?.contentMode = .scaleAspectFit
if self.searchController.isActive && !self.filteredCategory.isEmpty {
cell.textLabel?.text = self.filteredCategory[indexPath.section].value[indexPath.row].category
cell.imageView?.image = self.filteredCategory[indexPath.section].value[indexPath.row].categoryImg
} else {
cell.textLabel?.text = self.sortedCategory[indexPath.section].value[indexPath.row].category
cell.imageView?.image = self.sortedCategory[indexPath.section].value[indexPath.row].categoryImg
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let currentCell = tableView.cellForRow(at: indexPath)! as UITableViewCell
Add.details.category = (currentCell.textLabel?.text)!
let secondVC = DateController()
navigationController?.pushViewController(secondVC, animated: true)
print(Add.details.category)
}
func updateSearchResults(for searchController: UISearchController) {
guard let text = self.searchController.searchBar.text else {
return
}
let filteredCategory = self.sortedCategory.flatMap { $0.value.filter { $0.category.contains(text) } }
let groupedCategory = Dictionary(grouping: filteredCategory, by: { String($0.category.prefix(1)) } )
self.filteredCategory = []
self.filteredCategory = groupedCategory.sorted{ $0.key < $1.key }
self.filteredSectionTitles = []
for tuple in self.filteredCategory {
self.filteredSectionTitles.append(tuple.key)
}
self.tableView.reloadData()
}
}
CategoryCell
class CategoryCell: UITableViewCell {
var cellImageView = UIImageView()
var cellLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: "cell")
cellImageView.translatesAutoresizingMaskIntoConstraints = false
cellImageView.contentMode = .scaleAspectFit
cellImageView.tintColor = .systemPink
contentView.addSubview(cellImageView)
cellLabel.translatesAutoresizingMaskIntoConstraints = false
cellLabel.font = UIFont.systemFont(ofSize: 20)
contentView.addSubview(cellLabel)
NSLayoutConstraint.activate([
cellImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
cellImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
cellImageView.widthAnchor.constraint(equalToConstant: 44),
cellLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellLabel.leadingAnchor.constraint(equalTo: cellImageView.trailingAnchor, constant: 10),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
Add(DataStruct)
struct Add {
static var details: Add = Add()
var category: String = ""
func getDict() -> [String: Any] {
let dict = ["category": self.category,
] as [String : Any]
return dict
}
}
Couple tips that should help...
First, let's change some naming.
You're using your "Categories" of "Urgent","Important","Not Important" as Sections ... and your "subcategories" would be more accurately described as "Categories".
We can also think of the Sections as perhaps Category Status
So, we'll create an enum
like this:
enum CategoryStatus: Int, CustomStringConvertible {
case urgent
case important
case notimportant
var description : String {
switch self {
case .urgent: return "Urgent"
case .important: return "Important"
case .notimportant: return "Not Important"
}
}
var star : UIImage {
switch self {
case .urgent: return UIImage(named: "star") ?? UIImage()
case .important: return UIImage(named: "halfstar") ?? UIImage()
case .notimportant: return UIImage(named: "emptystar") ?? UIImage()
}
}
}
And we'll add a "status" property to the Category struct:
struct MyCategory {
var name: String = ""
var categoryImg: UIImage = UIImage()
var status: CategoryStatus = .important
}
Now, we can work through the process using "plain language":
So if we start with:
Biology : .important
Chemistry : .urgent
Algebra : .urgent
we can sort on name and get
Algebra : .urgent
Biology : .important
Chemistry : .urgent
then group by status
.urgent
Algebra
Chemistry
.important
Biology
If we have typed "b" in the search field, we start with our sorted ALL list, and filter it:
Algebra : .urgent
Biology : .important
then group by status
.urgent
Algebra
.important
Biology
Another tip: instead of using a "full list" and a "filtered list", along with a bunch of
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
blocks, use a single sorted, filtered and grouped list.
That list will then be set to either A) the FULL list (if there is no search text entered) or B) the Filtered list
Here is a complete example you can try out. I used a bunch of random topics as Categories, and used numbers in circles for each category image, and I used pngs of star, halfstar and emptystar.
Please note this is Example Code Only!. It is not meant to be, and should not be considered to be, "Production Ready":
enum CategoryStatus: Int, CustomStringConvertible {
case urgent
case important
case notimportant
var description : String {
switch self {
case .urgent: return "Urgent"
case .important: return "Important"
case .notimportant: return "Not Important"
}
}
var star : UIImage {
switch self {
case .urgent: return UIImage(named: "star") ?? UIImage()
case .important: return UIImage(named: "halfstar") ?? UIImage()
case .notimportant: return UIImage(named: "emptystar") ?? UIImage()
}
}
}
struct MyCategory {
var name: String = ""
var categoryImg: UIImage = UIImage()
var status: CategoryStatus = .important
}
class CategoryController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UISearchResultsUpdating {
private var searchController = UISearchController()
// array of ALL Categories, sorted by name
private var sortedCategories: [MyCategory] = []
// this will be either ALL items, or the filtered items
// grouped by Status
private var sortedByStatus = [(key: CategoryStatus, value: [MyCategory])]()
private let tableView = UITableView()
private let noMatchesLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.text = "NO Matches"
v.textAlignment = .center
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
var items: [MyCategory] = []
// this will be our list of MyCategory objects (they'll be sorted later)
let itemNames: [String] = [
"Algebra",
"Chemistry",
"Biology",
"Computer Sciences",
"Physics",
"Earth Sciences",
"Geology",
"Political Science",
"Psychology",
"Nursing",
"Economics",
"Agriculture",
"Communications",
"Engineering",
"Foreign Lanuages",
"English Language",
"Literature",
"Libary Sciences",
"Social Sciences",
"Visual Arts",
]
// create our array of MyCategory
// setting every 3rd one to .urgent, .important or .notimportant
for (str, i) in zip(itemNames, 0...30) {
let status: CategoryStatus = CategoryStatus.init(rawValue: i % 3) ?? .important
var img: UIImage = UIImage()
if let thisImg = UIImage(named: str) {
img = thisImg
} else {
if let thisImg = UIImage(systemName: "\(i).circle") {
img = thisImg
}
}
items.append(MyCategory(name: str, categoryImg: img, status: status))
}
// sort the full list of categories by name
self.sortedCategories = items.sorted{$0.name < $1.name}
//TABLEVIEW
tableView.rowHeight = 50
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.sectionIndexColor = .black
tableView.sectionIndexBackgroundColor = .lightGray
tableView.sectionIndexTrackingBackgroundColor = .gray
tableView.allowsMultipleSelection = false
tableView.dataSource = self
tableView.delegate = self
tableView.register(CategoryCell.self, forCellReuseIdentifier: CategoryCell.reuseIdentifier)
tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: MySectionHeaderView.reuseIdentifier)
//SEARCHCONTROLLER
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.searchResultsUpdater = self
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchBar.placeholder = "Search for your category"
self.searchController.hidesNavigationBarDuringPresentation = false
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.navigationItem.title = "Tasks"
navigationController?.navigationBar.prefersLargeTitles = true
self.searchController.searchBar.searchTextField.textColor = .label
// add the no-matches view
noMatchesLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(noMatchesLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
noMatchesLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
noMatchesLabel.heightAnchor.constraint(equalToConstant: 120.0),
noMatchesLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
noMatchesLabel.topAnchor.constraint(equalTo: tableView.frameLayoutGuide.topAnchor, constant: 40.0),
])
noMatchesLabel.isHidden = true
// call updateSearchResults to build the initial non-filtered data
updateSearchResults(for: searchController)
}
/// TABLEVIEW
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: MySectionHeaderView.reuseIdentifier) as! MySectionHeaderView
v.imageView.image = self.sortedByStatus[section].key.star
v.label.text = self.sortedByStatus[section].key.description
return v
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
// first char of each section title
return (sortedByStatus.map { $0.key.description }).compactMap { String($0.prefix(1)) }
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.sortedByStatus.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sortedByStatus[section].value.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CategoryCell.reuseIdentifier, for:indexPath) as! CategoryCell
cell.cellLabel.text = self.sortedByStatus[indexPath.section].value[indexPath.row].name
cell.cellImageView.image = self.sortedByStatus[indexPath.section].value[indexPath.row].categoryImg
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// get Category object from data
let thisCategory: MyCategory = self.sortedByStatus[indexPath.section].value[indexPath.row]
print("selected:", thisCategory.name, "status:", thisCategory.status)
}
func updateSearchResults(for searchController: UISearchController) {
var filteredList: [MyCategory] = []
if let text = self.searchController.searchBar.text, !text.isEmpty {
// we have text to search for, so filter the list
filteredList = self.sortedCategories.filter { $0.name.localizedCaseInsensitiveContains(text) }
} else {
// no text to search for, so use the full list
filteredList = self.sortedCategories
}
// filteredList is now either ALL Categories (no search text entered), or
// ALL Categories filtered by search text
// create a dictionary of items grouped by status
let groupedList = Dictionary(grouping: filteredList, by: { $0.status })
// order the grouped list by status
self.sortedByStatus = groupedList.sorted{$0.key.rawValue < $1.key.rawValue}
// show noMatchesLabel if we have NO matching Categories
noMatchesLabel.isHidden = self.sortedByStatus.count != 0
// reload the table
self.tableView.reloadData()
}
}
// simple cell with image view and label
class CategoryCell: UITableViewCell {
static let reuseIdentifier: String = String(describing: self)
var cellImageView = UIImageView()
var cellLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
cellImageView.translatesAutoresizingMaskIntoConstraints = false
cellImageView.contentMode = .scaleAspectFit
cellImageView.tintColor = .systemPink
contentView.addSubview(cellImageView)
cellLabel.translatesAutoresizingMaskIntoConstraints = false
cellLabel.font = UIFont.systemFont(ofSize: 20)
contentView.addSubview(cellLabel)
NSLayoutConstraint.activate([
cellImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
cellImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
cellImageView.widthAnchor.constraint(equalToConstant: 44),
cellLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellLabel.leadingAnchor.constraint(equalTo: cellImageView.trailingAnchor, constant: 10),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
// simple reusable section header with image view and label
class MySectionHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier: String = String(describing: self)
let imageView = UIImageView()
let label = UILabel()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
imageView.contentMode = .scaleAspectFit
label.font = .systemFont(ofSize: 20.0, weight: .bold)
[imageView, label].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 24.0),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
imageView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 12.0),
label.topAnchor.constraint(equalTo: g.topAnchor),
label.bottomAnchor.constraint(equalTo: g.bottomAnchor),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Here's how it looks when launched:
then, as we type "t" "e" "ra":
Edit
The for (str, i) in zip(itemNames, 0...30) {
block in the example code was just an easy way to generate some example items.
To use this in your code, you would likely do something like this:
let items = [
MyCategory(name: "Algebra", categoryImg: #imageLiteral(resourceName: "Algebra.png"), status: .urgent),
MyCategory(name: "Biology", categoryImg: #imageLiteral(resourceName: "Biology.png"), status: .important),
MyCategory(name: "Chemistry", categoryImg: #imageLiteral(resourceName: "Chemistry.png"), status: .notimportant),
// and so on
]