如何在 SwiftUI 应用程序中使用 GRDB 读取数据?

How do I read data using GRDB in a SwiftUI application?

我正在尝试按照此处的指南进行操作:https://elliotekj.com/2019/12/11/sqlite-ios-getting-started-with-grdb 虽然它很有帮助,但不完全是教程。

到目前为止我有这个代码:

AppDatabase

import GRDB

var dbQueue: DatabaseQueue!

class AppDatabase {

    static func setup(for application: UIApplication) throws {
        let databaseURL = try FileManager.default
            .url(for: .applicationDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent("db.sqlite")

        dbQueue = try DatabaseQueue(path: databaseURL.path)
    }
}

在我的 AppDelegate 中,此代码:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        try! AppDatabase.setup(for: application)
        return true
    }

认为以上是正确的。目前,我正在通过 Navicat 操作我的数据库,所以我知道我的 table 没问题。但是现在我需要做什么才能简单地阅读我的 table?

这是我的 SwiftUI ContentView:


import SwiftUI
import GRDB

struct ContentView: View {

    @State private var firstName: String = "Saul"
    @State private var dateOfBirth: String = "1992-05-12"

    var body: some View {
        ZStack {
            VStack{
                HStack {
                    Text("Name")
                    Spacer()
                    TextField(" Enter text ", text: $firstName)
                    .frame(width: 160, height: 44)
                    .padding(4)
                    .border(Color.blue)
                }.frame(width:300)
            HStack {
                Text("Date of Birth")
                Spacer()
                TextField(" Enter text ", text: $dateOfBirth)
                .frame(width: 160, height: 44)
                .padding(4)
                .border(Color.blue)
                }.frame(width:300)
            }.foregroundColor(.gray)
                .font(.headline)
            VStack {
                Spacer()
                Button(action: {


                }) {
                    Text("Add").font(.headline)
                }
                .frame(width: 270, height: 64)
                .background(Color.secondary).foregroundColor(.white)
                .cornerRadius(12)
            }
        }
    }
}

private func readPerson() {



}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct Person {
    var personID: Int64?
    var firstName: String
    var lastName: String?
    var dateOfBirth: String
}

extension Person: Codable, FetchableRecord, MutablePersistableRecord {
    // Define database columns from CodingKeys
    private enum Columns {
        static let personID = Column(CodingKeys.personID)
        static let firstName = Column(CodingKeys.firstName)
        static let lastName = Column(CodingKeys.lastName)
        static let dateOfBirth = Column(CodingKeys.dateOfBirth)
    }



    // Update a person id after it has been inserted in the database.
    mutating func didInsert(with rowID: Int64, for column: String?) {
        personID = rowID
    }
}

我真的不明白在 readPerson() 中写什么或在我的视图中将它放在什么地方。现在,我很乐意从 table 填充我的文本字段,但当然,我想 使用按钮添加人员。

我目前正在开发一个包,可以轻松地从 SwiftUI 访问 GRDB。请参阅 demo project 示例用法。

https://github.com/apptekstudios/AS_GRDBSwiftUI

在您的模型中:

extension YourModelType
{
    // You could define multiple different request types as needed
    struct Request: GRDBFetchRequest
    {
        var maxCount = 100 //An example of how you can make this request configurable.
        func onRead(db: Database) throws -> [YourModelType] {
            let models = try YourModelType
                .limit(maxCount)
                .fetchAll(db)
            return models
        }
    }

在您看来:

struct YourView: View
{
    @GRDBFetch(request: YourModelType.Request(maxCount: 20))
    var result

