Preload Core Data with data from a CSV file by parsing the CSV at first launch and inserting records using NSManagedObjectContext.
Understanding Core Data Preloading Strategies
Two approaches exist for seeding a Core Data SQLite database. CSV parsing at first launch works for small to medium datasets. Bundling a pre-filled SQLite database works for large datasets with hundreds of thousands of records.
Both approaches check a UserDefaults flag to ensure preloading runs only once. This prevents duplicate records on subsequent launches.
Parsing CSV Files in Swift
CSV files store tabular data with comma-separated values. Simple lines split cleanly with components(separatedBy:). Real-world CSV files contain quoted fields with embedded commas and newlines, requiring character-by-character parsing.
Use Scanner for robust CSV parsing:
func parseCSV(url: URL, encoding: String.Encoding = .utf8) -> [(name: String, detail: String, price: String)]? {
let delimiter = ","
var items: [(name: String, detail: String, price: String)] = []
guard let content = try? String(contentsOf: url, encoding: encoding) else { return nil }
let lines = content.components(separatedBy: .newlines)
for line in lines where !line.isEmpty {
var values: [String] = []
var remaining = line
while !remaining.isEmpty {
if remaining.hasPrefix(""") {
remaining.removeFirst()
let scanner = Scanner(string: remaining)
if let value = scanner.scanUpToString(""") {
values.append(value)
remaining = String(remaining.dropFirst(value.count + 1))
}
} else {
let scanner = Scanner(string: remaining)
if let value = scanner.scanUpToString(delimiter) {
values.append(value)
remaining = String(remaining.dropFirst(value.count))
} else {
values.append(remaining)
remaining = ""
}
}
if remaining.hasPrefix(delimiter) {
remaining.removeFirst()
}
}
if values.count == 3 {
items.append((name: values[0], detail: values[1], price: values[2]))
}
}
return items
}
Preloading Data into Core Data
Create a preload method that clears existing data, parses the CSV, and inserts records:
func preloadData() {
guard let csvURL = Bundle.main.url(forResource: "menudata", withExtension: "csv") else { return }
removeAllMenuItems()
guard let items = parseCSV(url: csvURL) else { return }
let context = persistentContainer.viewContext
for item in items {
let menuItem = MenuItem(context: context)
menuItem.name = item.name
menuItem.detail = item.detail
menuItem.price = Double(item.price) ?? 0.0
}
saveContext()
}
Clear existing data before preloading to avoid duplicates:
func removeAllMenuItems() {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "MenuItem")
let batchDelete = NSBatchDeleteRequest(fetchRequest: fetchRequest)
_ = try? persistentContainer.viewContext.execute(batchDelete)
}
Running Preloading Once with UserDefaults
Call preloading from AppDelegate.application(_:didFinishLaunchingWithOptions:):
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let defaults = UserDefaults.standard
if !defaults.bool(forKey: "dataPreloaded") {
preloadData()
defaults.set(true, forKey: "dataPreloaded")
}
return true
}
Loading CSV from a Remote URL
For frequently changing data, host the CSV file on a remote server and load it asynchronously:
func preloadDataFromRemote() async {
guard let remoteURL = URL(string: "https://example.com/menudata.csv") else { return }
removeAllMenuItems()
guard let items = parseCSV(url: remoteURL) else { return }
let context = persistentContainer.viewContext
for item in items {
let menuItem = MenuItem(context: context)
menuItem.name = item.name
menuItem.detail = item.detail
menuItem.price = Double(item.price) ?? 0.0
}
saveContext()
NotificationCenter.default.post(name: .dataPreloaded, object: nil)
}
Post a notification so the view controller refreshes after the async preload completes.
Bundling a Pre-Filled SQLite Database
For large datasets, prepare a pre-filled SQLite database and bundle it with your app. Run the app once, let Core Data create the database, populate it, then copy the SQLite files from the simulator's documents directory.
Find the database path:
print(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0])
Core Data uses three files: AppName.sqlite, AppName.sqlite-wal, and AppName.sqlite-shm. Copy all three into your Xcode project bundle.
Configure NSPersistentContainer to copy the bundled database on first launch:
lazy var persistentContainer: NSPersistentContainer = {
let storeURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("AppName.sqlite")
if !FileManager.default.fileExists(atPath: storeURL.path) {
let bundleURLs = [
Bundle.main.url(forResource: "AppName", withExtension: "sqlite")!,
Bundle.main.url(forResource: "AppName", withExtension: "sqlite-wal")!,
Bundle.main.url(forResource: "AppName", withExtension: "sqlite-shm")!
]
let destURLs = [
storeURL,
storeURL.appendingPathExtension("sqlite-wal"),
storeURL.appendingPathExtension("sqlite-shm")
]
for (index, bundleURL) in bundleURLs.enumerated() {
try? FileManager.default.copyItem(at: bundleURL, to: destURLs[index])
}
}
let description = NSPersistentStoreDescription(url: storeURL)
let container = NSPersistentContainer(name: "AppName")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Failed to load store: (error)")
}
}
return container
}()
Best Practices for Core Data Preloading
Use batch delete with NSBatchDeleteRequest instead of fetching and deleting individual objects. Batch delete operates directly on the SQLite store without loading objects into memory.
Save periodically during large imports. Calling saveContext() every 100-500 records reduces memory pressure and prevents data loss on crash:
for (index, item) in items.enumerated() {
let menuItem = MenuItem(context: context)
if index % 100 == 0 {
saveContext()
}
}
saveContext()
Run preloading on a private background context. Large imports block the main thread. Use persistentContainer.newBackgroundContext() for non-UI work.
Include all three SQLite files when bundling a pre-filled database. Core Data uses WAL mode by default since iOS 7. The .sqlite-wal and .sqlite-shm files contain uncommitted transactions. Omitting them results in an empty database.
Version your preloaded data. Store a version number in UserDefaults. Increment it when data changes to trigger re-preloading on update:
let currentVersion = 2
let preloadedVersion = defaults.integer(forKey: "dataVersion")
if preloadedVersion < currentVersion {
preloadData()
defaults.set(currentVersion, forKey: "dataVersion")
}
Test on a clean install. Delete the app and reinstall to verify preloading works correctly. Use the Simulator's Erase All Content and Settings for thorough testing.
Summary
This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.
Frequently Asked Questions
What Xcode version do I need for this tutorial? This tutorial uses Xcode 26 with Swift 6.2. The code examples work with any recent version of Xcode that supports the frameworks used here.
Can I test this on the iOS Simulator? Some features require a physical device with a compatible processor. The article notes specific sections where the Simulator is sufficient and where a real device is required.
What is NSManagedObjectContext? NSManagedObjectContext is the Core Data class you use to create, fetch and save managed objects. Changes stay in the context until you call save().
What iOS version is required? This tutorial targets iOS 7. Most of the APIs shown work on earlier versions as well, but some newer APIs may require the version specified in the article.
What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.