How do you force the ViewController to export the data in 'b' instead of 'a' to the next VC? - swift

This is my first attempt at XCode, used to work on MatLab so very new to this. I have a VC here (named guests2ViewController) which I'm trying to send data to the next VC (named resultsViewController). Unfortunately the VC is exporting the data from the lines (print "b") instead of the earlier lines (print "a"). Why is this, and how could I fix it?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let vc = segue.destination as? resultsViewController
if segue.identifier == "toresults"{
//prioritise this part of code - call it part A
Database.database().reference(withPath: self.password).observeSingleEvent(of: .value, with: { snapshot in
//dates
var arr = [String]()
//times
var arr1 = [String]()
//number of attendees
var arr2 = [String]()
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot {
let newobj=rest.children
while let rest1 = newobj.nextObject() as? DataSnapshot {
arr.append(rest.key)
arr1.append(rest1.key)
arr2.append(String(rest1.childrenCount))
let guests = arr2.max()
var index = 0
for n in arr2 {
if n == guests {
//print (index)
let datefromarray = String(arr[index])
let timefromarray = String(arr1[index])
let guests1 = String(Int(guests!)!)
vc?.databasedate = (datefromarray)
vc?.databasetime = (timefromarray)
vc?.databaseguests = (guests1)
break
}
index += 1
}
}
}
print(vc?.databasedate)
print(vc?.databasetime)
print(vc?.databaseguests)
print("a")
//this doesn't get sent to the next VC after taking the data from Firebase
})
}
print(vc?.databasedate)
print(vc?.databasetime)
print(vc?.databaseguests)
print("b")
//this gets sent to the next VC
}
}

Data is loaded from Firebase (and most modern web APIs) asynchronously. This is to prevent blocking the user from using your app, while the (possibly slow) network request is in progress.
If you check in a debugger, you'll see that print("b") happens before vc?.databasedate = (datefromarray), which explains why you're passing the wrong value to the next view controller.
The solution is always the same: any code that needs data from the database needs to be inside the callback/closure/completion handler, or be called from there.
For more on this and code examples, see:
Firebase with Swift 3 counting the number of children
Why isn't my function that pulls information from the database working?
Access Firebase variable outside Closure
getting data out of a closure that retrieves data from firebase
Swift / How to use dispatch_group with multiple called web service?
Completion handler Firebase observer in Swift

Related

fatal errors with optionals not making sense

I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.
I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}
Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).

Assign username value from firestore

