Swift 使用MVVM嵌套模型更新存储在Firebase中的数据
我构建了一个MVVM架构来支持我的应用程序,该应用程序应该控制前端和firebase数据库之间的管道。最初,我通过完全在前端编码成功地实现了整个工作,但是当我将它们封装到函数中时,有很多bug 例如,当前提交的工作表被驳回时,将提交下一个工作表。有时我需要等待很长时间,直到应用程序解冻。更糟糕的是,当我点击按钮时,应用程序崩溃了 我听说如果使用SwiftUI(),嵌套模型还不能工作。然而,如果我的类未经测试,我就无法找到更好的解决方案Swift 使用MVVM嵌套模型更新存储在Firebase中的数据,swift,firebase,mvvm,swiftui,nested,Swift,Firebase,Mvvm,Swiftui,Nested,我构建了一个MVVM架构来支持我的应用程序,该应用程序应该控制前端和firebase数据库之间的管道。最初,我通过完全在前端编码成功地实现了整个工作,但是当我将它们封装到函数中时,有很多bug 例如,当前提交的工作表被驳回时,将提交下一个工作表。有时我需要等待很长时间,直到应用程序解冻。更糟糕的是,当我点击按钮时,应用程序崩溃了 我听说如果使用SwiftUI(),嵌套模型还不能工作。然而,如果我的类未经测试,我就无法找到更好的解决方案 // This is Model import Founda
// This is Model
import Foundation
import SwiftUI
struct userModel {
var uid = UUID()
var name = ""
var bio = ""
var interest = ""
var level = 1
var xp = 0
var email = ""
var image: Data = Data(count: 0)
init() {
}
init(_ name:String, _ xp: Int) {
self.name = name
self.xp = xp
self.level = self.xp2Level(xp: xp)
}
func xp2Level(xp:Int) -> Int {
if xp < 9500 {
return xp / 500 + 1
}
else if xp < 29500 {
return (xp - 9500) / 1000 + 1
}
else {
return (xp - 29500) / 2000 + 1
}
}
}
//这是视图
导入快捷键
导入CoreData
进口火基
导入FirebaseFirestore
结构目标:视图{
让current_user_id=Auth.Auth().currentUser?.uid
@国家私有变量showingAlert=false
var ref=Firestore.Firestore()
@StateObject变量currentUser:userViewModel
@StateObject变量homeData=HomeViewModel()
@State var txt=“”
@State var edge=UIApplication.shared.windows.first?.safeAreaInsets
@FetchRequest(实体:Goal.entity(),SortDescriptor:[NSSortDescriptor(键:“日期”,
升序:true)],动画:.spring())变量结果:FetchedResults
//让timer=timer.publish(每隔:1,在:.main上,在:.common中)。自动连接()
@状态私有变量问候语:String=“Hello”
@环境(\.managedObjectContext)变量上下文
var body:一些观点{
ForEach(results){goal in
按钮(操作:{
删除(目标)
试试!context.save()
如果当前用户id!=nil{
currentUser.updatepnlv()
self.showinglert=true
}
},标签:文本(“某物”)
)
.警报(显示:$showingAlert){
()->警报进入
警报(标题:文本(“恭喜!”),消息:文本(“您今天完成了一个目标,XP+50!”),解雇按钮:。默认值(文本(“确定”))
}
}
}
}
编辑
我看到的另一个错误是AttributeGraph前置条件失败:attribute未能设置初始值:805912,ForEachChild。
AppStorage
用于视图
中,而它可能会在视图的结构
之外的可观察对象
中运行所有SwiftUI包装,但@已发布的
除外
类
和结构
都应该大写,因此更改类CurrentUserViewModel
和类UserInfoModel
@StateObject var currentUser:currentUserViewModel
更改为@StateObject var currentUser:currentUserViewModel=currentUserViewModel()
初始化是缺少的最重要的部分
ForEach
中的所有内容都是浮动的,它不在变量、函数或正文中。你的身体在哪里
var body: some View {
//All the code for the ForEach
}
按钮
似乎缺少标题
或标签
()->中的警报
视图中从头开始
,然后开始逐行输入代码
这是一个起点。Firebase部分需要大量的工作,但您应该能够通过关注我注释掉的代码并删除代码来模拟Firebase的响应来开始
所有这些都在FirebaseManager类中
一旦这起作用,其余的都会起作用
代码按原样工作,这样您就可以看到它与假响应一起工作
///Keep all the Firebase Code HERE
class FirebaseManager{
//private var ref = Firestore.firestore()
//private let store = Storage.storage().reference()
func retrieveFromDB(collectionName: String, variableName: String, completion: @escaping (Result<Any, Error>) -> Void) {
print(#function)
//This is sample code, likely has errors because I dont have Firebase setup but you can see the logic so you can touch up
// guard let uid = Auth.auth().currentUser?.uid else {
// completion(.failure(FirebaseError.notLoggedIn))
// return
// }
// catch the information of the current user
// let db = ref.collection(collectionName)
// db.addSnapshotListener { [self] (querySnapshot, error) in
//
// if let error = error{
// completion(.failure(error))
// return
// }
// guard (querySnapshot?.documents) != nil else {
// print("Document is empty")
// completion(.failure(FirebaseError.emptyDocument))
// return
// }
// let docRef = db.(document(uid)
//
// docRef.getDocument { (snapshot, error) in
// if let error = error{
// completion(.failure(error))
// return
// }
//
// completion(.success(snapshot.get(variableName)))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
if variableName == "xp" || variableName == "level"{
completion(.success(Int.random(in: 0...200)))
}else{
let strings = ["apple", "orange", "banana", "kiwi", "startfruit"]
completion(.success(strings.randomElement()!))
}
}
}
///For Int variables
func retrieveFromUsers(variableName: String, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromDB(collectionName: "Users", variableName: variableName, completion: {result in
switch result {
case .success(let value):
let xp = value as? Int
if xp != nil{
completion(.success(xp!))
}else{
completion(.failure(FirebaseError.wrongType))
}
return
case .failure(let error):
print(error)
completion(.failure(error))
}
})
}
///For String variables
func retrieveUserProperty(variableName: String, completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveFromDB(collectionName: "Users", variableName: variableName, completion: {result in
switch result {
case .success(let value):
let username = value as? String
if username != nil{
completion(.success(username!))
}else{
completion(.failure(FirebaseError.wrongType))
}
return
case .failure(let error):
print(error)
completion(.failure(error))
}
})
}
func retrieveXP(completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromUsers(variableName: "xp", completion: completion)
}
func retrieveLevel(completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromUsers(variableName: "level", completion: completion)
}
func retrieveName (completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "username", completion: completion)
}
func retrieveBio (completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "bio", completion: completion)
}
func retrieveInterest (completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "interest", completion: completion)
}
//Database code to retrieve Image needs to be added
func updateDB(collectionName: String, variableName: String, incrementBy: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: FieldValue.increment(incrementBy)])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
completion(.success(Int.random(in: 0...200) + incrementBy))
}
}
func updateDB(collectionName: String, variableName: String, value: String, completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: value])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
let strings = ["apple", "orange", "banana", "kiwi", "startfruit"]
completion(.success(strings.randomElement()!))
}
}
func updateDB(collectionName: String, variableName: String, value: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: value])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response
DispatchQueue.main.async {
completion(.success(Int.random(in: 0...200)))
}
}
func updateUsers(variableName: String, value: String, completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, value: value, completion: completion)
}
func updateUsers(variableName: String, value: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, value: value, completion: completion)
}
func updateUsers(variableName: String, incrementBy: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, incrementBy: incrementBy, completion: completion)
}
//Code to update Image will need to be added
}
///将所有Firebase代码保留在此处
类FirebaseManager{
//private var ref=Firestore.Firestore()
//private let store=Storage.Storage().reference()
func retrieveFromDB(collectionName:String,variableName:String,completion:@escaping(Result)->Void){
打印(#功能)
//这是示例代码,可能有错误,因为我没有Firebase设置,但您可以查看逻辑,以便进行润色
//guard let uid=Auth.Auth().currentUser?.uid else{
//完成(.failure(firebasererror.notLoggedIn))
//返回
// }
//捕获当前用户的信息
//设db=ref.collection(collectionName)
//db.addSnapshotListener{[self](querySnapshot,错误)位于
//
//如果let error=error{
//完成(.failure(error))
//返回
// }
//警卫(querySnapshot?.documents)!=无其他{
//打印(“文档为空”)
//完成(.failure(FirebaseError.emptyDocument))
//返回
// }
//让docRef=db。(文档(uid)
//
//docRef.getDocument{(快照,错误)位于
//如果let error=error{
//完成(.failure(error))
//返回
// }
//
var body: some View {
//All the code for the ForEach
}
///Keep all the Firebase Code HERE
class FirebaseManager{
//private var ref = Firestore.firestore()
//private let store = Storage.storage().reference()
func retrieveFromDB(collectionName: String, variableName: String, completion: @escaping (Result<Any, Error>) -> Void) {
print(#function)
//This is sample code, likely has errors because I dont have Firebase setup but you can see the logic so you can touch up
// guard let uid = Auth.auth().currentUser?.uid else {
// completion(.failure(FirebaseError.notLoggedIn))
// return
// }
// catch the information of the current user
// let db = ref.collection(collectionName)
// db.addSnapshotListener { [self] (querySnapshot, error) in
//
// if let error = error{
// completion(.failure(error))
// return
// }
// guard (querySnapshot?.documents) != nil else {
// print("Document is empty")
// completion(.failure(FirebaseError.emptyDocument))
// return
// }
// let docRef = db.(document(uid)
//
// docRef.getDocument { (snapshot, error) in
// if let error = error{
// completion(.failure(error))
// return
// }
//
// completion(.success(snapshot.get(variableName)))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
if variableName == "xp" || variableName == "level"{
completion(.success(Int.random(in: 0...200)))
}else{
let strings = ["apple", "orange", "banana", "kiwi", "startfruit"]
completion(.success(strings.randomElement()!))
}
}
}
///For Int variables
func retrieveFromUsers(variableName: String, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromDB(collectionName: "Users", variableName: variableName, completion: {result in
switch result {
case .success(let value):
let xp = value as? Int
if xp != nil{
completion(.success(xp!))
}else{
completion(.failure(FirebaseError.wrongType))
}
return
case .failure(let error):
print(error)
completion(.failure(error))
}
})
}
///For String variables
func retrieveUserProperty(variableName: String, completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveFromDB(collectionName: "Users", variableName: variableName, completion: {result in
switch result {
case .success(let value):
let username = value as? String
if username != nil{
completion(.success(username!))
}else{
completion(.failure(FirebaseError.wrongType))
}
return
case .failure(let error):
print(error)
completion(.failure(error))
}
})
}
func retrieveXP(completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromUsers(variableName: "xp", completion: completion)
}
func retrieveLevel(completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromUsers(variableName: "level", completion: completion)
}
func retrieveName (completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "username", completion: completion)
}
func retrieveBio (completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "bio", completion: completion)
}
func retrieveInterest (completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "interest", completion: completion)
}
//Database code to retrieve Image needs to be added
func updateDB(collectionName: String, variableName: String, incrementBy: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: FieldValue.increment(incrementBy)])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
completion(.success(Int.random(in: 0...200) + incrementBy))
}
}
func updateDB(collectionName: String, variableName: String, value: String, completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: value])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
let strings = ["apple", "orange", "banana", "kiwi", "startfruit"]
completion(.success(strings.randomElement()!))
}
}
func updateDB(collectionName: String, variableName: String, value: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: value])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response
DispatchQueue.main.async {
completion(.success(Int.random(in: 0...200)))
}
}
func updateUsers(variableName: String, value: String, completion: @escaping (Result<String, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, value: value, completion: completion)
}
func updateUsers(variableName: String, value: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, value: value, completion: completion)
}
func updateUsers(variableName: String, incrementBy: Int, completion: @escaping (Result<Int, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, incrementBy: incrementBy, completion: completion)
}
//Code to update Image will need to be added
}
import SwiftUI
import CoreData
//Capitalized no other changes here
struct UserModel {
var uid = UUID()
var name = ""
var bio = ""
var interest = ""
var level = 1
var xp = 0
var email = ""
var image: Data = Data(count: 0)
init() {
print(#function)
}
init(_ name:String, _ xp: Int) {
print(#function)
self.name = name
self.xp = xp
self.level = self.xp2Level(xp: xp)
}
func xp2Level(xp:Int) -> Int {
print(#function)
if xp < 9500 {
return xp / 500 + 1
}
else if xp < 29500 {
return (xp - 9500) / 1000 + 1
}
else {
return (xp - 29500) / 2000 + 1
}
}
}
//This is to standardize what comes from your Firebase Code. Try to condense code that is duplicated
enum FirebaseError: Error {
case notLoggedIn
case emptyDocument
case wrongType
case documentDoesntExist
}
Capitalize
class UserViewModel: ObservableObject {
let alertVM = AlertViewModel.shared
@Published var user: UserModel = UserModel()
@Published var isLoading = false
//AppStorage wont work here
var status: Bool{
get{
UserDefaults.standard.bool(forKey: "status")
}
set{
UserDefaults.standard.set(newValue, forKey: "status")
}
}
//Separate all Firebase Code
let firebaseManager = FirebaseManager()
init() {
populateAllVariables()
}
func increaseXPnLV() {
print(#function)
//sending xp to Firebase
firebaseManager.updateUsers(variableName: "xp", incrementBy: 50, completion: {result in
switch result {
case .success(let newXp):
self.user.xp = newXp
//sending level to Firebase
self.firebaseManager.updateUsers(variableName: "level", value: self.user.xp2Level(xp: newXp), completion: {result in
switch result {
case .success(let newLevel):
print("newLevel = \(newLevel)")
self.user.level = newLevel
self.alertVM.scheduleAlert(title: "Congratulations!", message: "You completed a goal today, XP+50!")
return
case .failure(let error as NSError):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
return
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
func populateAllVariables() {
print(#function)
getXP()
getLevel()
getName()
getBio()
getInterest()
}
public func getXP() {
print(#function)
firebaseManager.retrieveXP(completion: {result in
switch result {
case .success(let xp):
self.user.xp = xp
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getLevel() {
print(#function)
firebaseManager.retrieveLevel(completion: {result in
switch result {
case .success(let level):
self.user.level = level
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getName() {
print(#function)
firebaseManager.retrieveName(completion: {result in
switch result {
case .success(let name):
self.user.name = name
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getBio() {
print(#function)
firebaseManager.retrieveBio(completion: {result in
switch result {
case .success(let bio):
self.user.bio = bio
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getInterest() {
print(#function)
firebaseManager.retrieveInterest(completion: {result in
switch result {
case .success(let interest):
self.user.interest = interest
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
///This will need work
// public func getPhoto() -> Data {
// updatePhoto(completion: {
// return (self.user.image) as Data
// })
// }
//It is best to separate work from the View
func deleteGoal(moc: NSManagedObjectContext, goal: Goal) -> Bool{
print(#function)
var result = false
moc.performAndWait {
moc.delete(goal)
do{
try moc.save()
result = true
}catch{
self.alertVM.scheduleAlert(error: error)
result = false
}
}
return result
}
}
//This is to centralize alerts. When you are using the web there will be errors and therefore alerts that the user should be aware of
struct CustomAlert: Identifiable {
let id: UUID = UUID()
let title: String
let message: String
let dismissButtonTitle: String
}
//Again to centralize the alerts. I like putting this on the uppermost View so you can send alerts from anywhere
class AlertViewModel: ObservableObject {
//Singleton keeps everything connected
static let shared: AlertViewModel = AlertViewModel()
@Published var currentAlert: CustomAlert?
private init() {
//Required because you need to share the instance
}
//Use this for a custom message
func scheduleAlert(title: String = "ERROR", message: String, dismissButtonTitle: String = "OK") {
currentAlert = CustomAlert(title: title, message: message, dismissButtonTitle: dismissButtonTitle)
}
//Use this if you have a fully formed Error
func scheduleAlert(error: Error) {
let error = error as NSError
currentAlert = CustomAlert(title: "ERROR", message: (error.localizedFailureReason ?? "") + error.localizedDescription + (error.localizedRecoverySuggestion ?? ""), dismissButtonTitle: "OK")
}
}
struct Goals: View {
//The View should never be aware of where your data is stored. all Firebase code should be removed
@StateObject var currentUser: UserViewModel = UserViewModel()
//This observes the alerts that are sent from anywhere
@StateObject var alertVM: AlertViewModel = AlertViewModel.shared
//No code provided
//@StateObject var homeData = HomeViewModel()
@FetchRequest(entity: Goal.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Goal.date, ascending: true)], animation: .spring()) var results : FetchedResults<Goal>
@Environment(\.managedObjectContext) var context
var body: some View {
VStack{
Text("Name = " + currentUser.user.name.description)
Text("Level = " + currentUser.user.level.description)
Text("XP = " + currentUser.user.xp.description)
ForEach(results){goal in
Button(action: {
//There should be some kind of check here to make sure the goal got deleted
if currentUser.deleteGoal(moc: context, goal: goal){
//No code provided
//if current_user_id != nil {
currentUser.increaseXPnLV()
}
//}
}, label: {
//Missing Brackets
Text("Goal \(goal.name?.description ?? "") Completed")
})
//This gets presented from everywhere
.alert(item: $alertVM.currentAlert, content: {current in
Alert(title: Text(current.title), message: Text(current.message), dismissButton: .cancel(Text(current.dismissButtonTitle)))
})
}
}
}
}