如何在 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)")
}
}
}
}
我希望这或多或少是清楚的。任何可以添加到这里的人都绝对欢迎。它尚未经过广泛测试,目前仅在一个应用程序中进行过测试。但是,它在那里工作得很好。
我正在尝试按照此处的指南进行操作: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)")
}
}
}
}
我希望这或多或少是清楚的。任何可以添加到这里的人都绝对欢迎。它尚未经过广泛测试,目前仅在一个应用程序中进行过测试。但是,它在那里工作得很好。