TabularData Framework: DataFrame as a List in SwiftUI

The new TabularData framework in iOS 15, MacOS 12 and watchOS 8 opens up opportunities for easier, more efficient ingestion of data (and for ML possibilities). However, it does not appear to be possible to directly use a DataFrame's rows for a List or ForEach in SwiftUI: the compiler gives an error that the rows do not conform to RandomAccessCollection Protocol. The documentation for Rows does not state compliance with such, even through there are methods which inherit from the protocol.

Using a separate iteration to extract each Row (as a DataFrame.Row) from the DataFrame into a new array works. I make this array identifiable by using the row.index as the id. However, if the DataFrame is large this adds considerably to storage and processing overheads.

Any thoughts on directly using the DataFrame's Rows in SwiftUI?

Ah, fixed it :). All I needed to do was add protocol compliance in an extension for DataFrame.Rows

extension DataFrame.Rows : RandomAccessCollection { }.

Then use the index property of rows as the id in the SwiftUI ForEach (or List) with yourDataFrame.rows as the source, handling the optionals as required.

Cheers, Michaela

Here's a working example for Mac OS (but should be the same for iOS except for the URL setup for the incoming csv file).

The csv test data are in 3 columns, with headers of "Name", "Position" and "Score" - so as to test data types of String, Integer and Double.

The Data Model (ViewModel)

import Foundation
import TabularData

class DataModel {
    static let shared = DataModel()

    @Published var dataTable: DataFrame?

    init() {
        getData()
    }

     func getData() {
        var url: URL?
        do {
             url = try FileManager.default.url(for: FileManager.SearchPathDirectory.downloadsDirectory, in:     FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
        } catch{
              print("Failed to get Downsloads URL \(error)")
            return
        }

        let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")
        let fileURL = url?.appendingPathComponent("TestCSV.csv")
        
        do {
            dataTable = try DataFrame(contentsOfCSVFile: fileURL!,columns: nil, rows: nil, types: ["Name":CSVType.string,"Position":CSVType.integer,"Score":CSVType.double], options: csvOptions)
            } catch {
                print("Failed to get load datatable from CSV \(error)")
                return
        }
    }
}

extension DataFrame.Rows : RandomAccessCollection {
}

SwiftUI ContentView

import SwiftUI
import TabularData

struct ContentView: View {
    var model = DataModel.shared
    var body: some View {
        List(model.dataTable!.rows,id:\.index) { row in
            HStack{
                Text(row["Name"] as! String)
                Text(String(row["Position"] as! Int))
                Text(String(row["Score"] as! Double))
            }
        }
    }
}

With forced unwrap (as!) the app will crash if the processed column does not contain the correct type, so I tend to use a function that checks and returns a string - which then gets used in the View:


// in Data Model
func columnToString(_ column: Any) -> String {
        if let str = column as? String {
            return str
        }
        if let int = column as? Int {
            return String(int)
        }
        if let double = column as? Double {
            return String(double)
        }
        return ""
    }

// a row in ContentView
Text(model.columnToString(row["Position"] as Any))

I hope this helps. Regards, Michaela

TabularData Framework: DataFrame as a List in SwiftUI
 
 
Q