Хорошо, теперь, когда у меня было время углубиться в это, вот решение, которое я нашел.
Предполагая, что база данных уже присоединена, я создал файл 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 действительно есть какой-то код наблюдения, но для меня очень непонятно, как его использовать, или даже если это правильное решение здесь.
Я надеюсь, что это полезно для людей.