Parsing CSV natively in Swift

Sun, 16 Aug 2026 17:09:00 +0200

Load and process CSV tables in Swift using only bundled libraries.

Recently I needed to load a large CSV file into an app I was building. There are plenty of open-source Swift libraries to parse CSV, but for these small tasks I prefer to, in order:

1. Use a built-in Apple framework

2. Write it myself

3. Import an external dependency

I didn’t recall there ever being a native Objective-C framework for CSV, but how hard could writing a parser myself be? It’s just splitting a text file by newlines and then by commas.

That works until you encounter optionally quoted fields, header rows, multi-line fields and escaped characters. My CSV had all of these. This easy job was looking more like a distracting side quest, so I balked at writing my own. Before looking for something on GitHub though, I checked the Swift docs. It turns out there is now a native Swift library called TabularData for machine-learning training data. The main class is DataFrame and it supports CSV parsing.

It’s similar to DataFrame in Pandas, the popular Python library for data science. Both types allow sorting, slicing and grouping the table without touching the underlying data, thus are efficient on large datasets.

Using DataFrame

It’s simple enough to load a CSV file using the initialiser DataFrame.init(contentsOfCSVFile:). There are options for loading only a subset of rows or columns or customising the decoding but the default values worked fine.

For this example CSV file:

"id","name","time"
1,"Alice","2007-07-09 08:59:00"
2,"Bob","2007-07-08 16:30:00"
3,"Carol","2007-07-08 11:37:00"
4,"Dan","2007-07-07 00:20:00"

here’s how to inspect and query it:

import TabularData

let url = URL(filePath: "data.csv")

var csv = try DataFrame(contentsOfCSVFile: url)

print(csv.shape)                // (rows: 4, columns: 3)
print(csv.columns.map(\.name))  // ["id", "name", "time"]

csv.sort(on: "name", order: .descending)
print(csv.prefix(1))

/*
Prints:
┏━━━┳━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
┃   ┃ id    ┃ name     ┃ time              ┃
┃   ┃ <Int> ┃ <String> ┃ <Date>            ┃
┡━━━╇━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
│ 0 │     4 │ Dan      │ 6/7/2007, 4:20 pm │
└───┴───────┴──────────┴───────────────────┘
1 row, 3 columns
*/

The types of each column are automatically detected and parsed correctly. However my actual data was not as clean as this example and I ran into a couple of issues.

Gotchas and bugs

While the API for DataFrame is generally simple enough to figure out, there are some features that didn’t work as expected.

Grouping

You can call grouped(by:) with a column name, but it doesn’t return a Sequence that can be iterated over:

let groups = csv.grouped(by: "country")

for group in groups {
    // ⛔️ No 'grouped' overloads produce result type that conforms to 'Sequence'
}

This version of the function returns a RowGroupingProtocol type, but to iterate you need a concrete RowGrouping which is created by providing the type of the column along with the name:

let groups = csv.grouped(by: ColumnID("country", String.self))

for group in groups {
    print(group.key)    // Andorra, Argentina, Australia, Austria, ...
}

Optional types for incomplete data

Column types are very strict. One of the columns in my CSV contained dates but also a few empty cells. The date parsing worked fine until it choked on the first empty value. There’s no way to make the column an optional Date?. Even when I specified CSVReadingOptions.dateParsers which return Date? I couldn’t get it to work. I ended up overriding the column type to plain String and doing the date parsing manually.

Whitespace in header rows

If a header row has spaces around the names then it picks those up as part of the name. A header row containing "id", "name", "time" parses into column names of ["id", " name", " time"]. Note the leading spaces, even though they were outside the quotes. Then calling csv.sort(on: "name") would crash because that column name doesn’t exist as expected. The only workaround for this was to manually edit the header row in the source CSV.

In the end, working around these bugs may have taken longer than writing a basic CSV parser myself. Even so, it’s still worth using a decent first-party framework when possible. It’s less code to maintain and comes with some handy features I wouldn’t have written myself.