i'm creating my first app (and newbie in swift). When i login from Facebook, the name and email are saved in Firestore. I'm trying to set the name from facebook to a variable to use it in other places, but i can't assign the value, always shows "nil" in the console. Anyone can help me please?
I set the variable
var userN: String?
I get the data from Firestore
func readDatabase(){
let db = Firestore.firestore()
let docRef = db.collection("users").document("email")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
let data = document.data()
let userName = data!["name"]! as! String
print(userName)
let userEmail = data!["email"]! as! String
print(userEmail)
let containerController = ContainerController()
let containerController.userN = userName;
return
}
}
}
i want to assign userN = userName, to use it in other view
How can i do that? thanks
If you are using StoryBoards you can pass this through the segue function;
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "YourStoryBoardSegue" {
if let viewController = segue.destination as? ContainerController {
viewController.userN = userName
}
}
}
otherwise, best practice would be to use a delegate method.
Search stack overflow for best practices using delegates to pass data.
The question is extremely broad and without knowing details the only way to address it is with a general answer that specifically addresses how to read data from Firestore and save it in a variable to be used later.
Suppose your Firestore looks like this
root
users
uid_0
name: "users name"
uid_1
name: "another users name"
and when the app loads, we want to read the users name and store it in in a variable per your question:
a variable to use it in other places
Here's what that could look like
class ViewController: NSViewController {
var usersName = ""
override func viewDidLoad() {
super.viewDidLoad()
FirebaseApp.configure()
self.db = Firestore.firestore()
let settings = self.db.settings
self.db.settings = settings
self.readUserName()
}
func readUsersName() {
let users = self.db.collection("users")
let thisUser = users.document("uid_1")
thisUser.getDocument(completion: { documentSnapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
let name = documentSnapshot?.get("name") as! String
self.usersName = name
})
}
The code sets up Firestore, reads the user name from the uid_1 document and stores it in a variable where it could be used later.
Suppose we want to let the user change their name. There's 100 ways to do it; passing data via a segue, use a delegate method or open a detail view controller and before it closes, have this master controller read the updated name from a textField and save the data. You could even pass the users uid and then in the detail viewcontroller read the document via that uid and then update it upon closing. However, all of those go beyond the scope of the question.

Getting data out of a firebase function in Swift [duplicate]

In my iOS app, I have two Firebase-related functions that I want to call within viewDidLoad(). The first picks a random child with .queryOrderedByKey() and outputs the child's key as a string. The second uses that key and observeEventType to retrieve child values and store it in a dict. When I trigger these functions with a button in my UI, they work as expected.
However, when I put both functions inside viewDidLoad(), I get this error:
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
The offending line of code is in my AppDelegate.swift, highlighted in red:
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate
When I comment out the second function and leave the first inside viewDidLoad, the app loads fine, and subsequent calls of both functions (triggered by the button action) work as expected.
I added a line at the end of the first function to print out the URL string, and it doesn't have any offending characters: https://mydomain.firebaseio.com/myStuff/-KO_iaQNa-bIZpqe5xlg
I also added a line between the functions in viewDidLoad to hard-code the string, and I ran into the same InvalidPathException issue.
Here is my viewDidLoad() func:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
pickRandomChild()
getChildValues()
}
Here is the first function:
func pickRandomChild () -> String {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
})
return movieToGuess
}
Here is the second function:
func getChildValues() -> [String : AnyObject] {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
})
return movieDict
)
And for good measure, here's the relevant portion of my AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
return true
}
I'm guessing Swift is executing the code not in the order I expect. Does Swift not automatically wait for the first function to finish before running the second? If that's the case, why does this pairing work elsewhere in the app but not in viewDidLoad?
Edit: The issue is that closures are not called in order.
I'm not sure what your pickRandomChild() and getChildValues() methods are, so please post them as well, but the way I fixed this type issue was by sending the data through a closure that can be called in your ViewController.
For example when I wanted to grab data for a Full Name and Industry I used this. This method takes a Firebase User, and contains a closure that will be called upon completion. This was defined in a class specifically for pulling data.
func grabDataDict(fromUser user: FIRUser, completion: (data: [String: String]) -> ()) {
var myData = [String: String]()
let uid = user.uid
let ref = Constants.References.users.child(uid)
ref.observeEventType(.Value) { (snapshot, error) in
if error != nil {
ErrorHandling.defaultErrorHandler(NSError.init(coder: NSCoder())!)
return
}
let fullName = snapshot.value!["fullName"] as! String
let industry = snapshot.value!["industry"] as! String
myData["fullName"] = fullName
myData["industry"] = industry
completion(data: myData)
}
}
Then I defined an empty array of strings in the Viewcontroller and called the method, setting the variable to my data inside the closure.
messages.grabRecentSenderIds(fromUser: currentUser!) { (userIds) in
self.userIds = userIds
print(self.userIds)
}
If you post your methods, however I can help you with those specifically.
Edit: Fixed Methods
1.
func pickRandomChild (completion: (movieToGuess: String) -> ()) {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
// Put whatever you want to return here.
completion(movieToGuess)
})
}
2.
func getChildValues(completion: (movieDict: [String: AnyObject]) -> ()) {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
// Put whatever you want to return here.
completion(movieDict)
})
}
Define these methods in some model class, and when you call them in your viewcontroller, you should be able to set your View Controller variables to movieDict and movieToGuess inside each closure. I made these in playground, so let me know if you get any errors.
Your functions pickRandomChild() and getChildValues() are asynchronous, therefore they only get executed at a later stage, so if getChildValues() needs the result of pickRandomChild(), it should be called in pickRandomChild()'s completion handler / delegate callback instead, because when one of those are called it is guaranteed that the function has finished.
It works when you comment out the second function and only trigger it with a button press because there has been enough time between the app loading and you pushing the button for the asynchronous pickRandomChild() to perform it action entirely, allowing getChildValues() to use its returned value for its request.

