2
votes

I am trying to integrate an SQLite database (with SQLite.swift) into my project (since my unrecovered apparent limitations of CoreData referenced here.

Ive got a helper file with a class that I instantiate to do all of the heavy lifting with regards to SQLite queries.

I can not figure out how to get that data, let's say a simple list of the contents of the database, into a ForEach statement in SwiftUI.

I have tried to set a return from the function that runs the query, but its return type is AnySequence<Row>, and I'm not sure what to do with that. When I try to return it after the data is fetched, I get errors about not returning is on failure (understandably so). Don't know how to return a blank AnySequence<Row> in that case. See code below.

Any thoughts?

Code from helper class (built following some of the instructions at Kilo Loco's YouTube video:


import Foundation
import SQLite



class DataModel {

    var database: Connection!
    let usersTable = Table("users")
    let id = Expression<Int>("id")
    let name = Expression<String>("name")
    let email = Expression<String>("email")
    var dataDict = [Dictionary<String, String>()]

    init() {

        do {

            let documentDirectory = try FileManager.default.url(for: .applicationSupportDirectory, in: .allDomainsMask, appropriateFor: nil, create: true)
            let fileUrl = documentDirectory.appendingPathComponent("users").appendingPathExtension("sqlite3")
            let database = try Connection(fileUrl.path)
            self.database = database
            print("Database initialized at path \(fileUrl)")
        } catch {
            print(error)
        }

    }


    func createTable() {

        print("Create Tapped")

        let createTable = self.usersTable.create { (table) in
            table.column(self.id, primaryKey: true)
            table.column(self.name)
            table.column(self.email, unique: true)
        }

        do {

            try self.database.run(createTable)
            print("Table Created")

        } catch {
            print(error)
        }

    }


    func insertRecord(name: String, email: String) {

        let insertRecord = self.usersTable.insert(self.name <- name, self.email <- email)

        do {

            try self.database.run(insertRecord)
            print("record added")
        } catch {
            print(error)
        }

    }

    func listUsers() {

        print("Listed Users")

        do {

            let users = try self.database.prepare(self.usersTable)

            for user in users {
                print("User ID: \(user[self.id]), UserName: \(user[self.name]), Email: \(user[self.email])")

            }

        } catch {
            print(error)
        }
    }
}

and simple SwiftUI content View


import SwiftUI

let myData = DataModel()

struct ContentView: View {

    @State var name: String = ""
    @State var email: String = ""

    var body: some View {


        VStack {

            TextField("Enter Name", text: self.$name)
            TextField("Enter email", text: self.$email)

            Button(action: {
                myData.insertRecord(name: self.$name.wrappedValue, email: self.$email.wrappedValue)
            }) {
                Text("Insert Record")
            }

            Button(action: {
                myData.createTable()
            }) {
                Text("Create Table")
            }


            Button(action: {
                myData.listUsers()
            }) {
                Text("List users")
            }
        }
    }
}

In theory, what I would like to be able to do is something like in my VStack:

ForEach (myData.listUsers, id: \.self) { record in 
      Text("UserName \(record.userName)")
}

I was trying to have the listUsers function return its result so it could be used like this, but that did not work out so well..

Update #1

Trying to have the function return the data, then I can iterate over it in the SwiftUI View. I was trying this:

    func listUsers() -> (Int, String, String) {
        print("Listed Users")
        do {
            let users = try self.database.prepare(self.usersTable)
            for user in users {
                print("User ID: \(user[self.id]), UserName: \(user[self.name]), Email: \(user[self.email])")
                return (user[self.id], user[self.name], user[self.email])
            }
        } catch {
            print(error)
        }
        return (0, "No Name", "No Email")
    }

and then in the View I was trying to call:

                let response = myData.listUsers()
                for resp in response {
                    Text(resp.name)
                }

but I then get the error:

Type '(Int, String, String)' does not conform to protocol 'Sequence'

Update #2

Holy C*&@ - What a pain..

I was able to get the function to return something that I can work with, but it is most definitely not the most efficient way to do this. Any idea how I can do it better:

My function now is:

    func listUsers() ->  [Dictionary<String,Any>]  {
        print("Listed Users")
        do {
            let users = try self.database.prepare(self.usersTable)
            for user in users {
                print("User ID: \(user[self.id]), UserName: \(user[self.name]), Email: \(user[self.email])")
                dataDictArray.append(["id":user[self.id], "name":user[self.name], "email":user[self.email]])
            }
        } catch {
            print(error)
        }
        return dataDictArray
    }


}

Which returns an Array of Dictionary of String:Any (because I'm mixing String and Int in my database. Then my function looks like:

                let response = myData.listUsers()
                print(response)
                for dicts in response {
                    for record in dicts {
                        if (record.key.datatypeValue == "name") {
                            print("Record Key: \(record.key)")
                            print("Record Value: \(record.value)")
                        }
                    }
                }
                }) {
                     Text("List users")
                 }

So what its doing, which is ridiculous, is looping through the array, then looping through the dictionary to get the key value pairs..

It just feels cumbersome and overly complicated.

  • Still can't figure out how to get it into the actual view. I can print the output to the log, but not in a Text() tag..
1

1 Answers

3
votes

Ive discovered how to do this:

in my SQLite worker file, I needed to create a struct to represent the data model:

struct TestDataModel: Hashable {
    let id: Int
    let name: String
    let email: String
}

Then in my helper class, my function loads the data into an array of TestDataModel objects. The function then returns the array like this:

    func listUsers2() ->  [TestDataModel]  {
         print("Listed Users")
        var theseVars = [TestDataModel]()
         do {
             let users = try self.database.prepare(self.usersTable)
             for user in users {
                 print("User ID: \(user[self.id]), UserName: \(user[self.name]), Email: \(user[self.email])")
                theseVars.append(TestDataModel(id: user[self.id], name: user[self.name], email: user[self.email]))
            }
         } catch {
             print(error)
         }
         return theseVars
     }

Then, in my SwiftUI view, I can use the array of TestDataModel objects like normal:

            ForEach (myData.listUsers2(), id: \.self) { record in
                Text(record.email)

            }

This works, not sure if its the most elegant approach, but results are good..