1

Been trying to solve this for over 10 days now and still haven't had any luck fixing the situation.

I am trying to add sections to the table view in alphabetical order from A-Z based on the names of "ExerciseName" column. Also i have a "BodyPartName", "CategoryName"? column of data for each "ExerciseName".

I want to populate sections from A-Z and have the ExerciseName sorted into each section with BodyPartName & CategoryName for other labels in same row.

I was successful in implementing ExerciseName under sections A-Z, but unable to add BodyPartName & CategoryName into other labels in same row. Please Help!!

Result Expected

A

Abs "(Core)"

Arm Press "(Arms)" "(Dumbbell)"

B

Bicep Curl "(Arms)" "(Barbell)"

Back Extension "(Back)" "(Machine)"

Unable to populate "BodyPartName" label & "CategoryName"label for same row.

This is my Realm DataModel:

class Exercises: Object
{

    @objc dynamic var ExerciseID = 0
    @objc dynamic var ExerciseName = ""
    @objc dynamic var BodyPartName = ""
    @objc dynamic var CategoryName: String? = ""

    //Adding Parent Table BodyPart & Category.
    var parentBodyParts = LinkingObjects(fromType: BodyParts.self, property: "exercises")
    var parentCategory = LinkingObjects(fromType: Category.self, property: "category")

}

My Code:

let realm = try! Realm()
var exerciseArray: Results<Exercises>!

override func viewDidLoad()
    {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "customCell", bundle: nil), forCellReuseIdentifier: cellID)
        sectionFunction()
    }

var sectionDictionary = [String:[String]]()
var sectionTitles = [String]()

func sectionFunction()
{
     exerciseArray = realm.objects(Exercises.self).sorted(byKeyPath: "ExerciseName")

    for section in exerciseArray
    {
        let sectionKey = String(section.ExerciseName.prefix(1)) 

        if var sectionValues = sectionDictionary[sectionKey]
        {
            sectionValues.append(section.ExerciseName) // Adding ExerciseNames to the Keys
            sectionDictionary[sectionKey] = sectionValues
        }
        else
        {
            sectionDictionary[sectionKey] = [section.ExerciseName]
        }
    }
    sectionTitles = [String](sectionDictionary.keys)
    sectionTitles = sectionTitles.sorted(by: {$0 < $1}) // Alphabetical order for Keys:Values
}

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


    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
    {
        return sectionTitles[section]
    }


    override func sectionIndexTitles(for tableView: UITableView) -> [String]?
    {
        return sectionTitles
    }


    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        let sectionKey = sectionTitles[section]
        if let sectionValues = sectionDictionary[sectionKey]
        {
            return sectionValues.count
        }
        return 0
       // return exercises.filter("ExerciseName == %@", sectionNames[section]).count
    }

    // Register custom cell for Table View
    let cellID = "customCell"

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
      let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! customCell

        let sectionKey = sectionTitles[indexPath.section]
        if let sectionValues = sectionDictionary[sectionKey]
        {
          exerciseArray = realm.objects(Exercises.self).sorted(byKeyPath: "ExerciseName")

            cell.exerciseName.text = sectionValues[indexPath.row]
            cell.bodyPartName.text = exerciseArray.filter("ExerciseName == %@", sectionValues[indexPath.row]) [indexPath.row].BodyPartName
            cell.categoryName.text = exerciseArray.filter("ExerciseName == %@", sectionValues[indexPath.row])  [indexPath.row].CategoryName ?? ""
            cell.backgroundColor = .clear
        }
        return cell
    }

Custom Cell:

class customCell: UITableViewCell {

    @IBOutlet weak var exerciseName: UILabel!
    @IBOutlet weak var bodyPartName: UILabel!
    @IBOutlet weak var categoryName: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
    }

