Edit: it turns out that "getting the first letter of each row to use as the index" is much more complicated than I thought when accounting for multiple languages, especially non-Latin ones. I'm making use of UILocalizedIndexedCollation
to simplify this task.
I think that UILocalizedIndexedCollation
is more confusing than rolling your own data models. You need to 2 models: one to represent the row and one to represent the section:
// UILocalizedIndexedCollation uses a selector on the `name` property
// so we have to turn this data type in to a class.
class TableRow: NSObject {
@objc var name: String
var id: String
var sectionTitle = ""
init(name: String, id: String) {
self.name = name
self.id = id
}
}
// TableSection does not have to be a class but we are making it so
// that it's consistent with `TableRow`
class TableSection: NSObject {
var title: String
var rows: [TableRow]
init(title: String, rows: [TableRow]) {
self.title = title
self.rows = rows
}
}
After that, populating and filtering the table is very easy:
class Library: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet var tableView: UITableView!
var detail: Detail? = nil
var list = [TableSection]()
var filter = [TableSection]()
let search = UISearchController(searchResultsController: nil)
let collation = UILocalizedIndexedCollation.current()
override func viewDidLoad() {
super.viewDidLoad()
// search
search.searchResultsUpdater = self as UISearchResultsUpdating
search.obscuresBackgroundDuringPresentation = false
search.searchBar.placeholder = "search".localized
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.searchController = search
definesPresentationContext = true
// Set the color of the index on the right of the table.
// It's settable from Interface Builder as well
tableView.sectionIndexColor = UIColor(red: 0, green: 122.0 / 255.0, blue: 1, alpha: 1)
// I took the liberty to add a few more items to the array
let rows = ["something", "somethingelse", "apple", "orange", "apricot", "strawberry"].map {
TableRow(name: $0.localized, id: $0)
}
list = organizeIntoSections(rows: rows)
tableView.reloadData()
}
// Organize rows into sections with titles
func organizeIntoSections(rows: [TableRow]) -> [TableSection] {
// Organize the rows into sections based on their `name` property
let selector: Selector = #selector(getter: TableRow.name)
// Sort the rows by `name`
let sortedRows = collation.sortedArray(from: rows, collationStringSelector: selector) as! [TableRow]
// Allocate rows into sections
var sections = collation.sectionTitles.map { TableSection(title: $0, rows: []) }
for row in sortedRows {
let sectionNumber = collation.section(for: row, collationStringSelector: selector)
sections[sectionNumber].rows.append(row)
}
// Remove empty sections
sections.removeAll(where: { $0.rows.isEmpty })
return sections
}
override func viewWillAppear(_ animated: Bool) {
if let selection = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selection, animated: animated)
}
super.viewWillAppear(animated)
}
// MARK: - Table View
func numberOfSections(in tableView: UITableView) -> Int {
return filtering() ? filter.count : list.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// If a section has no row, don't show its header
let data = filtering() ? filter[section] : list[section]
return data.rows.isEmpty ? nil : data.title
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filtering() ? filter[section].rows.count : list[section].rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "library", for: indexPath)
let data = filtering() ? filter[indexPath.section].rows[indexPath.row]
: list[indexPath.section].rows[indexPath.row]
cell.textLabel!.text = data.name
return cell
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return filtering() ? filter.map { $0.title } : list.map { $0.title }
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "transporter" {
if let indexPath = tableView.indexPathForSelectedRow {
let selected = filtering() ? filter[indexPath.section].rows[indexPath.row]
: list[indexPath.section].rows[indexPath.row]
let controller = (segue.destination as! Detail)
// This assumes you change `controller.result` to have type TableRow
controller.result = selected
}
}
}
// search filter
func filterContent(_ searchText: String) {
let query = searchText.lowercased()
filter = list.compactMap { section in
let matchingRows = section.rows.filter { $0.name.lowercased().contains(query) }
return matchingRows.isEmpty ? nil : TableSection(title: section.title, rows: matchingRows)
}
tableView.reloadData()
}
func searchEmpty() -> Bool {
return search.searchBar.text?.isEmpty ?? true
}
func filtering() -> Bool {
return search.isActive && (!searchEmpty())
}
}
Results:
English:
Italian:
Slovak:
(I got the translation from Google Translate so my apology in advance if any word is out of whack -- I cannot speak Italian or Slovak)