A Swift library that provides a declarative, type-safe approach to converting any object arrays into TabularData DataFrames. TabularBuilder enables seamless data export and analysis by bridging the gap between object-oriented data models and columnar data structures.
- Universal Object Conversion: Convert any object arrays (Core Data entities, structs, classes) to TabularData DataFrames
- Declarative Column Definition: Use SwiftUI-like syntax to define columns with
@TabularColumnBuilder
- Type-Safe Transformations: Leverage Swift's type system for safe data mapping and transformation
- Conditional Logic: Support for conditional column creation and conditional value mapping
- Custom Column Ordering: Precise control over column order and naming
- Value Mapping: Transform values using custom mapping functions before export
- Non-Intrusive: Add DataFrame conversion capability to existing types without modification
- iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+ / visionOS 1.0+
- Swift 6.0+
- Xcode 16.0+
When working with data-intensive applications, there's often a need to convert object collections (like Core Data entities) into structured tabular formats for two primary purposes:
- Data Export: Easily save data as JSON/CSV files for sharing or archival
- Data Analysis: Leverage TabularData's powerful APIs for filtering, aggregation, and statistical operations
While this seems straightforward—converting "row-oriented" data structures to "column-oriented" ones—the reality becomes complex when dealing with dozens of different entity types. Writing individual conversion code for each entity is both tedious and error-prone.
TabularBuilder was created to solve this challenge by providing a universal, type-safe, and declarative approach to DataFrame conversion. Instead of repetitive boilerplate code, you can define your data transformations once and apply them consistently across all your data types.
The library showcases Swift's modern language features including generics, KeyPath, type erasure, protocol extensions, and Result Builders to create an elegant solution that is both powerful and easy to use.
The result? Clean, maintainable code that leverages Swift's powerful type system while providing the flexibility to handle complex data transformation scenarios.
For a detailed exploration of the design philosophy and Swift features that make this library possible, read the full article: Experience the Charm of Swift: One-Click DataFrame Export
Don't miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to Fatbobman's Swift Weekly and receive weekly insights and valuable content directly to your inbox.
Add TabularBuilder to your project through Xcode:
- Go to File → Add Package Dependencies
- Enter the repository URL:
https://github.com/fatbobman/TabularBuilder
- Select the version you want to use
Or add it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/fatbobman/TabularBuilder", from: "0.5.0")
]
import TabularBuilder
struct Student: DataFrameConvertible {
let name: String
let age: Int
let score: Double
let isActive: Bool
}
let students = [
Student(name: "Alice", age: 20, score: 95.5, isActive: true),
Student(name: "Bob", age: 22, score: 87.0, isActive: false),
Student(name: "Charlie", age: 19, score: 92.5, isActive: true)
]
let dataFrame = Student.makeDataFrame(objects: students) {
TabularColumn(name: "Name", keyPath: \.name)
TabularColumn(name: "Age", keyPath: \.age)
TabularColumn(name: "Score", keyPath: \.score, mapping: { "\($0)%" })
TabularColumn(name: "Status", keyPath: \.isActive, mapping: { $0 ? "Active" : "Inactive" })
}
// Export to CSV
try dataFrame.writeCSV(to: URL(fileURLWithPath: "students.csv"))
Transform values during column creation:
let dataFrame = Student.makeDataFrame(objects: students) {
// Convert Int to String with formatting
TabularColumn(name: "Age", keyPath: \.age, mapping: { "\($0) years old" })
// Complex transformations
TabularColumn(name: "Grade", keyPath: \.score, mapping: { score in
switch score {
case 90...100: return "A"
case 80..<90: return "B"
case 70..<80: return "C"
default: return "F"
}
})
}
Apply different mappings based on object properties:
let dataFrame = Student.makeDataFrame(objects: students) {
// Display score differently for active vs inactive students
TabularColumn.conditional(
name: "Performance",
keyPath: \.score,
filter: { $0.isActive },
then: { score in "Active: \(score)%" }, // For active students
else: { score in "Inactive: \(score)%" } // For inactive students
)
}
Create columns only when certain conditions are met:
let dataFrame = Student.makeDataFrame(objects: students) {
TabularColumn(name: "Name", keyPath: \.name)
TabularColumn(name: "Age", keyPath: \.age)
// Only include this column for high-performing student groups
TabularColumn(name: "Honors", keyPath: \.score, mapping: { $0 > 90 ? "Yes" : "No" })
.when { $0.score > 85 } // Only create this column if first student has score > 85
}
Disable columns for certain conditions(when the condition is met, the column will not be created):
let dataFrame = Student.makeDataFrame(objects: students) {
TabularColumn(name: "Name", keyPath: \.name)
TabularColumn(name: "Age", keyPath: \.age)
// Only include this column for high-performing student groups
TabularColumn(name: "Honors", keyPath: \.score, mapping: { $0 > 90 ? "Yes" : "No" })
.disable { $0.score < 85 } // Only create this column if first student has score < 85
}
Handle optional properties gracefully:
struct Person: DataFrameConvertible {
let name: String
let email: String?
let phone: String?
}
let dataFrame = Person.makeDataFrame(objects: people) {
TabularColumn(name: "Name", keyPath: \.name)
TabularColumn(name: "Email", keyPath: \.email, mapping: { $0 ?? "N/A" })
TabularColumn(name: "Contact", keyPath: \.phone, mapping: { $0 ?? "No phone" })
}
struct Order: DataFrameConvertible {
let id: UUID
let amount: Decimal
let date: Date
let items: [String]
}
let dataFrame = Order.makeDataFrame(objects: orders) {
TabularColumn(name: "Order ID", keyPath: \.id, mapping: { $0.uuidString.prefix(8).uppercased() })
TabularColumn(name: "Amount", keyPath: \.amount, mapping: { "$\($0)" })
TabularColumn(name: "Date", keyPath: \.date, mapping: { DateFormatter.shortDate.string(from: $0) })
TabularColumn(name: "Item Count", keyPath: \.items, mapping: { $0.count })
}
The fundamental building block that defines how to extract and transform data from your objects:
// Basic column (no transformation)
TabularColumn(name: "Age", keyPath: \.age)
// Column with transformation
TabularColumn(name: "Formatted Age", keyPath: \.age, mapping: { "\($0) years" })
// Conditional column with different logic paths
TabularColumn.conditional(
name: "Status",
keyPath: \.score,
filter: { student in student.score >= 60 },
then: { score in "Pass (\(score))" },
else: { score in "Fail (\(score))" }
)
Type-erased wrapper that allows columns with different types to be stored together:
let columns: [AnyTabularColumn<Student>] = [
AnyTabularColumn(TabularColumn(name: "Name", keyPath: \.name)), // String column
AnyTabularColumn(TabularColumn(name: "Age", keyPath: \.age)), // Int column
AnyTabularColumn(TabularColumn(name: "Score", keyPath: \.score)) // Double column
]
Result builder that enables declarative column definition syntax:
@TabularColumnBuilder<Student>
var columns: [AnyTabularColumn<Student>] {
TabularColumn(name: "Name", keyPath: \.name)
TabularColumn(name: "Age", keyPath: \.age)
if includeScores {
TabularColumn(name: "Score", keyPath: \.score)
}
// Can also include arrays of columns
additionalColumns
}
Protocol that adds DataFrame conversion capabilities to any type:
extension MyCustomType: DataFrameConvertible {}
// Now you can convert arrays to DataFrames
let dataFrame = MyCustomType.makeDataFrame(objects: myObjects) {
// Column definitions...
}
// Export Core Data entities to CSV
let dataFrame = Person.makeDataFrame(objects: people) {
TabularColumn(name: "Full Name", keyPath: \.name)
TabularColumn(name: "Birth Year", keyPath: \.birthDate, mapping: { Calendar.current.component(.year, from: $0) })
}
try dataFrame.writeCSV(to: exportURL)
// Leverage TabularData's powerful analysis capabilities
let dataFrame = SalesRecord.makeDataFrame(objects: sales) {
TabularColumn(name: "Amount", keyPath: \.amount)
TabularColumn(name: "Quarter", keyPath: \.date, mapping: { getQuarter(from: $0) })
}
// Perform analysis
let totalsByQuarter = dataFrame.grouped(by: "Quarter").sums(on: "Amount")
// Generate reports with conditional formatting
let dataFrame = Employee.makeDataFrame(objects: employees) {
TabularColumn(name: "Name", keyPath: \.name)
TabularColumn.conditional(
name: "Performance",
keyPath: \.rating,
filter: { $0.rating >= 4.0 },
then: { "Excellent (\($0))" },
else: { "Needs Improvement (\($0))" }
)
}
- Type Safety: Leverage Swift's type system to prevent runtime errors
- Expressiveness: Clean, readable syntax inspired by SwiftUI
- Flexibility: Support for complex transformations and conditional logic
- Performance: Efficient conversion with minimal overhead
- Integration: Seamless integration with existing codebases
- Maintainability: Declarative approach makes code easier to understand and modify
TabularBuilder is available under the MIT license. See the LICENSE file for more info.
Contributions are welcome! Please feel free to submit a Pull Request.