Query for data to add data to prepareForSegue function

I am attempting to query for data from a Parse table to add it to a prepareForSegue function. But once I go into the newViewController the label is blank. Here's my line of code.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "marathonDetail"){
var upcoming: marathonDetailViewController = segue.destinationViewController as! marathonDetailViewController
let indexPath = self.marathonsTableView.indexPathForSelectedRow!
let currentCell = marathonsTableView.cellForRowAtIndexPath(indexPath) as! marathonTableViewCell
let marathonEvents = currentCell.marathonName.text
upcoming.nameMarathon = marathonEvents
self.marathonsTableView.deselectRowAtIndexPath(indexPath, animated: true)
var query = PFQuery(className: "marathons")
query.whereKey("marathonName", equalTo: marathonEvents!)
query.findObjectsInBackgroundWithBlock{
(marathonPickeds: [PFObject]?, error: NSError?) -> Void in
if (error == nil){
if let marathonPicked = marathonPickeds as? [PFObject]?{
for marathonPicked in marathonPickeds!{
var selectedDescription = marathonPicked.description
upcoming.marathonDescription = selectedDescription
print(selectedDescription)
}
}
}else {
print(error)
}
}
}
}
The marathonsEvents= currentCell.marathonName.text works well but the marathonDescription is blank.
Any advice? I am using Parse as my backend XCODE 7, and swift
You're performing a network call on a background thread so by the time its finished you've already completed the segue. What you probably want to do is:
Pull that query logic out into a separate class, get it out of your view controllers.
In this instance perform the request in the view controller that is being pushed to. You can start it in viewWillAppear and refresh your view when its finished. It looks like it has all the information it needs to perform the request using just the marathonEvents.

Realm causing program to behave unexpectedly. (OS X and Swift)

So I am creating a fairly simple program using Realm as my database. I am fairly new to programing in Swift (or any OS X or iOS environment.) In my program when a button is pressed IBAction func createInvoice I want a few things to happen, I want to count the previous rows in the database and create an invoice number, I want to write new data to the database and I want to call a new view and view controller and pass along the invoice number. My code works except for one thing when using Realm the new view controller is called (override func prepareForSegue) before the invoice number is created so a 0 value is passed along to the new view controller.
If I create a dummy invoice number value such as let invoicenumber = 42 everything works perfectly. It seems that Realm is causing things to happen 'out of order' How can I make the veiwcontroller wait for a value before loading?
#IBAction func createInvoice(sender: AnyObject) {
let realm = Realm()
let invoicepull = Invoice()
let invoicecount = realm.objects(Invoice)
let invoicenraw = invoicecount.count
let a = 100
let invoicenumber = a + invoicenraw
var invoicefile = Invoice()
invoicefile.inumber = invoicenumber
invoicefile.cnumber = clientcombo.stringValue
invoicefile.cost = owed.doubleValue
invoicefile.paid = paid.doubleValue
invoicefile.sevicecode = service.stringValue
invoicefile.dateofservice = NSDate()
// Save your object
realm.beginWrite()
realm.add(invoicefile)
realm.commitWrite()
//Sent notification
performSegueWithIdentifier("cinvoiceseuge", sender: nil)
println("Inside Action")
println(invoicenumber)
dismissViewController(self)
}
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "cinvoiceseuge") {
//Checking identifier is crucial as there might be multiple
// segues attached to same view
var detailVC = segue.destinationController as! invociegenerator;
detailVC.toPass = invoicenumber
println("Inside Sugue")
println(invoicenumber)
}
}
If createInvoice is happening on a different thread than prepareForSegue, you'll have to refresh the realm (Realm().refresh()) before accessing your invoicenumber variable (which I assume is of type RealmSwift.Object).
I have solved this issue, thanks to the help of #Shmidt by using Realm's built in notification center. To use the notifications you can use this basic structure.
var notificationToken: NotificationToken?
deinit{
let realm = Realm()
if let notificationToken = notificationToken{
realm.removeNotification(notificationToken)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let realm = Realm()
notificationToken = realm.addNotificationBlock { note, realm in
println("The realm is complete")
}
...
}
One small other error in my code was let invoicenumber = a + invoicenraw I needed to drop the let as it is a variable and not a constant.