I'm trying to execute loadQuestionToView() after executing loadQuestionsForTests() using defer, but I'm unsuccessful... Can you help me with that?
override func viewDidLoad() {
super.viewDidLoad()
defer {
loadQuestionToView()
}
loadQuestionsForTest(testID: testID)
}
func loadQuestionToView() {
questionLabel.text = self.questions[self.questionIterator].question
}
func loadQuestionsForTest(testID: String) {
let docRef = db.collection(Constants.FStore.collectionTests).document(testID).collection("questions")
docRef.getDocuments { (querySnapshot, error) in
if let e = error {
print(e)
} else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
let question: Question = Question(question: data["question"] as! String, correctAnswer: data["answerCorrect"] as! String, wrongAnswer1: data["answerWrong1"] as! String, wrongAnswer2: data["answerWrong2"] as! String, wrongAnswer3: data["answerWrong3"] as! String)
self.questions.append(question)
//print(self.questions[self.iterator].question)
}
}
}
}
}
The best way to do is using closure.
If you want learn about closures, here you go: https://docs.swift.org/swift-book/LanguageGuide/Closures.html
override func viewDidLoad() {
super.viewDidLoad()
loadQuestionsForTest(testID: testID) { success in
if success {
questionLabel.text = self.questions[self.questionIterator].questio
} else {
// Whatever you want to do on failure
}
}
}
func loadQuestionsForTest(testID: String, #escaping completion: (Bool) -> Void)) {
let docRef = db.collection(Constants.FStore.collectionTests).document(testID).collection("questions")
docRef.getDocuments { (querySnapshot, error) in
if let e = error {
completion(false)
} else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
let question: Question = Question(question: data["question"] as! String, correctAnswer: data["answerCorrect"] as! String, wrongAnswer1: data["answerWrong1"] as! String, wrongAnswer2: data["answerWrong2"] as! String, wrongAnswer3: data["answerWrong3"] as! String)
self.questions.append(question)
}
completion(true)
} else {
completion(false)
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadQuestionsForTest(testID: testID, completionHandler: { [weak self] result in
// here I call loadQuestionToView when result is true
if result {
self?.loadQuestionToView()
}
})
}
func loadQuestionToView() {
questionLabel.text = self.questions[self.questionIterator].question
}
func loadQuestionsForTest(testID: String, completionHandler: #escaping (Bool) -> Void) {
let docRef = db.collection(Constants.FStore.collectionTests).document(testID).collection("questions")
docRef.getDocuments { (querySnapshot, error) in
if let e = error {
print(e)
} else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
let question: Question = Question(question: data["question"] as! String, correctAnswer: data["answerCorrect"] as! String, wrongAnswer1: data["answerWrong1"] as! String, wrongAnswer2: data["answerWrong2"] as! String, wrongAnswer3: data["answerWrong3"] as! String)
self.questions.append(question)
//print(self.questions[self.iterator].question)
}
completionHandler(true)
return
}
}
completionHandler(false)
}
}
Related
I built an MVVM architecture to support my app which is supposed to control the pipeline between the frontend and my firebase database. Initially, I successfully implemented the entire work by coding totally in the frontend, but there are lots of bugs when I encapsulated them into a function.
For example, the next sheet will be presented when the currently presented sheet gets dismissed. Sometimes I needed to wait for a long time until the app is unfrozen. Even worse, the app crashed down when I clicked the button.
I heard that nested models don't work yet if SwiftUI is in use (reference). However, I just cannot come up with a better solution if my classes are untested.
// 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")))
}
}
}
}
EDIT
Another error I saw is AttributeGraph precondition failure: attribute failed to set an initial value: 805912, ForEachChild<Array<userInfoModel>, ObjectIdentifier, HStack<VStack<HStack<TupleView<(Text, Divider, Text)>>>>>.
AppStorage is for use in a View while it may appear to be working all the SwiftUI wrappers with the exception of #Published inside an ObservableObject seem to be unreliable outside of a struct that is a View.
https://developer.apple.com/documentation/swiftui/appstorage
As a standard practice all your class and struct should be capitalized so change class CurrentUserViewModel andclass UserInfoModel
Also, change #StateObject var currentUser: currentUserViewModel to #StateObject var currentUser: CurrentUserViewModel = CurrentUserViewModel()
The initialization is the most important part that is missing.
Everything in the ForEach is just floating it isn't within a variable, function or inside the body. Where is your body?
This is probably what the error is talking about. wrap all of that code in a body
var body: some View {
//All the code for the ForEach
}
Your Button seems to be missing a title or label
Remove this line
() -> Alert in
I am sure there are other little things. I suggest you start from scratch in this View and you start putting in code line by line.
Here is a starting point. The Firebase part needs quite a but of work but you should be able to get started by focusing on the code that I commented out and removing the code to mimic a response from Firebase.
All of this is in the FirebaseManager class
Once this is working the rest will work.
The code as is works so you can see it in action with the fake responses
///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
}
Here is the rest
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)))
})
}
}
}
}
I have the following problem to solve:
All datas are loaded in realtime (this time even multiple times per post), but I just want to refresh with a refresher I already have.
This is my refresher:
// Refresher
func refresh() {
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Aktualisieren")
refresher.addTarget(self, action: #selector(DiscoveryViewController.refreshData) , for: UIControl.Event.valueChanged)
tableView.addSubview(refresher)
}
#objc func refreshData(sender: Any) {
loadTopPosts()
refresher.endRefreshing()
}
And with this I load all posts:
func loadTopPosts() {
ProgressHUD.show("Lade...", interaction: false)
self.postArray.removeAll()
self.tableView.reloadData()
// Aktuelle Location des aktuell eingeloggten Users laden
guard let currentUserUid = UserApi.shared.CURRENT_USER_ID else { return }
let databaseRef = LocationApi.shared.geoRef
databaseRef.getLocationForKey(currentUserUid) { (location, error) in
if error != nil {
ProgressHUD.showError("Posts konnten nicht geladen werden")
} else if location != nil {
print("Location for \(currentUserUid) is [\(location!.coordinate.latitude), \(location!.coordinate.longitude)]")
// Alle Posts im vorgegebenen Umkreis laden
let REF_GEO_POSTS = Database.database().reference().child("geolocation_posts")
let geoRef = GeoFire(firebaseRef: REF_GEO_POSTS)
// Lade den aktuell eingestellten Radius aus der Datenbank
self.observeRadius(completion: { (radius) in
let currentRadius = radius
// Üperprüfe, welche Posts im Umkreis erstellt wurden
let circleQuery = geoRef.query(at: location!, withRadius: Double(currentRadius)!)
circleQuery.observe(.keyEntered, with: { (postIds, location) in
self.observePost(withPostId: postIds, completion: { (posts) in
guard let userUid = posts.uid else { return }
self.observeUser(uid: userUid, completion: { (users) in
let postArray = UserPostModel(post: posts, user: users)
self.postArray.append(postArray)
self.postArray.sort(by: {$0.post!.secondsFrom1970! > $1.post!.secondsFrom1970!})
self.tableView.reloadData()
self.tableView.setContentOffset(CGPoint.zero, animated: true)
ProgressHUD.dismiss()
})
})
})
})
if self.postArray.count == 0 {
ProgressHUD.dismiss()
}
} else {
ProgressHUD.showError("Posts konnten nicht geladen werden")
}
}
}
Here are the functions where I over serve datas from firebase:
let REF_POSTS = Database.database().reference().child("posts")
func observePost(withPostId id: String, completion: #escaping (PostModel) -> Void) {
REF_POSTS.child(id).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newPost = PostModel(dictionary: dic, key: snapshot.key)
completion(newPost)
}
}
let REF_USERS = Database.database().reference().child("users")
func observeUser(uid: String, completion: #escaping (UserModel) -> Void) {
REF_USERS.child(uid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
completion(newUser)
}
}
func observeRadius(completion: #escaping (String) -> Void) {
guard let currentUserUid = UserApi.shared.CURRENT_USER_ID else { return }
let REF_RADIUS = Database.database().reference().child("users").child(currentUserUid).child("radius")
REF_RADIUS.observeSingleEvent(of: .value) { (radius) in
let currentRadius = radius.value as? String
completion(currentRadius!)
}
}
What I now want to do is to disable the realtime function (updating the tableView only if I refresh). So if I refresh, everything will be displayed correctly.
How to solve this problem?
Thanks in advance for your help!
There is observe, that keeps notifying every time an update happens in the database. And there is observeSingleEvent, that will only provide you data when requested.
https://firebase.google.com/docs/database/ios/read-and-write
And also, your geofire reference will keep notifying you for every update, if you don't want it to do that, remove it like this:
"If you're not interested in getting updates on new/moving users after the initial query, this is also a great moment to remove your observer by calling removeObserverWithFirebaseHandle or removeAllObservers."
in your case it's posts, and that was mentioned here:
https://stackoverflow.com/a/50722984/8869493
I have this code:
enum Result<T> {
case succes(T)
case error(String)
}
typealias completionHandler = (Result<Data >) -> ()
func getJsonFromServer(parameters: String, completion: #escaping completionHandler) {
let fullUrlString = ApiConstans.fullPath + parameters
guard let url = URL(string: fullUrlString) else {
return completion(.error("Error 100: Problem with url"))
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {
return completion(.error("Error 101: Problem with data"))
}
guard let data = data else {
return completion(.error("Error 102: Problem with data"))
}
debugPrint("R> \(fullUrlString)")
return completion(.succes(data))
}.resume()
}
func checkUsersLogin(login: String?, password: String?, completion: #escaping completionHandler) {
self.getJsonFromServer(parameters: "?action=LOGOWANIE&login=\(login!)&password=\(password!)", completion: completion)
}
How can I call it?
I try this:
#IBAction func btnLoginPressed(_ sender: Any) {
if self.textFieldLogin.text?.isEmpty ?? true || self.textFieldPassword.text?.isEmpty ?? true {
self.errorLoginMessage(txt: "Error", title: "Error")
} else {
cms.checkUsersLogin(login: self.textFieldLogin.text, password: self.textFieldPassword.text, completion: { (data) in
if data.error == nil, let data = data {
do {
let decoder = JSONDecoder()
loggedUser = try decoder.decode(LoginUser.self, from: data)
if ((loggedUser?.id ) == nil) {
let jsonValues = try? JSONSerialization.jsonObject(with: data, options: [])
if let downloadJson = jsonValues as? [String: Any], let message = downloadJson["komunikat"] as? String, let title = downloadJson["error"] as? String {
DispatchQueue.main.async {
self.errorLoginMessage(txt: message, title: title)
}
} else {
DispatchQueue.main.async {
self.errorLoginMessage(txt: "Podany login lub hasło jest błędny!!", title: "Błąd")
}
}
} else {
DispatchQueue.main.async {
dump(loggedUser)
self.performSegue(withIdentifier: "toLoginUser", sender: self)
}
}
}
catch {
print("Error in decoder")
}
} else {
print("Error 104: \(error)")
}
})
}
}
But I have error with:
Enum element 'error' cannot be referenced as an instance member: if data.error == nil, let data = data {
and Use of unresolved identifier 'error': print("Error 104: (error)")
could I ask you to repair this code?
Change your code like:
#IBAction func btnLoginPressed(_ sender: Any) {
if self.textFieldLogin.text?.isEmpty ?? true || self.textFieldPassword.text?.isEmpty ?? true {
self.errorLoginMessage(txt: "Error", title: "Error")
} else {
cms.checkUsersLogin(login: self.textFieldLogin.text, password: self.textFieldPassword.text, completion: { (data) in
switch(data) {
case .success(let data):
do {
let decoder = JSONDecoder()
loggedUser = try decoder.decode(LoginUser.self, from: data)
if ((loggedUser?.id ) == nil) {
let jsonValues = try? JSONSerialization.jsonObject(with: data, options: [])
if let downloadJson = jsonValues as? [String: Any], let message = downloadJson["komunikat"] as? String, let title = downloadJson["error"] as? String {
DispatchQueue.main.async {
self.errorLoginMessage(txt: message, title: title)
}
} else {
DispatchQueue.main.async {
self.errorLoginMessage(txt: "Podany login lub hasło jest błędny!!", title: "Błąd")
}
}
} else {
DispatchQueue.main.async {
dump(loggedUser)
self.performSegue(withIdentifier: "toLoginUser", sender: self)
}
}
}
catch {
print("Error in decoder")
}
case .error(let error):
print("Error 104: \(error)")
}
})
}
}
Try this:
#IBAction func btnLoginPressed(_ sender: Any) {
if self.textFieldLogin.text?.isEmpty ?? true || self.textFieldPassword.text?.isEmpty ?? true {
self.errorLoginMessage(txt: "Error", title: "Error")
} else {
cms.checkUsersLogin(login: self.textFieldLogin.text, password: self.textFieldPassword.text, completion: { (data) in
switch data {
case .error(let error):
print(error)
case .success(let data):
do {
let decoder = JSONDecoder()
loggedUser = try decoder.decode(LoginUser.self, from: data)
if ((loggedUser?.id ) == nil) {
let jsonValues = try? JSONSerialization.jsonObject(with: data, options: [])
if let downloadJson = jsonValues as? [String: Any], let message = downloadJson["komunikat"] as? String, let title = downloadJson["error"] as? String {
DispatchQueue.main.async {
self.errorLoginMessage(txt: message, title: title)
}
} else {
DispatchQueue.main.async {
self.errorLoginMessage(txt: "Podany login lub hasło jest błędny!!", title: "Błąd")
}
}
} else {
DispatchQueue.main.async {
dump(loggedUser)
self.performSegue(withIdentifier: "toLoginUser", sender: self)
}
}
}
catch {
print("Error in decoder")
}
}
})
}
}
To check a variable is a specific enum value:
if case let Result.error(error) = data {
// do something with error
}
Thread Sanitizer does not show all issues. for example
private func observeConversationUsers(_ isObserve: Bool, conversationID: String, updated: (() -> Void)?, fail: ((_ error: Error) -> Void)?) {
guard isObserveConversationUsers != isObserve else { return }
isObserveConversationUsers = isObserve
DispatchQueue.global(qos: .background).async {
let conversationUserRef = Database.database().reference().child(MainGateways.chat.description).child(MainGateways.conversationUsers.description).child(conversationID)
if !isObserve {
conversationUserRef.removeAllObservers()
return
}
if !self.references.contains(conversationUserRef) { // for example this issue
self.references.append(conversationUserRef)
}
conversationUserRef.observe(.childAdded, with: { (snap) in
if snap.value is NSNull {
return
}
guard let dict = snap.value as? [String : Any] else { return }
guard let chatUserActivityModel = Mapper<ChatUserActivityModel>().map(JSON: dict) else { return }
self.downloadImageProfile(chatUserActivityModel.userID, conversationID: conversationID)
}, withCancel: { (error) in
// TODO: - it
})
conversationUserRef.observe(.childRemoved, with: { (snap) in
}, withCancel: { (error) in
// TODO: - it
})
}
}
My diagnostics settings
How can I fix it?
Update: I made a very simple example but Xcode does not show an error here, although the isPushSettingsFormVC property is called in another thread
class MainTabBarController: UITabBarController {
var isPushSettingsFormVC = false
override func viewDidLoad() {
super.viewDidLoad()
// I think here is a data race
DispatchQueue.global(qos: .background).async { [weak self] in
self?.isPushSettingsFormVC = false
}
}
}
I am currently following zero2launches chat application, and I also watched/completed their other tutorials on YouTube. While I started the chat application, I thought about changing the chat portion from a tableViewController to JSQMessageViewController. In that effort, I can post the data to firebase, but it won't show up in the individual chat rooms.
Here is the code:
var roomId: String!
var messages = [JSQMessage]()
func observeMessage() {
messageRef.observeEventType(.ChildAdded, withBlock: { snapshot in
//print(snapshot.value)
if let dict = snapshot.value as? [String: AnyObject] {
let mediaType = dict["MediaType"] as! String
let senderId = dict["senderId"] as! String
let senderName = dict["senderName"] as! String
switch mediaType {
case "TEXT":
let text = dict["text"] as? String
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, text: text))
case "PHOTO":
let fileUrl = dict["fileUrl"] as! String
let url = NSURL(string: fileUrl)
let data = NSData(contentsOfURL: url!)
let picture = UIImage(data: data!)
let photo = JSQPhotoMediaItem(image: picture)
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photo))
case "VIDEO":
let fileUrl = dict["fileUrl"] as! String
let video = NSURL(string: fileUrl)
let videoItem = JSQVideoMediaItem(fileURL: video, isReadyToPlay: true)
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: videoItem))
default:
print("unknow data type")
}
if mediaType == "TEXT" {
let text = dict["text"] as? String
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, text: text))
}else if mediaType == "PHOTO" {
let fileUrl = dict["fileUrl"] as! String
let url = NSURL(string: fileUrl)
let data = NSData(contentsOfURL: url!)
let picture = UIImage(data: data!)
let photo = JSQPhotoMediaItem(image: picture)
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photo))
}else if mediaType == "VIDEO" {
let fileUrl = dict["fileUrl"] as! String
let video = NSURL(string: fileUrl)
let videoItem = JSQVideoMediaItem(fileURL: video, isReadyToPlay: true)
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: videoItem))
}
print(dict)
let text = dict["text"] as? String
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, text: text))
self.collectionView.reloadData()
}
})
}
override func didPressSendButton(button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: NSDate!) {
let newMessage = messageRef.childByAutoId()
let messageData = ["text": text, "senderId": senderId, "senderName": senderDisplayName, "MediaType": "TEXT"]
newMessage.setValue(messageData)
}
override func didPressAccessoryButton(sender: UIButton!) {
print("didPressAccessoryButton")
let sheet = UIAlertController(title: "Media Messages", message: "Please select a media", preferredStyle: UIAlertControllerStyle.ActionSheet)
let cancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel) { (alert:UIAlertAction) in
}
let photoLibrary = UIAlertAction(title: "Photo Library", style: UIAlertActionStyle.Default) { (alert: UIAlertAction) in
self.getMediaFrom(kUTTypeImage)
}
let videoLibrary = UIAlertAction(title: "Video Library", style: UIAlertActionStyle.Default) { (alert: UIAlertAction) in
self.getMediaFrom(kUTTypeMovie)
}
sheet.addAction(photoLibrary)
sheet.addAction(videoLibrary)
sheet.addAction(cancel)
self.presentViewController(sheet, animated: true, completion: nil)
}
func getMediaFrom(type: CFString) {
print(type)
let mediaPicker = UIImagePickerController()
mediaPicker.delegate = self
mediaPicker.mediaTypes = [type as String]
self.presentViewController(mediaPicker, animated: true, completion: nil)
}
override func collectionView(collectionView: JSQMessagesCollectionView!, messageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageBubbleImageDataSource! {
let bubbleFactory = JSQMessagesBubbleImageFactory()
return bubbleFactory.outgoingMessagesBubbleImageWithColor(.blackColor())
}
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("number of item:\(messages.count)" )
return messages.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath) as! JSQMessagesCollectionViewCell
return cell
}
override func collectionView(collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAtIndexPath indexPath: NSIndexPath!) {
print("didTapMessageBubbleAtIndexPath: \(indexPath.item)")
let message = messages[indexPath.item]
if message.isMediaMessage {
if let mediaItem = message.media as? JSQVideoMediaItem {
let player = AVPlayer(URL: mediaItem.fileURL)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.presentViewController(playerViewController, animated: true, completion: nil)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func sendMedia(picture: UIImage?, video: NSURL?) {
print(picture)
FIRStorage.storage().reference()
if let picture = picture {
let filePath = "\(FIRAuth.auth()?.currentUser!)\(NSDate.timeIntervalSinceReferenceDate())"
print(filePath)
let data = UIImageJPEGRepresentation(picture, 0.1)
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpg"
FIRStorage.storage().reference().child(filePath).putData(data!, metadata: metadata) { (metadata, error) in
if error != nil {
print(error?.localizedDescription)
return
}
let fileUrl = metadata!.downloadURLs![0].absoluteString
let newMessage = self.messageRef.childByAutoId()
let messageData = ["fileUrl": fileUrl, "senderId": self.senderId, "senderName": self.senderDisplayName, "MediaType": "PHOTO"]
newMessage.setValue(messageData)
}
}else if let video = video {
let filePath = "\(FIRAuth.auth()?.currentUser!)\(NSDate.timeIntervalSinceReferenceDate())"
print(filePath)
let data = NSData(contentsOfURL: video)
let metadata = FIRStorageMetadata()
metadata.contentType = "video/mp4"
FIRStorage.storage().reference().child(filePath).putData(data!, metadata: metadata) { (metadata, error) in
if error != nil {
print(error?.localizedDescription)
return
}
let fileUrl = metadata!.downloadURLs![0].absoluteString
let newMessage = self.messageRef.childByAutoId()
let messageData = ["fileUrl": fileUrl, "senderId": self.senderId, "senderName": self.senderDisplayName, "MediaType": "VIDEO"]
newMessage.setValue(messageData)
}
}
}
}
Here is my existing message database functions
let roofRef = FIRDatabase.database().reference()
class DataService {
static let dataService = DataService()
private var _BASE_REF = roofRef
private var _ROOM_REF = roofRef.child("captions")
private var _MESSAGE_REF = roofRef.child("messages")
private var _PEOPLE_REF = roofRef.child("people")
var currentUser: FIRUser? {
return FIRAuth.auth()!.currentUser!
}
var BASE_REF: FIRDatabaseReference {
return _BASE_REF
}
var ROOM_REF: FIRDatabaseReference {
return _ROOM_REF
}
var MESSAGE_REF: FIRDatabaseReference {
return _MESSAGE_REF
}
var PEOPLE_REF: FIRDatabaseReference {
return _PEOPLE_REF
}
var storageRef: FIRStorageReference {
return FIRStorage.storage().reference()
}
var fileUrl: String!
func CreateNewPost(user: FIRUser, caption: String, data: NSData) {
let filePath = "\(user.uid)/\(Int(NSDate.timeIntervalSinceReferenceDate()))"
let metaData = FIRStorageMetadata()
metaData.contentType = "image/jpg"
storageRef.child(filePath).putData(data, metadata: metaData, completion: { (metadata, error) in
if let error = error{
print("Error uploarding: \(error.description)")
return
}
self.fileUrl = metadata!.downloadURLs![0].absoluteString
if let user = FIRAuth.auth()?.currentUser {
let idCaption = self.BASE_REF.child("captions").childByAutoId()
idCaption.setValue(["caption": caption, "thumbnailURLFromStorage": self.storageRef.child(metadata!.path!).description, "filelUrl": self.fileUrl])
}
})
}
func SignUp(username: String, email: String, password: String, data: NSData) {
FIRAuth.auth()?.createUserWithEmail(email, password: password, completion: { (user, error) in
if let error = error {
print(error.localizedDescription)
return
}
let changeRequest = user?.profileChangeRequest()
changeRequest?.displayName = username
changeRequest?.commitChangesWithCompletion({ (error) in
if let error = error {
print(error.localizedDescription)
return
}
})
let filePath = "profileImage/\(user!.uid)"
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpeg"
self.storageRef.child(filePath).putData(data, metadata: metadata, completion: { (metadata, error) in
if let error = error {
print("\(error.description)")
return
}
self.fileUrl = metadata?.downloadURLs![0].absoluteString
let changeRequestPhoto = user!.profileChangeRequest()
changeRequestPhoto.photoURL = NSURL(string: self.fileUrl)
changeRequestPhoto.commitChangesWithCompletion({ (error) in
if let error = error {
print(error.localizedDescription)
return
}else{
print("profile updated")
}
})
self.PEOPLE_REF.child((user?.uid)!).setValue(["username": username, "email": email, "profileImage": self.storageRef.child((metadata?.path)!).description])
ProgressHUD.showSuccess("Succeeded.")
let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.login()
})
})
}
//implement login func
func login(email: String, password: String) {
FIRAuth.auth()?.signInWithEmail(email, password: password, completion: { (user, error) in
if let error = error {
print(error.localizedDescription)
return
}
ProgressHUD.showSuccess("Succeeded")
let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.login()
})
}
// Update profile
func SaveProfile(username: String, email: String, data: NSData) {
let user = FIRAuth.auth()?.currentUser!
let filePath = "\(user!.uid)/\(Int(NSDate.timeIntervalSinceReferenceDate()))"
let metaData = FIRStorageMetadata()
metaData.contentType = "image/jpg"
self.storageRef.child(filePath).putData(data, metadata: metaData) { (metaData, error) in
if let error = error {
print("Error uploading: \(error.description)")
return
}
self.fileUrl = metaData!.downloadURLs![0].absoluteString
let changeRequestProfile = user?.profileChangeRequest()
changeRequestProfile?.photoURL = NSURL(string: self.fileUrl)
changeRequestProfile?.displayName = username
changeRequestProfile?.commitChangesWithCompletion({ (error) in
if let error = error {
print(error.localizedDescription)
ProgressHUD.showError("NetWork error")
}else{
}
})
if let user = user {
user.updateEmail(email, completion: { (error) in
if let error = error {
print(error.description)
}else{
print("email update")
}
})
}
ProgressHUD.showSuccess("Saved")
}
}
func CreateNewMessage(userId: String, roomId: String, textMessage: String) {
let idMessage = roofRef.child("messages").childByAutoId()
DataService.dataService.MESSAGE_REF.child(idMessage.key).setValue(["message": textMessage, "senderId": userId])
DataService.dataService.ROOM_REF.child(roomId).child("messages").child(idMessage.key).setValue(true)
}
func fetchMessageFromServer(roomId: String, callback: (FIRDataSnapshot) -> ()) {
DataService.dataService.ROOM_REF.child(roomId).child("messages").observeEventType(.ChildAdded, withBlock: {snapshot -> Void in
DataService.dataService.MESSAGE_REF.child(snapshot.key).observeEventType(.Value, withBlock: {
snap -> Void in
callback(snap)
})
})
}