Using an excel document as a database/dictionary in Swift

Very new to all this but I'm trying to make an excel document work as almost a dictionary where in the first column becomes the key for the answer in the next column. I found something on a different forum that I couldn't understand but it also seems more complex than what I'm trying to do.

I guess I have 3 questions.

  1. Does a database have to be on the cloud or does it just refer to a literally

  2. If i have 200 terms i want to link to 200 phrases do all of those have to be actually in the code or can the code just be told to read an excel or excel type document and setup a dictionary accordingly.

  3. Does this need to be a dictionary or a database and what is the difference? (The only other small point is whether dictionaries can have one key linked to many answers)

The code I previously found is linked here

var theDatabase = YourDatabase() // whatever is your database model

// read the file as one big string var fileContents:String do { fileContents = try String(contentsOfFile: destinationPath, encoding: String.Encoding.utf8) } catch let error { print("Error reading file: (error.localizedDescription)") fileContents = "" }

guard fileContents.count>0 else { return theDatabase }

// split out records (separated by returns) let records = fileContents.split { $0 == "\r" }

// first record is field names, i.e., column titles let fieldNames = findFields(String(records[0]))

// all remaining records are data, so match each with field names of record 0 for k in 1..<records.count { let values = findFields(String(records[k])) var dictionary = [String:String ])() // the first close bracket immediately above was needed to post on forum not in actual code for n in 0..<values.count { dictionary[fieldNames[n]] = values[n] } theDatabase.append(dictionary) // append would be a method in YourDatabase }

credit - DelawareMathGuy

https://developer.apple.com/forums/thread/124621

Any help would be much appreciated

Answered by AncientCoder in 708381022

The answers to your questions depend what you want to do with the "Dictionary", how volatile it is (i.e. updates, additions, deletions) and how large it is. A few hundred Terms that never change could be handled without a database (i.e. without an indexed, structured permanent store such as SQLite or CoreData). From my understanding of what you're looking for, I'd suggest creating your terms/phrases in a spreadsheet (e.g. Excel) and then exporting the table as a CSV file for use with code like in the SwiftUI sample below:

ContentView

import SwiftUI
import TabularData

struct ContentView: View {
    var model = DataModel.shared
    var body: some View {
        List(model.dataTable.rows,id:\.index) { row in
            HStack{
                Text(row["Term"] as! String)
                Text(String(row["Phrase"] as! String))
            }
        }
    }
}

Data Model - this is not a database: it's where the importing and processing of the terms takes place within the app, and the data have to be loaded into the app again when next run.

import Foundation
import TabularData
class DataModel {
    static let shared = DataModel() 

    @Published var dataTable = DataFrame()

    init() {
        getData()
        let searchTerm = "Nine"
        print("there are \(findTerm(searchTerm).rows.count) terms for \(searchTerm)")
    }

    func getData() {
        var url: URL?  

        do {
             url = try FileManager.default.url(for: FileManager.SearchPathDirectory.downloadsDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
        } catch {
              print("Failed to get Downsloads URL \(error)")
            return
        }
            
        let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")
        let fileURL = url?.appendingPathComponent("TermPhrases.csv")
      
        do {
            dataTable = try DataFrame(contentsOfCSVFile: fileURL!,columns: nil, rows: nil, types: ["Term":CSVType.string,"Phrase":CSVType.string], options: csvOptions)
            } catch {
                print("Failed to load datatable from CSV \(error)")
                return
        }
    }

    func findTerm(_ searchTerm: String) -> DataFrame.Slice {
        let results = dataTable.filter ({ row in
            guard let term = row["Term"] as? String else {
                return false
            }
            if term == searchTerm {
                return true
            }
            return false
        })
        return results
    }
}

extension DataFrame.Rows : RandomAccessCollection {
}

Thiis sample uses the TabularData Framework which has many features for importing and manipulating data from CSV files (spreadsheet output), but also for Machine Learning. This sample app was written for MacOS using csv input from the Downloads folder:

Sample Data in a file called TermPhrases.csv

Term Phrase

Nine A stitch in time saves these

Two A bird in the hand is worth these in the bush

Seven Pillars of wisdom

The findTerm function in the DataModel shows how to filter (Search) the Data Table for a term. DataFrame filtering is not like normal Swift array filtering.

Hopefully this gets you started, then you can decide what other processing you'd like to do on the Data Table (i.e. your dictionary)

Best wishes and regards, Michaela

Accepted Answer

The answers to your questions depend what you want to do with the "Dictionary", how volatile it is (i.e. updates, additions, deletions) and how large it is. A few hundred Terms that never change could be handled without a database (i.e. without an indexed, structured permanent store such as SQLite or CoreData). From my understanding of what you're looking for, I'd suggest creating your terms/phrases in a spreadsheet (e.g. Excel) and then exporting the table as a CSV file for use with code like in the SwiftUI sample below:

ContentView

import SwiftUI
import TabularData

struct ContentView: View {
    var model = DataModel.shared
    var body: some View {
        List(model.dataTable.rows,id:\.index) { row in
            HStack{
                Text(row["Term"] as! String)
                Text(String(row["Phrase"] as! String))
            }
        }
    }
}

Data Model - this is not a database: it's where the importing and processing of the terms takes place within the app, and the data have to be loaded into the app again when next run.

import Foundation
import TabularData
class DataModel {
    static let shared = DataModel() 

