Search code examples
iosswift3

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?


Solution

  • 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]
    }
    

    Edit #2:

    After re-reading my answer above, I would suggest an update to the approach. Having an array of section titles and then a separate 2 dimensional array of section/row table data seems fragile. (It would be possible for the count of section titles to be different from teh count of sections in the 2 dimensional arrays.)

    Instead, I would suggest creating a struct for each section (call it SectionData) which would contain a section title and an array of row data for that section. Then I would create an array of SectionData.

    That way you don't have to maintain two separate arrays and keep their item counts in sync.

    struct SectionData: {
       var sectionTitle: String
       var rowData: [RowData]
    }
    
    struct RowData {
        var rowString: String
        var rowValue: Int // Some value that you display for each row
        // More row data could go here
    }
    
    var sections = [SectionData]()