iosswiftuitableviewuitableviewsectionheader

UITableView Alphabetical Header Sections Determined by a First Letter of a Model Property


I'm trying to create an UITableView with alphabetical order headers depending on my businessName property's first letter of my Company model, and then hide the header sections if there's not a company in the collection that starts with a letter. I'm already fetching the companies into the controller, but I'm having difficulty with limiting my companies into the right section and then hiding the sections that don't have the businessName's first letter.

// Current situation

enter image description here

// What I'm looking for

enter image description here

// Company Model

`struct Company {

var uid: String
var businessAddress: String
var businessName: String

init(dictionary: [String : Any]) {
    self.uid = dictionary["uid"] as? String ?? ""
    self.businessAddress = dictionary["businessAddress"] as? String ?? ""
    self.businessName = dictionary["businessName"] as? String ?? ""
}

}`

// Service

`struct EmployeeService {

static func fetchCompanies(completion: @escaping([Company]) -> Void) {
    
    var companies = [Company]()
    
    let query = REF_COMPANIES.order(by: "businessName")
    
    query.addSnapshotListener { snapshot, error in
        snapshot?.documentChanges.forEach({ change in
            let dictionary = change.document.data()
            let company = Company(dictionary: dictionary)
            companies.append(company)
            companies.sort {
                $0.businessName < $1.businessName
            }
            completion(companies)
        })
    }
    
}

}`

// CompanySelectionController

`class CompanySelectionController: UIViewController {

// MARK: - Properties

var companies = [Company]()

let sectionTitles = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".map(String.init)

}

// MARK: - UITableViewDataSource

extension CompanySelectionController: UITableViewDataSource {

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

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return companies.count
}

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    // Create the header view
    let headerView =  UIView.init(frame: CGRectMake(0, 0, self.view.frame.size.width, 40))
    headerView.backgroundColor = UIColor.groupTableViewBackground
    
    // Create the Label
    let label = UILabel(frame: CGRectMake(10, -40, 120, 60))
    label.font = UIFont(name: "AvenirNext-DemiBold", size: 16)
    label.textAlignment = .left
    label.text = sectionTitles[section]
    
    // Add the label to your headerview
    headerView.addSubview(label)
    
    return headerView
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 5
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.CompanySelectionCell, for: indexPath) as! CompanySelectionCell
    
    cell.selectionStyle = .none
    cell.company = companies[indexPath.row]
    return cell
}

}`

// CompanySelectionCell

`protocol CompannyViewModelItem {
var type: CompannyViewModelItemType { get }
var rowCount: Int { get }
var sectionTitle: String { get }
}

class CompanySelectionCell: UITableViewCell {

// MARK: - Properties

var company: Company! {
    didSet {
        configure()
    }
}

private let businessNameLabel: UILabel = {
    let label = UILabel()
    label.font = UIFont(name: "AvenirNext-DemiBold", size: 14)
    label.textColor = .black
    label.textAlignment = .left
    return label
}()

private let businessAddressLabel: UILabel = {
    let label = UILabel()
    label.font = UIFont(name: "AvenirNext-MediumItalic", size: 12)
    label.textColor = .darkGray
    label.textAlignment = .left
    return label
}()

lazy var businessStack = UIStackView(arrangedSubviews: [businessNameLabel, businessAddressLabel])

// MARK: - Lifecycle

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

// MARK: - Helper Functions

fileprivate func configure() {
    
    guard let company = company else { return }
    let viewModel = CompannyViewModel(company: company)
    
    businessNameLabel.text = viewModel.businessName
    businessAddressLabel.text = viewModel.businessAddress
    
}

}`

// CompannyViewModel

enum CompannyViewModelItemType { case uid case businessName }

I have tried changing the label property inside viewForHeaderInSection to try and conform to the right letter, but the screenshot of my problem has been the furthest I've got.


Solution

  • Your problem is your datasource for the UITableView. What you're using is actually a static structure with 26 sections (all letters) and you're appending all your companies to every section. That is happening here:

    func numberOfSections(in tableView: UITableView) -> Int {
        return sectionTitles.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return companies.count
    }
    
    

    To show what you actually want you need a different datastructure like a nested Array to fill your table view.

    
    let dataSource: [String: [Company]] = // however you want to read the data in here 
    
    func numberOfSections(in tableView: UITableView) -> Int {
        dataSource.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        datasource[section].count
    }
    

    To create the datasource you'd need to go through all your companies and only fill the letters (section) of the ones actually existing.

    Edit: I'd highly recommend you using AutoLayout to create your views