The new View i am getting sent to afterwards is dependent on a cached variable. I am not experienced with threads, but i think the machine continues on the main thread while another thread does the "getVIN"-function.
I dont care if the UI "sleeps".
Is there a way to force it so it doesn't continue until the "getVIN"-function is finished?
func verify() {
if self.email != "" && self.pass != "" {
Auth.auth().signIn(withEmail: self.email, password: self.pass) {
(res, err) in
if err != nil {
print(err!.localizedDescription)
self.error = err!.localizedDescription
self.alert.toggle()
return
}
print("success")
//getVIN, finds a number from a database in Firestore, with the users email, and uploads
//it to UserDefault
self.getVIN(email: self.email)
UserDefaults.standard.set(true, forKey: "status")
UserDefaults.standard.set(self.email, forKey: "email")
print(UserDefaults.standard.string(forKey: "email"))
//when i am finished i get sent to a new View with this function
//the new View uses the cached email, but the getVIN-function is not finished until after i am redirected to the new page.
NotificationCenter.default.post(name: NSNotification.Name("status"), object: nil)
}
}
else {
self.error = "The information is wrong"
self.alert.toggle()
}
}
You can do something like this. You can store the result of getVin in a Published Property and then use it to load the target view you want to load.
This is a pseudo code, but it should give you an idea of what to do.
#Published var vin = getVin()
// In SwiftUI
if !vin {
ProgressView()
} else {
TargetView()
}
Also, the getVIN method should be having a completion handler if it runs in background. The completion handler will execute after the database operations are done in that method. You would then change your view in completion handler.
FYI, using notification for changing the Views in SwiftUI is a wrong practice. For exact same reasons Combine was introduced. Learn about it in this article.
Related
To load some information in my app's view, I need it to finish networking because some methods depend on the result. I looked into serial DispatchQueue and .async methods, but it's not working as expected.
Here is what I tried so far. I defined 3 blocks:
Where I'd get hold of the user's email, if any
The email would be used as input for a method called getData, which reads the database based on user's email address
This block would populate the table view with the data from the database. I've laid it out like this, but I'm getting an error which tells me the second block still executes before we have access to user's email address, i.e. the first block is finished. Any help is appreciated, thanks in advance!
let serialQueue = DispatchQueue(label: "com.queue.serial")
let block1 = DispatchWorkItem {
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
print("unable to identify user")
} else {
print(user!.profile?.email ?? "")
self.email = user!.profile?.email ?? ""
print("email is: \(self.email)")
}
}
}
let block2 = DispatchWorkItem{
self.getData(self.email)
}
let block3 = DispatchWorkItem {
DispatchQueue.main.async {
self.todoListTable.reloadData()
}
}
serialQueue.async(execute: block1)
block1.notify(queue: serialQueue, execute: block2)
block2.notify(queue: serialQueue, execute: block3)
Your problem is that you are dispatching asynchronous work inside your work items; GIDSignIn.sharedInstance.restorePreviousSignIn "returns" immediately but fires the completion handler later when the work is actually done. Since it has returned, the dispatch queue considers the work item complete and so moves on to the next item.
The traditional approach is to invoke the second operation from the completion handler of the first (and the third from the second).
Assuming you modified self.getData() to have a completion handler, it would look something like this:
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
print("unable to identify user")
} else {
print(user!.profile?.email ?? "")
self.email = user!.profile?.email ?? ""
print("email is: \(self.email)")
self.getData(self.email) {
DispatchQueue.main.async {
self.todoListTable.reloadData()
}
}
}
}
You can see you quickly end up with a "pyramid of doom".
The modern way is to use async/await. This requires your functions to support this approach and imposes a minimum iOS level of iOS 13.
It would look something like
do {
let user = try await GIDSignIn.sharedInstance.restorePreviousSignIn()
if let email = user.profile?.email {
let fetchedData = try await getData(email)
self.data = fetchedData
self.tableView.reloadData()
}
} catch {
print("There was an error \(error)")
}
Ahh. Much simpler to read.
I have this truck driving app in swiftUI where I use fire base to log users in and out. The problem is that when I sign in with one user, and all it’s fire base functionalities are triggered, After I log out from the current user and into a new user, the functionality of the old user is still playing out. I think it might have something to do with the firebase functions being in an onAppear method. I am not sure though.
This is the firebase code. I dont think what Im querying has any relation to the solution so I wont explain it but if you think it does than please let me know.
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]) { (_,_) in
}
myDrivers = []
getEmails()
db.collectionGroup("resources").whereField("control", isEqualTo: true).addSnapshotListener { (snapshot, err) in
if myDrivers.count == 0{}
else{
if err != nil{print("Error fetching motion status: \(err)")}
if ((snapshot?.documents.isEmpty) != nil){}
// Gets all the driverLocation documents
for doc in snapshot!.documents{
if myDrivers.contains(doc.reference.parent.parent!.documentID){
let isDriving = doc.get("isDriving") as! Bool
let isStopped = doc.get("isStopped") as! Bool
let notified = doc.get("notified") as! Bool
if (isDriving == false && isStopped == false) || notified{}
else{
// Gets the name of the drivers
doc.reference.parent.parent?.getDocument(completion: { (snapshot, err) in
if err != nil{print("Error parsing driver name: \(err)")}
let firstName = snapshot?.get("firstName") as! String
let lastName = snapshot?.get("lastName") as! String
self.notifiedDriver = "\(firstName) \(lastName)"
})
// Logic
if isDriving{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) is back on the road.")
showDrivingToast.toggle()
doc.reference.updateData(["notified" : true])
}else if isStopped{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) has stopped driving.")
showStoppedToast.toggle()
doc.reference.updateData(["notified" : true])
}
}
}
else{}
}
}
}
})
Listeners don't die when a view controller is left. They remain active.
It's important to manage them through handlers for specific listeners as a view closes or the user navigates away. Here's how to remove a specific listener
let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
// ...
}
and then later
// Stop listening to changes
listener.remove()
Or, if your user is logging out, you can use removeAllObservers (for the RealtimeDatabase) to remove them all at one time, noting that
removeAllObservers must be called again for each child reference where
a listener was established
For Firestore, store the listeners in a listener array class var and when you want to remove them all, just iterate over the array elements calling .remove() on each.
There's additional info in the Firebase Getting Started Guide Detach Listener
How can I access my array objects in another function, array objects from arr and from emails. I can so far only access my array objects inside of the else statement when I call auth.auth() function. I want to find out how I can do this.
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, err) in
if let err = err{
print(err.localizedDescription)
return
}
if granted{
print("Access granted")
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey]
let req = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
do {
try store.enumerateContacts(with: req) { (contact, stop) in
print(contact.emailAddresses.first?.value as Any)
if let em = contact.emailAddresses.first?.value{
//print("cool")
Auth.auth().fetchProviders(forEmail: em as String, completion: {
(providers, error) in
if error != nil {
print("wierd")
}else{
if providers == nil{
print("No active account")
}else{
self.emails.append(em as String)
self.arr.append(contact.givenName + " " + contact.familyName)
print("Active Account")
print(self.arr)
print(self.emails)
}
}
})
}else{
//print("wow")
}
}
}catch let err{
print(err.localizedDescription)
}
}else{
print("Access denied")
}
}
print("Arr")
print(self.arr)
print(self.emails)
print("Emails")
I want to find out how I can access my array elements in "arr" and in "emails" in another function, because my ultimate goal is to put out the array info in a tableView.
Data is loaded from Firebase asynchronously. While you may store it in variables that can be accessed from anywhere, the timing matters.
In your code, when the print(self.arr) runs, the self.arr.append(...) hasn't run yet. You can most easily see this by placing breakpoints on these lines and running the code in a debugger, or by placing some logging code and checking its output.
For this reason, all code that needs data from the database needs to be inside the completion handler or be called from there.
For some more on this, and examples of solutions, see:
Code not finishing the function, it is ending execution halfway through, which in turn includes more links
Make code with Firebase asynchronous
Why isn't my function that pulls information from the database working?, which even more links
Finish all asynchronous requests before loading data?, using a dispatch group
Asynchronous Swift call into array
using variables outside of completion block
wait for two asynchronous completion functions to finish, before executing next line code
Finish all asynchronous requests before loading data?
In my iOS app, I'm using Firebase Transactions to update scoreboards based on user-generated data to avoid mistakes with concurrent updates. I save data at three points:
when the user presses 'Sign Out'
when the user force quits the app (not working yet due to timeout issue)
when the day changes while the app is running
Below is the transaction code:
func saveDataToFirebase(signUserOut: Bool)
{
let countyRef = Database.database().reference().child("countyleaderboard")
countyRef.child(self.userCounty).runTransactionBlock({ (currentData: MutableData) in
var value = currentData.value as? Float
if value == nil
{
value = Float(0.00)
}
currentData.value = value! + self.currentSessionAlt
return TransactionResult.success(withValue: currentData)
}, andCompletionBlock: {
error, commited, snap in
if commited && signUserOut
{
do
{
print("transaction complete")
try Auth.auth().signOut()
}
catch let logoutError
{
print(logoutError)
}
}
})
}
The code reaches the sign out method, but the database is not being updated. I ran the code separately, without including user sign out and the transaction completes fine.
Why does the transaction not actually complete before signing the user out? And is there a way to fix this?
I’m using Realm Object Server for a simple test project and I’m facing problems synchronizing ROS connection setup and follow up usage of the realm object to access the database.
In viewDidLoad I’m calling connectROS function to initialize realmRos object/connection:
var realmRos: Realm!
override func viewDidLoad() {
connectROS()
if(FBSDKAccessToken.current() != nil){
// logged in
getFBUserData()
}else{
// not logged in
print("didLoad, FB user not logged in")
}
}
func connectROS() {
let username = "realm-admin"
let password = "*********"
SyncUser.logIn(with: .usernamePassword(username: username, password: password, register: false), server: URL(string: "http://146.185.154.***:9080")!)
{ user, error in
print("ROS: checking user credentials")
if let user = user {
print("ROS: user credentials OK")
DispatchQueue.main.async {
// Opening a remote Realm
print("ROS: entering dispatch Q main async")
let realmURL = URL(string: "realm://146.185.154.***:9080/~/***book_test1")!
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: realmURL))
self.realmRos = try! Realm(configuration: config)
// Any changes made to this Realm will be synced across all devices!
}
} else if let error = error {
// handle error
print("ROS: user check FAIL")
fatalError(String(describing: error))
}
}
}
In viewDidLoad function next step is to get FB logged user (in this case I’m using FB authentication). After the logged FB user is fetched, the application perform check is that FB user is new user for my application and my proprietary ROS User’s table.
func checkForExistingProfile(user: User) -> Bool {
var userThatExist: User?
do {
try self.realmRos.write() {
userThatExist = self.realmRos.object(ofType: User.self, forPrimaryKey: user.userName)
}
} catch let error as NSError {
print("ROS is not connected")
print(error.localizedDescription)
}
if userThatExist != nil {
return true
} else {
return false
}
}
At this point checkForExistingProfile usually (not always) crashes at try self.realmRos.write() which happens to be nil.
I think the problem comes from the synchronization between connectROS execution (which is asynchrony) and checkForExistingProfile which execution comes before connectROS completion.
Since you didn't show how checkForExistingProfile() is called after viewDidLoad() this is conjecture, but based on everything else you described it's the likely cause.
What you need to do is not call checkForExistingProfile() until the sync user has logged in and your self.realmRos variable has been initialized. Cocoa Touch does nothing to automatically synchronize code written using an asynchronous pattern (like logIn(), which returns immediately but reports its actual completion state in a callback), so you need to manually ensure that whatever logIn() is supposed to do has been done before you call any additional code that depends on its completion.