    var body: some View {
        //Use the result as needed here
    }

好的,现在我有时间深入研究这个问题,这是我找到的解决方案。

假设已经附加了一个数据库,我创建了一个 envSetting.swift 文件来保存 ObservableObject。这是那个文件,我觉得它是不言自明的(这是一个基本的 ObservableObject 设置,请参阅 https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects):

import UIKit
import GRDB

class EnvSettings: ObservableObject {
    @Published var team: [Athlete] = getAthletes(withQuery: "SELECT * FROM Athlete ORDER BY lastName")
    func updateAthletes() {
        team = getAthletes(withQuery: "SELECT * FROM Athlete ORDER BY lastName")
    }

}

在此代码中,getAthletes 函数 returns 一个 Athlete 对象的数组。它驻留在一个 Athlete.swift 文件中,其中大部分来自 GRDB 演示应用程序,具有针对我的案例的特定编辑和功能:

import SwiftUI
import GRDB

// A plain Athlete struct
struct Athlete {
    // Prefer Int64 for auto-incremented database ids
    var athleteID: Int64?
    var firstName: String
    var lastName: String
    var dateOfBirth: String
}

// Hashable conformance supports tableView diffing
extension Athlete: Hashable { }

// MARK: - Persistence
// Turn Player into a Codable Record.
// See https://github.com/groue/GRDB.swift/blob/master/README.md#records
extension Athlete: Codable, FetchableRecord, MutablePersistableRecord {
    // Define database columns from CodingKeys
    private enum Columns {
        static let id = Column(CodingKeys.athleteID)
        static let firstName = Column(CodingKeys.firstName)
        static let lastName = Column(CodingKeys.lastName)
        static let dateOfBirth = Column(CodingKeys.dateOfBirth)
    }

    // Update a player id after it has been inserted in the database.
    mutating func didInsert(with rowID: Int64, for column: String?) {
        athleteID = rowID
    }
}

// MARK: - Database access
// Define some useful player requests.
// See https://github.com/groue/GRDB.swift/blob/master/README.md#requests
extension Athlete {
    static func orderedByName() -> QueryInterfaceRequest<Athlete> {
        return Athlete.order(Columns.lastName)
    }
}


// This is the main function I am using to keep state in sync with the database. 

func getAthletes(withQuery: String) -> [Athlete] {
    var squad = [Athlete]()
    do {
    let athletes = try dbQueue.read { db in
        try Athlete.fetchAll(db, sql: withQuery)
        }
        for athlete in athletes {
            squad.append(athlete)
            print("getATHLETES: \(athlete)")// use athlete
        }
    } catch {
       print("\(error)")
    }
    return squad
}

func addAthlete(fName: String, lName: String, dob: String) {
    do {
        try dbQueue.write { db in
            var athlete = Athlete(
                firstName: "\(fName)",
                lastName: "\(lName)",
                dateOfBirth: "\(dob)")
            try! athlete.insert(db)
        print(athlete)
        }
    } catch {
        print("\(error)")
    }
}

func deleteAthlete(athleteID: Int64) {
    do {
        try dbQueue.write { db in
            try db.execute(
                literal: "DELETE FROM Athlete WHERE athleteID = \(athleteID)")
            }
        } catch {
            print("\(error)")
    }
}


//This code is not found in GRDB demo, but so far has been helpful, though not 
//needed in this Whosebug answer. It allows me to send any normal query to 
//my database and get back the fields I need, even - as far as I can tell - from 
//`inner joins` and so on.

func fetchRow(withQuery: String) -> [Row] {

    var rs = [Row]()

    do {
        let rows = try dbQueue.read { db in
            try Row.fetchAll(db, sql: withQuery)
        }
        for row in rows {
            rs.append(row)
        }
    } catch {
        print("\(error)")
    }
   return rs
}

这是我的 ContentView.swift 文件:

import SwiftUI

struct ContentView: View {

    @EnvironmentObject var env: EnvSettings

    @State var showingDetail = false

