Как мне прочитать данные, используя GRDB в приложении SwiftUI? - PullRequest
0 голосов
/ 07 апреля 2020

Я пытаюсь следовать этому руководству здесь: https://elliotekj.com/2019/12/11/sqlite-ios-getting-started-with-grdb и хотя это полезно, это не совсем учебник.

Пока у меня есть этот код:

База данных приложений

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, поэтому я знаю, что мой стол в порядке. Но теперь, что мне нужно сделать, чтобы иметь возможность просто читать мою таблицу?

Вот мой 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 () или где разместить его на мой взгляд. Сейчас я был бы рад заполнить свои текстовые поля из таблицы, но, конечно, я бы хотел добавить людей, использующих кнопку.

Ответы [ 2 ]

2 голосов
/ 18 апреля 2020

В настоящее время я работаю над пакетом, который облегчает доступ к GRDB из SwiftUI. См. Пример демонстрационного проекта .

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
    }
0 голосов
/ 09 мая 2020

Хорошо, теперь, когда у меня было время углубиться в это, вот решение, которое я нашел.

Предполагая, что база данных уже присоединена, я создал файл envSetting.swift для хранения ObservableObject. Вот тот файл, который, как мне кажется, довольно понятен (это базовые настройки c 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 возвращает массив объектов Athlete. Он находится в файле Athlete.swift, основная часть которого взята из демонстрационного приложения GRDB с указанными c изменениями и функциями для моего случая:

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

И не забудьте добавить environemnt к 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()
        }
    }

Для меня это работает, пока, после ограниченного тестирования. Я не уверен, что это лучший способ go.

По сути, мы настраиваем ObservableObject для запроса файла БД каждый раз, когда мы вносим в него соответствующие изменения. Вот почему вы видите, что я вызываю функцию env.updateAthletes() в .onDelete и действие кнопки «Готово» для AddAthlete ().

В противном случае я не уверен, как сообщить SwiftUI, что БД изменилась. В GRDB действительно есть какой-то код наблюдения, но для меня очень непонятно, как его использовать, или даже если это правильное решение здесь.

Я надеюсь, что это полезно для людей.

...