    @Published var dataTable = DataFrame()

    init() {
        getData()
        let searchTerm = "Nine"
        print("there are \(findTerm(searchTerm).rows.count) terms for \(searchTerm)")
    }

    func getData() {
        var url: URL?  

        do {
             url = try FileManager.default.url(for: FileManager.SearchPathDirectory.downloadsDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
        } catch {
              print("Failed to get Downsloads URL \(error)")
            return
        }
            
        let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")
        let fileURL = url?.appendingPathComponent("TermPhrases.csv")
      
        do {
            dataTable = try DataFrame(contentsOfCSVFile: fileURL!,columns: nil, rows: nil, types: ["Term":CSVType.string,"Phrase":CSVType.string], options: csvOptions)
            } catch {
                print("Failed to load datatable from CSV \(error)")
                return
        }
    }

    func findTerm(_ searchTerm: String) -> DataFrame.Slice {
        let results = dataTable.filter ({ row in
            guard let term = row["Term"] as? String else {
                return false
            }
            if term == searchTerm {
                return true
            }
            return false
        })
        return results
    }
}

extension DataFrame.Rows : RandomAccessCollection {
}

Thiis sample uses the TabularData Framework which has many features for importing and manipulating data from CSV files (spreadsheet output), but also for Machine Learning. This sample app was written for MacOS using csv input from the Downloads folder:

Sample Data in a file called TermPhrases.csv

Term Phrase

Nine A stitch in time saves these

Two A bird in the hand is worth these in the bush

Seven Pillars of wisdom

The findTerm function in the DataModel shows how to filter (Search) the Data Table for a term. DataFrame filtering is not like normal Swift array filtering.

Hopefully this gets you started, then you can decide what other processing you'd like to do on the Data Table (i.e. your dictionary)

Best wishes and regards, Michaela

In SwiftUI, ContentView is the main user interface, equivalent to Main ViewController in UIKit. The List command (actually a struct in SwiftUI) produces a Table on the UI.

List(model.dataTable.rows,id:\.index) { row in  // this creates a Table of entries from dataTable (i.e. Terms and phrases
            HStack{     // this contains formatting for the rows of the table
                Text(row["Term"] as! String)  // this is the Term "cell", although it's not a cell like in UIKit
                Text(String(row["Phrase"] as! String))  // this is the Phrase cell,  although it's not a cell like in UIKit
            }
        }

That's all that's needed in SwiftUI to create a working UI for your Term/Phrase data. However, in Xcode (when creating a project) you have to make sure that the Interface option for a new project specifies SwiftUI not Storyboard.

With UIKit, processing of data for a View is normally done within the ViewController of the View. With SwiftUI, the data processing is preferably done in a Data Model with results made available to SwiftUI views via (for example) the var model = DataModel.shared statement. So, for the Search Bar, the query text would be provided to the Data Model, which runs the query, then provides the result(s) back to the SwiftUI View.

If I get time later today (it's 8:30am now here in Oz) I'll extend my sample code to include searching.

Best wishes and regards, Michaela

Here's a revised Example App for your situation, where there's now a Search Bar and only search results are shown in the View - not the full set of Terms and Phrases.

ContentView

import SwiftUI
import TabularData
struct ContentView: View {
    @ObservedObject var model = DataModel.shared
    @State private var searchText = ""
    var body: some View {
        NavigationView{
            Text("Searching for \(searchText)")
                            .searchable(text: $searchText)
            List(model.searchResults.rows,id:\.index) { row in
                HStack{
                    Text(row["Term"] as! String)
                    Text(String(row["Phrase"] as! String))
                }
            }
        }
        .onChange(of: searchText) { srchText in
            model.searchResults = model.findTerm(srchText)
        }
    }
}

Data Model

import Foundation
import TabularData
class DataModel : ObservableObject {
    static let shared = DataModel()
    var dataTable = DataFrame()
    @Published var searchResults = DataFrame()
     init() {
        getData()
    }
    func getData() {
        var url: URL? 
        do {
             url = try FileManager.default.url(for: FileManager.SearchPathDirectory.downloadsDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
        } catch {
              print("Failed to get Downsloads URL \(error)")
            return
        }
            
        let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")
        let fileURL = url?.appendingPathComponent("TermPhrases.csv")
        do {
            dataTable = try DataFrame(contentsOfCSVFile: fileURL!,columns: nil, rows: nil, types: ["Term":CSVType.string,"Phrase":CSVType.string], options: csvOptions)
            } catch {
                print("Failed to get load datatable from CSV \(error)")
                return
        }
    }
    func findTerm(_ searchTerm: String) -> DataFrame {
        let results = dataTable.filter ({ row in
            guard let term = row["Term"] as? String else {
                return false
            }
            if term == searchTerm {
                return true
            }
            return false
        })
        return DataFrame(results)
    }
}

extension DataFrame.Rows : RandomAccessCollection {  
}

Caution!! The findTerm func checks for an exact match of the Term, i.e. case sensitive and full match. If you need case insensitivity and partial matches, you need to change the findTerm function.

If your purpose is only to look up phrases for a Term, then this is pretty much a complete solution, although if you've started an Xcode project with View Controllers (Storyboard) then to use my solution you'll need to create a new project with SwiftUI Interface.

Best wishes and regards, Michaela

If your data are static, or change only infrequently, putting them into the app is fine. If the data change, and you are distributing the app to others, you will need to replace the app’s data file, recompile, and re-release the app.  There’s a way of using files from the Documents folder of iOS, or “external” files, but this can be a bit of a learning curve.

Assuming that your data are static, then here’s what to do:

Place your term/phrase csv file into your App’s project folder using MacOS’ Finder, preferably using Copy (not Move), so as to keep a backup copy.

In Xcode, select File -> Add Files to….  From the Top Menu and a pop-up screen will appear

Select your file from the list (others should be greyed out) and make sure that the Destination - copy files if needed box is ticked and the Add to targets box - YourAppName is also ticked

Press the Add button (far right, bottom of pop-up)

Back in Xcode’s main screen, replace the getData function in DataModel with:

func getData() {
        // DatFrame(contentsOfCSVFile) needs a URL reference, so we need to get the app's data (Bundle resource) reference as a URL
        guard let fileURL = Bundle.main.url(forResource: "TermPhrases", withExtension: "csv") else {
        // CAUTION - withExtension is case sensitive, so if your CSV file was created as TermPhrases.CSV then you need to use uppercase in the parameter
            print("**** error getting file url")
            return
        }

        let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")

        do {
            dataTable = try DataFrame(contentsOfCSVFile: fileURL,columns: nil, rows: nil, types: ["Term":CSVType.string,"Phrase":CSVType.string], options: csvOptions)
            } catch {
                print("Failed to load datatable from CSV \(error)")
                return
        }
        print("**** Loaded \(dataTable.rows.count) Terms")  // check to make sure terms got loaded
    }

I haven't tested this with iOS (just on the Mac), but it should work - assuming that you created a new iOS project or multi-platform project with Shared resources.

Best wishes and regards, Michaela

Thank you Again michaela, Works Great And the tips and knowledge are greatly appreciated Cheers

Using an excel document as a database/dictionary in Swift
 
 
Q