override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)   
}   

}

  • Without looking too far into the code a couple of things. There's a comment `Register custom cell for Table View` but the following code doesn't do that; it only sets up a class level cellID *string*. You should, in viewDidLoad, load the custom cell nib and register it to your tableView, something like `self.myTableView.register(myNib, forIdentifier: cellIdentifier)`, which will also avoid having to repeatedly do it in `cellForRowAt`. Secondly code in `cellForRowAt` should be minimal to ensure a responsive UI. So this `let exerciseList = realm.objects(Exercises.self)` should be elsewhere. – Jay Dec 22 '19 at 14:27
  • ...you should be leveraging a tableView datasource for your tableView data, which is a pre-loaded 'list' of items for your tableView, and pull your data from that list, don't hit the disk everytime you need data for your cellview. There are a number of great tutorials on the internet about working with UITableViews which you may want tot take a look at. – Jay Dec 22 '19 at 14:28
  • Thank you for your response @Jay, I tried adding the code to viewdidload: self.tableView.register(customCell.self, forCellReuseIdentifier: "customCell") //.register(myNib, forIdentifier: cellIdentifier) but it returns an empty tableview. – Santhosh Umapathi Dec 23 '19 at 05:36
  • @Jay, but nothing changed to fix my issue. Still populates my Sections with ExerciseNames, but not bodypartName and categoryName – Santhosh Umapathi Dec 23 '19 at 05:43
  • There's a number of issues with that code - too many to really go over in comments but for example, this `cell.bodyPartName.text = exerciseArray.filter("ExerciseName == %@", sectionValues[indexPath.row]) [indexPath.row].BodyPartName` is a bad idea. You don't want to be filtering when trying to populate a tableView. – Jay Dec 24 '19 at 17:43

1 Answers1

2

There are a number of issues with the code in the question and instead of trying to dissect them, let me provide an example that may help.

Suppose we have a bunch of fruits stored in Realm. We want to display those in alphabetical order in a tableview using sections

A
   Apple
B
   Banana

etc

This first piece of code is a structure to hold a section title and then an array of the fruits that go in that section. Also, a class fruitNameArray which will act as our tableView DataSource that holds all of the FruitStructs.

struct FruitStruct {
    var sectionTitle = ""
    var fruitNameArray = [String]()
}

var fruitDataSource = [FruitStruct]()

I am using an array to hold the initial data but in your case it could be a Realm results object populated from a Realm. This is called from viewDidLoad to organize our data into the dataSource array

func setupDataSourceData() {
    let fruitArray = ["Apple", "Pear", "Banana", "Bing Cherry", "Grape", "Orange", "Plum", "Watermelon", "Cantelope"]

    let allFirstChars = fruitArray.map { String($0.prefix(1)) } //get all first chars for section titles
    let sectionTitles = Array(Set(allFirstChars)).sorted() //eliminate dups and sort

    //iterate over the unique section titles and get the fruits that are in that section
    // sort and then craft structs to hold the title and the associated fruits
    sectionTitles.forEach { firstChar in
        let results = fruitArray.filter { $0.prefix(1) == firstChar }
        let sortedFruits = results.sorted()
        let fruit = FruitStruct(sectionTitle: firstChar, fruitNameArray: sortedFruits)
        fruitDataSource.append(fruit)
    }

    for fruitData in fruitDataSource {
        print(fruitData.sectionTitle)
        let fruits = fruitData.fruitNameArray
        for fruitName in fruits {
            print("  \(fruitName)")
        }
    }
}

Then, there are 4 functions a tableView needs to show the sections and then the rows in each section. You appear to have these 4 but an additional one sectionIndexTitles which may not be needed.

//
//handle sections
//
func numberOfSections(in tableView: UITableView) -> Int {
    return self.fruitDataSource.count
}

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    let title = self.fruitDataSource[section].sectionTitle
    return title
}

//
//handleTableView rows
//
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let rowsInSection = self.fruitDataSource[section].fruitNameArray.count
    return rowsInSection
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cellReuseIdentifier", for: indexPath)
    let text = self.fruitDataSource[indexPath.section].fruitNameArray[indexPath.row]
    cell.textLabel?.text = text
    return cell
}

Note that I am not performing any filtering and any other stuff as the tableView is being populated. That section of code needs to be fast and lean to the UI is as responsive as possible.

I am just getting a string

let text = self.fruitDataSource[indexPath.section].fruitNameArray[indexPath.row]

but in your case, you may want to return an exersise object where you could then get the exerciseName and bodypartName to then assign those in the custom cellView

Best Practice Note: class properties should be lower cased - exerciseName, not uppercase, ExerciseName. Class and Struct names are typically capitalized.

Jay
  • 34,438
  • 18
  • 52
  • 81