Adding Index List and Section Headers to Translated Tableview

Adding index list and section headers to translated tableview

There are a few ways to accomplish this. One of the simplest is to use the delegate functions provided with UITableView.

For organizational purposes I'd start by creating a class to represent sections. Something like this:

class GhostSection {

// a title for this section
var sectionTitle: String

// a list of items for this particular section
var items: [Ghost] = []

init(title: String, items: [Ghost]) {
sectionTitle = title
self.items = items
}
}

Then update the list variable to use the sections instead of a raw list of items:

var tabledata = [GhostSections]()

Ideally I'd create my GhostSection from a json object, but if it must be done manually then something like this would work:

// setup the table data to use GhostSection instances for each section
var tabledata = [
GhostSection(title: "a", items: [Ghost(name:"an".localized, sort:"sort.an".localized, id:"0101")]),
GhostSection(title: "b", items: [Ghost(name:"bc".localized, sort:"sort.bc".localized, id:"0102")]),
GhostSection(title: "c", items: [Ghost(name:"cd".localized, sort:"sort.cd".localized, id:"0103")])
]

I'd have numberOfSections(in tableView: UITableView) return the tabledata count, which would be the count of the total number of sections:

func numberOfSections(in tableView: UITableView) -> Int {
return tabledata.count
}

and the numberOfRowsInSection return the list count for each items array on each GhostSection in your tabledata array:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tabledata[section].items.count
}

Then I'd update cellForRow(at indexPath) to return the info for the right cell in that section:

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

let cell = tableView.dequeueReusableCell(withIdentifier: "library", for: indexPath)
// this gets the section data, then gets the item from that section
var data = list[indexPath.section].items[indexPath.row]
cell.textLabel?.text = data.name
return cell

}

I also like to create my headers as reusable nib views, so the ListHeaderView just has a title label in it:

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

// this loads the view from the nib
guard let view = ListHeaderView.fromNib() as? ListHeaderView else {
return nil
}

// get the section object for this current section
let data = tabledata[section]

// and set the title label text here from our section object
view.titleLabel.text = data.sectionTitle
return view
}

// if you want to also have a custom footer you can do that here...
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return UIView()
}

// and let the header height be dynamic, or return a set height here...
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}

In case you want to use this approach here's how I implemented the fromNib() function so you can instantiate any view that has an associated .xib file:

public extension UIView {

public static func fromNib() -> UIView? {
let nibName = String(describing: self)
let bundle = Bundle(for: self)
let nib = UINib(nibName: nibName, bundle: bundle)
guard let view = nib.instantiate(withOwner: nil, options: nil).first as? UIView else {
print("Unable to instantiate nib named: \(nibName) in bundle: \(String(describing: bundle)) -- nib: \(String(describing: nib))")
return nil
}
return view
}
}

Also note it's a good idea to get in the habit of using camel case for class/object names with the first letter capitalized IF you want to keep with current conventions. So instead of naming your class ghost get in the habit of doing Ghost. For variables the convention is also camel case but start with a lowercase.

Customizing Section indexes in UITableView in iphone application

It doesn't look like standard index view is customizable.

In my application I just created custom index view instead of standard one. Basically all you need to do here is track touch position in that view and scroll UITableView accordingly. You may also need to add some visual effects - change view's background color on touch and highlight current section title.

How to separate cells into sections with custom header?

just set your your tableview functions like and you'll have no problem setting things up by section

   func numberOfSections(in tableView: UITableView) -> Int {
return brandNames.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let brand = brandNames[section]
return groupedItems[brand]!.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cartCell = tableView.dequeueReusableCell(withIdentifier: "CartCell") as! CartCell

let brand = brandNames[indexPath.section]
let itemsToDisplay = groupedItems[brand]![indexPath.row]
cartCell.configure(withItems: itemsToDisplay.cart)

return cartCell
}

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cartHeader = tableView.dequeueReusableCell(withIdentifier: "CartHeader") as! CartHeader

let headerTitle = brandNames[section]
cartHeader.brandName.text = "Brand: \(headerTitle)"

return cartHeader
}

UILocalizedIndexedCollation in this use case

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)

how to retrieve all visible table section header views

The problem with using indexPathsForVisibleRows is that it doesn't include sections without any rows. To get all visible section, including empty sections you have to check the rect of the section and compare it to the contentOffset of the table.

You also have to pay attention to the difference between plain style with floating sections and grouped style without floating sections.

I made a category that support this calculation:

@interface UITableView (VisibleSections)

// Returns an array of NSNumbers of the current visible section indexes
- (NSArray *)indexesOfVisibleSections;
// Returns an array of UITableViewHeaderFooterView objects of the current visible section headers
- (NSArray *)visibleSections;

@end

@implementation UITableView (VisibleSections)

- (NSArray *)indexesOfVisibleSections {
// Note: We can't just use indexPathsForVisibleRows, since it won't return index paths for empty sections.
NSMutableArray *visibleSectionIndexes = [NSMutableArray arrayWithCapacity:self.numberOfSections];
for (int i = 0; i < self.numberOfSections; i++) {
CGRect headerRect;
// In plain style, the section headers are floating on the top, so the section header is visible if any part of the section's rect is still visible.
// In grouped style, the section headers are not floating, so the section header is only visible if it's actualy rect is visible.
if (self.style == UITableViewStylePlain) {
headerRect = [self rectForSection:i];
} else {
headerRect = [self rectForHeaderInSection:i];
}
// The "visible part" of the tableView is based on the content offset and the tableView's size.
CGRect visiblePartOfTableView = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.bounds.size.width, self.bounds.size.height);
if (CGRectIntersectsRect(visiblePartOfTableView, headerRect)) {
[visibleSectionIndexes addObject:@(i)];
}
}
return visibleSectionIndexes;
}

- (NSArray *)visibleSections {
NSMutableArray *visibleSects = [NSMutableArray arrayWithCapacity:self.numberOfSections];
for (NSNumber *sectionIndex in self.indexesOfVisibleSections) {
UITableViewHeaderFooterView *sectionHeader = [self headerViewForSection:sectionIndex.intValue];
[visibleSects addObject:sectionHeader];
}

return visibleSects;
}

@end

Section header title lost when customize the section header view

When you implement viewForHeaderInSection, titleForHeaderInSection isn’t called at all. So you need to set the title of your header view in viewForHeaderInSection. You probably need to create a UILabel.

You must also implement heightForHeaderInSection.



Related Topics



Leave a reply



Submit