使用MVVM嵌套模型更新存储在Firebase中的数据



我构建了一个MVVM体系结构来支持我的应用程序,该应用程序应该控制前端和firebase数据库之间的管道。最初,我完全在前端编码,成功地实现了整个工作,但当我将它们封装到一个函数中时,会有很多错误。

例如,当前呈现的工作表被取消时,将呈现下一个工作表。有时我需要等待很长时间,直到应用程序被解冻。更糟糕的是,当我点击按钮时,应用程序崩溃了。

我听说如果使用SwiftUI,嵌套模型还不能工作(参考(。然而,如果我的课程未经测试,我就无法想出更好的解决方案。

// 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
}
}
}
// This is ViewModel
import Foundation
import SwiftUI
import Firebase
class userViewModel: ObservableObject {
@Published var user: userModel = userModel()

@Published var isLoading = false
@AppStorage("status") var status = false
private var ref = Firestore.firestore()
private let store = Storage.storage().reference()
var picker = false

func updateXP(completion: @escaping () -> Int) -> Int {
guard let uid = Auth.auth().currentUser?.uid else {
return 0
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)

docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.user.xp = xp
}
}
}
return completion()
}

func updateLevel(completion: @escaping () -> Int) -> Int {
guard let uid = Auth.auth().currentUser?.uid else {
return 1
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)

docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let level = doc.get("level") as? Int {
self.user.level = level
}
}
}
return completion()
}

func updateName (completion: @escaping () -> String) -> String {
guard let uid = Auth.auth().currentUser?.uid else {
return ""
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)

docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let name = doc.get("username") as? String {
self.user.name = name
}
}
}
return completion()
}

func updateBio (completion: @escaping () -> String) -> String {
guard let uid = Auth.auth().currentUser?.uid else {
return ""
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)

docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let bio = doc.get("bio") as? String {
self.user.bio = bio
}
}
}
return completion()
}

func updateInterest (completion: @escaping () -> String) -> String {
guard let uid = Auth.auth().currentUser?.uid else {
return ""
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)

docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let interest = doc.get("interest") as? String {
self.user.interest = interest
}
}
}
return completion()
}

func updatePhoto (completion: @escaping () -> Data) -> Data {
guard let uid = Auth.auth().currentUser?.uid else {
return Data(count: 0)
}

// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)

docRef.getDocument { (snapshot, error) in
if snapshot != nil {
let imageRef = store.child("profile_Photos").child(uid)
imageRef.getData(maxSize: 1000 * 64 * 64, completion: { (data, error) in
if let error = error {
print("Encountered error: (error) when getting image")
self.user.image = Data(count: 0)
} else if let data = data,
!data.isEmpty{
//                            self.currentUser.image = Image(uiImage: UIImage(data: data)!).resizable()
self.user.image = data
} else {
//                            self.currentUser.image = Image(systemName: "person").resizable()
self.user.image = Data(count: 0)
}
})
} else if let error = error {
print(error)
}
}
}
return completion()
}

public func getXP() -> Int{
updateXP {
return (self.user.xp) as Int
}
}

public func getLevel() -> Int {
updateLevel(completion: {
return (self.user.level) as Int
})
}

public func getName() -> String {
updateName(completion: {
return (self.user.name) as String
})
}

public func getBio() -> String {
updateBio(completion: {
return (self.user.bio) as String
})
}

public func getInterest() -> String {
updateInterest(completion: {
return (self.user.interest) as String
})
}

public func getPhoto() -> Data {
updatePhoto(completion: {
return (self.user.image) as Data
})
}

func updatePersonalInfo() {
//sending user data to Firebase
let uid = Auth.auth().currentUser?.uid

isLoading = true
self.uploadImage(imageData: self.getPhoto(), path: "profile_Photos") { (url) in
self.ref.collection("Users").document(uid ?? "").setData([
"uid": uid ?? "",
"imageurl": url,
"username": self.user.name,
"bio": self.user.bio,
"interest" : self.user.interest
], merge: true) { (err) in

if err != nil{
self.isLoading = false
return
}
self.isLoading = false
// success means settings status as true...
self.status = true
}

}
}

func increaseXPnLV() {
//sending user data to Firebase
let uid = Auth.auth().currentUser!.uid
let docRef = ref.collection("Users").document(uid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
docRef.updateData(["xp": FieldValue.increment(Int64(50))])
// update level
let xp = document.data()!["xp"] as! Int
docRef.updateData(["level": self.user.xp2Level(xp: xp)])
} else {
print("Document does not exist")
}
}
}

func uploadImage(imageData: Data, path: String, completion: @escaping (String) -> ()){

let storage = Storage.storage().reference()
let uid = Auth.auth().currentUser?.uid

storage.child(path).child(uid ?? "").putData(imageData, metadata: nil) { (_, err) in
print("imageData: (imageData)")
if err != nil{
completion("")
return

}
// Downloading Url And Sending Back...
storage.child(path).child(uid ?? "").downloadURL { (url, err) in
if err != nil{
completion("")
return

}
completion("(url!)")
}
}
}
}
// This is View
import SwiftUI
import CoreData
import Firebase
import FirebaseFirestore
struct Goals: View {
let current_user_id = Auth.auth().currentUser?.uid
@State private var showingAlert = false
var ref = Firestore.firestore()
@StateObject var currentUser: userViewModel
@StateObject var homeData = HomeViewModel()
@State var txt = ""
@State var edge = UIApplication.shared.windows.first?.safeAreaInsets
@FetchRequest(entity: Goal.entity(), sortDescriptors: [NSSortDescriptor(key: "date",
      ascending: true)], animation: .spring()) var results : FetchedResults<Goal>

//    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var greeting : String = "Hello"
@Environment(.managedObjectContext) var context
var body: some View {
ForEach(results){goal in
Button(action: {
context.delete(goal)
try! context.save()
if current_user_id != nil {
currentUser.updateXPnLV()
self.showingAlert = true
}
}, label: Text("Something")
)
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You completed a goal today, XP+50!"), dismissButton: .default(Text("OK")))
}
}
}
}

编辑

我看到的另一个错误是AttributeGraph precondition failure: attribute failed to set an initial value: 805912, ForEachChild<Array<userInfoModel>, ObjectIdentifier, HStack<VStack<HStack<TupleView<(Text, Divider, Text)>>>>>.

  1. AppStorageView中使用,而它似乎可以处理除@Published之外的所有SwiftUI包装。ObservableObjectstruct(即View(之外似乎不可靠

https://developer.apple.com/documentation/swiftui/appstorage

  1. 作为标准做法,您的所有classstruct都应大写,因此请更改class CurrentUserViewModelclass UserInfoModel

此外,将@StateObject var currentUser: currentUserViewModel更改为@StateObject var currentUser: CurrentUserViewModel = CurrentUserViewModel()

初始化是缺失的最重要的部分。

  1. ForEach中的所有内容都是浮动的——它不在变量、函数或body中。你的body在哪里

这可能就是错误所说的。将所有代码封装在主体中

var body: some View {
//All the code for the ForEach
}
  1. 您的Button似乎缺少titlelabel

  2. 删除该行

    () -> Alert in

我相信还有其他一些小事情。我建议您在这个View中从头开始,并开始逐行放入代码。

这是一个起点。Firebase部分需要大量的工作,但您应该能够通过关注我注释掉的代码并删除代码来模拟Firebase的响应来开始。

所有这些都在FirebaseManagerclass

一旦这起作用,其余的都会起作用。

代码按原样工作,这样你就可以看到它与虚假响应的作用

///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)))
})

}
}
}
}

最新更新