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()
}
}
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"