Help Loading an External CSV File on iOS in Swift

Hi everyone! I’m fairly new to Swift and currently working on a small iOS app in SwiftUI. The app is able to load a CSV file embedded in the Xcode project (using Bundle.main.path(forResource:)), and everything works well with that.

Now, I want to take it a step further by allowing the app to load an external CSV file located in the iPhone’s directories (like “Documents” or “Downloads”). However, I’m struggling to make it work. I tried using a DocumentPicker to select the CSV file, and I believe I’m passing the file URL correctly, but the app keeps reading only the embedded file instead of the one selected by the user.

Could anyone offer guidance on how to properly set up loading an external CSV file? I’m still learning, so any suggestions or examples would be really appreciated!

Thanks a lot in advance for the help!

Here’s the code that isn’t working as expected:

import Foundation

struct Product: Identifiable {
    let id = UUID()
    var codice: String
    var descrizione: String
    var prezzo: Double
    var installazione: Double
    var trasporto: Double
}

class ProductViewModel: ObservableObject {
    @Published var products: [Product] = []
    @Published var filteredProducts: [Product] = []
    
    func loadCSV(from url: URL) {
        products = []
        
        do {
            let data = try String(contentsOf: url)
            let lines = data.components(separatedBy: "\n")
            
            // Legge e processa ogni riga del CSV (saltando la prima riga se è l'intestazione)
            for line in lines.dropFirst() {
                let values = line.components(separatedBy: ";")
                
                // Assicurati che ci siano abbastanza colonne e gestisci i valori mancanti
                if values.count >= 5 {
                    let codice = values[0].trimmingCharacters(in: .whitespaces)
                    let descrizione = values[1].trimmingCharacters(in: .whitespaces)
                    
                    let prezzo = parseEuropeanDouble(values[2]) ?? 0.0
                    let installazione = parseEuropeanDouble(values[3].isEmpty ? "0,00" : values[3]) ?? 0.0
                    let trasporto = parseEuropeanDouble(values[4].isEmpty ? "0,00" : values[4]) ?? 0.0
                    
                    let product = Product(
                        codice: codice,
                        descrizione: descrizione,
                        prezzo: prezzo,
                        installazione: installazione,
                        trasporto: trasporto
                    )
                    products.append(product)
                }
            }
            
            filteredProducts = products
        } catch {
            print("Errore nel caricamento del CSV: \(error)")
        }
    }
    
    private func parseEuropeanDouble(_ value: String) -> Double? {
        let formatter = NumberFormatter()
        formatter.locale = Locale(identifier: "it_IT")
        formatter.numberStyle = .decimal
        return formatter.number(from: value)?.doubleValue
    }
}

struct ContentView: View {
    @StateObject var viewModel = ProductViewModel()
    @State private var showFilePicker = false
    
    var body: some View {
        VStack {
            Button("Carica file CSV") {
                showFilePicker = true
            }
            .fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.commaSeparatedText]) { result in
                switch result {
                case .success(let url):
                    viewModel.loadCSV(from: url)
                case .failure(let error):
                    print("Errore nel caricamento del file: \(error.localizedDescription)")
                }
            }
            
            List(viewModel.filteredProducts) { product in
                VStack(alignment: .leading) {
                    Text("Codice: \(product.codice)")
                    Text("Descrizione: \(product.descrizione)")
                    Text("Prezzo Lordo: €\(String(format: "%.2f", product.prezzo))")
                    Text("Installazione: €\(String(format: "%.2f", product.installazione))")
                    Text("Trasporto: €\(String(format: "%.2f", product.trasporto))")
                }
            }
        }
        .padding()
    }
}
Answered by DTS Engineer in 812410022

It’s quite hard to read your question because you put all of the text into a code block. (Funnily enough, that’s the opposite of the mistake that folks normally make! :-) See Quinn’s Top Ten DevForums Tips for hints and tips on how to format your post nicely.

There’s two parts to my answer:

  • Parsing CSVs

  • Accessing the CSV file itself

Regarding the first, iOS includes a nice API for handling CSV files, the TabularData framework. See TabularData Resources for links to docs and other resources.

Regarding the main issue, using .fileImporter(…) is a reasonable option. Looking at your code the only thing that’s obviously wrong is that you don’t bracket your reading code with startAccessingSecurityScopedResource() and stopAccessingSecurityScopedResource(). Without that, you won’t be able to access a file that’s outside of your static sandbox.

For example, you might write code like this:

let url: URL = …
let didStart = url.startAccessingSecurityScopedResource()
defer {
    if didStart {
        url.stopAccessingSecurityScopedResource()
    }
}
let data = try Data(contentsOf: url)

Note that in my example I’m using Data rather than String, because that’s the type you’ll want to start with when using TabularData.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

It’s quite hard to read your question because you put all of the text into a code block. (Funnily enough, that’s the opposite of the mistake that folks normally make! :-) See Quinn’s Top Ten DevForums Tips for hints and tips on how to format your post nicely.

There’s two parts to my answer:

  • Parsing CSVs

  • Accessing the CSV file itself

Regarding the first, iOS includes a nice API for handling CSV files, the TabularData framework. See TabularData Resources for links to docs and other resources.

Regarding the main issue, using .fileImporter(…) is a reasonable option. Looking at your code the only thing that’s obviously wrong is that you don’t bracket your reading code with startAccessingSecurityScopedResource() and stopAccessingSecurityScopedResource(). Without that, you won’t be able to access a file that’s outside of your static sandbox.

For example, you might write code like this:

let url: URL = …
let didStart = url.startAccessingSecurityScopedResource()
defer {
    if didStart {
        url.stopAccessingSecurityScopedResource()
    }
}
let data = try Data(contentsOf: url)

Note that in my example I’m using Data rather than String, because that’s the type you’ll want to start with when using TabularData.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I was able to load a CSV into a TabularData based SwiftUI app -- loading and displaying was no issue ... but, HOW to update a value in a column.

I created a bindable textfield for the columns in the table, and it displays the value for the rows/columns, and while the code to update the column compiles, it never updates the value in the DataFrame.

Is a DataFrame read only ?

Help Loading an External CSV File on iOS in Swift
 
 
Q