UILocalizedIndexedCollation in this use case
Asked Answered
B

1

5

How to add section headers and index list to UITableView in this use case?

@IBOutlet var tableView: UITableView!

var detail: Detail? = nil
var list = [tabledata]()
let search = UISearchController(searchResultsController: nil)

override func viewDidLoad() {

    super.viewDidLoad()
    list = [

        tabledata(name:"something".localized, sort:"sort.something".localized, id:"something.html"),
        tabledata(name:"somethingelse".localized, sort:"sort.somethingelse".localized, id:"somethingelse.html"),
        ...

    ]

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "library", for: indexPath)
    var data: tabledata
    data = list[indexPath.row]
    cell.textLabel!.text = data.name
    return cell

}

Now the point is that table data are going to be translated.

Note that

  • name: is the actual cell name going to be .localized
  • sort: has to help characters like á é etc. in cell name to sort properly (avoiding them to show in the end of alphabet)
  • id: calls the html file location to display in detailViewController ('cause name has to be translated and we want a static text here)

A usual implementation of section headers and index list will result in something like

T                 // section header
translation       // cell names
transmission
...

T                 // table in
Übersetzung       // another language
Getriebe
...

What's the correct model for UILocalizedIndexedCollation?


.xcodeproj on my github. More info on demand.

Thanks for help!

Bullivant answered 4/8, 2018 at 14:17 Comment(1)
@matt I've got it running based on your tutorial with my tabledata list. but I am not able to use search bar. when I use search bar, I can scroll to the next letter and than got crash. do you have time to help me with that? :)Enliven
D
6

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:

English

Italian:

Italian

Slovak:

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)

Dishwater answered 10/8, 2018 at 4:15 Comment(12)
can you make a pull request to my GitHub maybe? I've got Cannot assign value of type 'TableRow' to type 'ghost?' in controller.result = selectedEnliven
so now everything is working (thanks!) except segue to the detail view controller (maybe I am missing something)Enliven
I did change result property of the detail controller to TableRow. Did you make the same change? Also, can you elaborate on “not working”Dishwater
and is it hard to implement index list with this model?Enliven
Very easy indeed. You already have all the sections and their titles. Away from a computer right now so can't type up an edit. Will look at it laterDishwater
I miss only one function to properly show the list. If you can help me then, will be awesome :) thanksEnliven
only that now I've got Use of unresolved identifier 'collation' in let sortedRows = collation.sortedArray ... Am I missing something again? :)Enliven
My bad... missed adding that to the view controller class. let collation = UILocalizedIndexedCollation.current()Dishwater
sorry I am too exhausting... Now it works! only that it shows the whole alphabet, is there a way to get only used letters (like in sections)?Enliven
I considered that option and thought you don't want it. If you do, edit the organizeIntoSections with an extra command just before the return: sections.removeAll(where: { $0.rows.isEmpty })Dishwater
Perfect answer. I'm sorry I do not have more bounty, as it was definitely worth more...Enliven
Thanks. You gave a bounty so I have to make the answer worth it.Dishwater

© 2022 - 2024 — McMap. All rights reserved.