Philip Philip - 1 month ago 12
iOS Question

Generating a tableview with an array of an array of String

I have a CSV file which I would like to import into a tableview and it looks as follow.

enter image description here

I would like the Headers Headlight Wiper Bumper and spoiler
to be the Section's Text and the Values of each column listed in underneath each section.

so the TableView should look something like:

Headlight

123

10

Wiper

456

11

Bumper

789

12

spiler

999

888

So far my CSV importer is returning an array of array of String [[String]]

and my code looks as follow

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

@IBOutlet weak var tableView: UITableView!

@IBAction func buttonPressed(_ sender: Any) {
print(bins)
}

var bins = [[String]]()

override func viewDidLoad() {
super.viewDidLoad()

tableView.delegate = self
tableView.dataSource = self

importCsv()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

//MARK: -Table View Delegate & DataSource



func numberOfSections(in tableView: UITableView) -> Int {
return self.bins.count > 0 ? self.bins[0].count : 0
}



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



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


let bin = self.bins[indexPath.row]
cell.textLabel?.text = bin[0]
return cell
}

func importCsv() -> Void {
let path = "/Users/philipchan/Downloads/test.csv"
let defaultImporter = CSVImporter<[String]>(path: path)

defaultImporter.startImportingRecords{ $0 }.onFinish { importedRecords in
self.bins = importedRecords
print(self.bins)
self.tableView.reloadData()
}
}


}

and this is the 2d array I am getting back as of a result of printing to the output.

[["Headlight", "Wiper", "Bumper", "spoiler"], ["123", "456", "789", "999"], ["10", "11", "12", "888"]]


What would be the correct way to render this 2D array in a table view?

Answer

Table views don't display 2D data. They display a 1D list, which may or may not be divided into sections.

If it were me, and I only needed to display 4 columns of data, I'd probably create cells where each cell had 4 labels in it, and a header that showed the column headings, just like the table you show at the beginning of your post. That seems like the clearest way to display your data.

Anyway, on to your question:

Your data is in kind of a screwy format.

It's arranged by row, and then by section inside each row's array. To make things even more confusing, your section titles are at the first position of your array.

Normally you'd want your data with the outer array being the sections and then the inner array containing the data for each row in a given section. That way you could have the number of rows be different for each section.

You should probably "peel off" the section titles, and then feed your table view's data source methods using the remainder of the array. Something like this:

var sectionTitles: [String]!
var tableData: [[String]]!

/**
 This is the input data for our table view. The section titles are in the first array.
 Use a DidSet method so that if the input "bins" variable changes, we format the data
 And reload the table
 */
var bins: [[String]]? {

  //If the bins array changed, parse it's data.
  didSet {
    if let bins = bins {

      //Peel off the section titles into a separate array
      sectionTitles = bins[0]

      //Remove the section titles from the tableData array
      tableData = Array(bins.dropFirst(1))

      //Tell the table view to reload itself.
      tableView.reloadData()
    }
  }
}

override func viewDidLoad() {
  super.viewDidLoad()

  bins = [
    ["Headlight", "Wiper", "Bumper", "Spoiler"],
    ["123", "456", "789", "999"],
    ["10", "11", "12", "888"]
  ]
}

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

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

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
  let row = indexPath.row
  let section = indexPath.section
  cell?.textLabel?.text = tableData[row][section]
  return cell!
}

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

EDIT:

The code above works, but storing your data by row, and by section within each row is just wrong. Table views are designed to have an arbitrary number of sections and then each section can have a different number of rows in that section. When you store your data as an array of rows and section data inside each row you can't do that. You can instead have a given number of rows and then a variable number of sections in each row, which is NOT how table views work.

Given that you're reading your data from a CSV file you might not have much choice about the input format, but you can certainly restructure it to be organized by section, then by row within each section. Here is the equivalent code to the above but restructuring the data in a way that makes more sense:

var sectionTitles: [String]!
var tableData: [[String]]!

/**
 This is the input data for our table view. The section titles are in the first array.
 Use a DidSet method so that if the input "bins" variable changes, we format the data
 And reload the table
 */
var bins: [[String]]? {

  //If the bins array changed, parse it's data.
  didSet {
    if let bins = bins {

      //Peel off the section titles into a separate array
      sectionTitles = bins[0]

      //Remove the section titles from the tableData array
      let inputData  = Array(bins.dropFirst(1))

      //The code below restructures the arrays to be "section major". It assumes the arrays are not
      //"jagged" and will crash if they ARE jagged.

      //Create a new array of arrays for the output
      var output = [[String]]()
      let rows = inputData.count
      let sections = inputData.first?.count ?? 0
      for section in 0..<sections {

        //Create an array for this section
        var aSection = [String]()
        for row in 0..<rows {
          //populate this section with an entry from each row
          aSection.append(inputData[row][section])
        }
        //Add the finished section to the output array
        output.append(aSection)
      }

      tableData = output
      //Tell the table view to reload itself.
      tableView.reloadData()
    }
  }
}

override func viewDidLoad() {
  super.viewDidLoad()

  bins = [
    ["Headlight", "Wiper", "Bumper", "Spoiler"],
    ["123", "456", "789", "999"],
    ["10", "11", "12", "888"]
  ]
}

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

override func tableView(_ tableView: UITableView,
                        numberOfRowsInSection section: Int) -> Int {
  guard tableData.count > section else {
    return 0
  }
  return tableData[section].count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
  let row = indexPath.row
  let section = indexPath.section
  cell?.textLabel?.text = tableData[section][row]
  return cell!
}

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