    var body: some View {

        NavigationView {
            VStack {
                List {
                    ForEach(env.team, id: \.self) { athlete in
                        NavigationLink(destination: DetailView(athlete: athlete)) {
                            HStack {
                                Text("\(athlete.firstName)")
                                Text("\(athlete.lastName)")
                            }
                        }
                    }.onDelete(perform: delete)
                }
                Button(action: {
                    self.showingDetail.toggle()
                }) {
                    Text("Add Athlete").padding()
                }.sheet(isPresented: $showingDetail) {

                 //The environmentObject(self.env) here is needed to avoid the         
                 //Xcode error "No ObservableObject of type EnvSettings found.
                 //A View.environmentObject(_:) for EnvSettings may be missing as
                 //an ancestor of this view which will show when you try to 
                 //dimiss the AddAthlete view, if this object is missing here. 

                    AddAthlete().environmentObject(self.env)

                }
            }.navigationBarTitle("Athletes")
        }
    }

    func delete(at offsets: IndexSet) {
        deleteAthlete(athleteID: env.team[(offsets.first!)].athleteID!)
        env.updateAthletes()
      }

}


struct AddAthlete: View {
    @EnvironmentObject var env: EnvSettings
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @State private var firstName: String = ""
    @State private var lastName: String = ""
    @State private var dob: String = ""

    var body: some View {
        VStack {
            HStack{
                Button(action: {
                    self.presentationMode.wrappedValue.dismiss()
                }) {
                    Text("Cancel")
                }
                Spacer()
                Button(action: {
                    addAthlete(fName: self.firstName, lName: self.lastName, dob: self.dob)
                    self.env.updateAthletes()
                    self.presentationMode.wrappedValue.dismiss()

                }) {
                    Text("Done")
                }
            }
            .padding()
            VStack (alignment: .leading, spacing: 8) {
                Text("First Name:")
                TextField("Enter first name ...", text: $firstName).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Last Name:")
                TextField("Enter last name ...", text: $lastName).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Date Of Birth:")
                TextField("Enter date of birth ...", text: $dob).textFieldStyle(RoundedBorderTextFieldStyle())
            }.padding()
            Spacer()
        }
    }
}

struct DetailView: View {
   let athlete: Athlete
    var body: some View {
        HStack{
            Text("\(athlete.firstName)")
            Text("\(athlete.lastName)")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(EnvSettings())
    }
}

并且不要忘记将环境添加到 SceneDelegate:

 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).


        //  HERE

        var env = EnvSettings()

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)

        // AND HERE ATTACHED TO THE contentView

            window.rootViewController = UIHostingController(rootView: contentView.environmentObject(env))
            self.window = window
            window.makeKeyAndVisible()
        }
    }

对我来说,到目前为止,经过有限的测试后,这是可行的。我不确定这是最好的方法。

本质上,我们正在设置 ObservableObject 以便在我们对其进行相关更改时随时查询数据库文件。这就是为什么你看到我在 .onDelete 和 "Done" 按钮操作中为 'AddAthlete()' 调用 env.updateAthletes() 函数。

否则我不确定如何让 SwiftUI 知道数据库已更改。 GRDB 确实有某种观察代码在进行,但对我来说如何使用它真的、真的不透明,或者即使这是正确的解决方案。

希望对大家有所帮助。

SwiftUI 应用生命周期

这是一个更新,显示了在“SWiftUI 应用程序生命周期”中使用 GRDB 的一种方法

首先,创建一个类似这样的环境对象:

import UIKit
import GRDB

class EnvSettings: ObservableObject {
    @Published var players: [Player] = getData(withQuery: "SELECT * FROM Player")
}

其次,在您的 App 结构中使用 @StateObject,如下所示:

import SwiftUI

    @main
    struct myNewApp: App {

        @StateObject private var env = EnvSettings()

    //I initialize here with a modified setupDataBase function,
    //removing the AppDelegate paramters

        init() {
            try! setupDatabase()
          }
            
        var body: some Scene {
            WindowGroup {
                StartView()
                    .environmentObject(env) // Don't forget to add the environmentObject here
            }
        }
    }

如上所述,我修改setupDatabase()如下,去掉_ application: UIApplication参数:

import Foundation
import GRDB

func setupDatabase() throws {
     
     let path = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] as String
     let url = NSURL(fileURLWithPath: path)
     if let pathComponent = url.appendingPathComponent("db.sqlite") {
         let filePath = pathComponent.path
         let fileManager = FileManager.default
         if fileManager.fileExists(atPath: filePath) {
             print("FILE AVAILABLE")
             try openGSdb()
         } else {
             print("FILE NOT AVAILABLE")
             try fileManager.copyfileToApplicationSupportDirectory(forResource: "db", ofType: "sqlite")
             try openDatabase()
         }
     } else {
         print("FILE PATH NOT AVAILABLE")
     }
 }
 
 // MARK: Open the databaseQueue
 
 func openDatabase() throws {
     let databaseURL = try FileManager.default
         .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
         .appendingPathComponent("db.sqlite")
     dbQueue = try AppDatabase.openDatabase(atPath: databaseURL.path)
 }


将您的数据库文件放入应用程序需要与我在上面的回答中相同的 FileManager 扩展名:

extension FileManager {
    //lets us copy in the database, creating the Application Support Directory if needed.
    func copyfileToApplicationSupportDirectory(forResource name: String,
                                         ofType ext: String) throws
    {
        
        let fileManager = FileManager.default
        let urls = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)
        if let applicationSupportURL = urls.last {
            do{
                try fileManager.createDirectory(at: applicationSupportURL, withIntermediateDirectories: true, attributes: nil)
                print("WE ARE CREATING THE APPLICATION SUPPORT FOLDER ...")
                print("APP SUPPORT FOLDER PATH:\n\(applicationSupportURL.path)")
            }
            catch{
                print(error)
            }
        }
        
        if let bundlePath = Bundle.main.path(forResource: name, ofType: ext),
            let destPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory,
                                .userDomainMask,
                                true).first {
            let fileName = "\(name).\(ext)"
            let fullDestPath = URL(fileURLWithPath: destPath)
                                   .appendingPathComponent(fileName)
            let fullDestPathString = fullDestPath.path

            if !self.fileExists(atPath: fullDestPathString) {
                try self.copyItem(atPath: bundlePath, toPath: fullDestPathString)
                print("WE COPIED THE FILE OVER")
                print("BUNDLE:\n\(bundlePath)")
                print("DEST:\n\(fullDestPath.path)")
            }
        }
    }
}


我还有一个 AppDatabase.swift 文件:


import GRDB

// The shared database queue
var dbQueue: DatabaseQueue!

/// A type responsible for initializing the application database.
///
/// See AppDelegate.setupDatabase()
struct AppDatabase {
    
    /// Creates a fully initialized database at path
    static func openDatabase(atPath path: String) throws -> DatabaseQueue {
        // Connect to the database
        // See https://github.com/groue/GRDB.swift/blob/master/README.md#database-connections
        let dbQueue = try DatabaseQueue(path: path)
        print("OPEN DATABASE")
        print("DATAEBASE PATH:\n\(path)")
        // Define the database schema
        try migrator.migrate(dbQueue)
        
        return dbQueue
    }
    
    /// The DatabaseMigrator that defines the database schema.
    ///
    /// See https://github.com/groue/GRDB.swift/blob/master/README.md#migrations
    static var migrator: DatabaseMigrator {
        var migrator = DatabaseMigrator()
        print("TRYING MIGRATOR")
        
 // removed database specific code, see the Demo in GRDB ...

        
        return migrator
    }
}

然后,在实际的视图结构中,你可以这样做:

struct ContentView: View {

    @EnvironmentObject var env: EnvSettings

    var body: some View {
        VStack {
            ForEach(players, id: \.id) { player in
                Text("\(player.name)")
            }
        }
    }
}

我希望这或多或少是清楚的。任何可以添加到这里的人都绝对欢迎。它尚未经过广泛测试,目前仅在一个应用程序中进行过测试。但是,它在那里工作得很好。