SearchBar problem while trying to search Firestore and reload the tableview - swift

I have a tableView and I use infinite scroll to populate firestore data with batches. Also I have a searched bar and I am trying to query firestore with the text from the text bar and then populate it in the tableview. I have 3 main problems.
When I click search thee first time I get an empty array and an empty tableview, but when I click search the second time everything seems fine.
When I finally populate the searched content I want to stop fetching new content while I am scrolling.
If I text a wrong word and press search then I get the previous search and then the "No Ingredients found" printed twice.
This is my code for searchBar:
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let text = searchBar.text else {return}
searchIngredients(text: text)
self.searchBarIngredient.endEditing(true)
print("\(searchIngredients(text: text))")
}
The code for function when I click search
func searchIngredients(text: String) -> Array<Any>{
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
self.searchedIngredientsArray = querySnapshot!.documents.compactMap({Ingredients(dictionary: $0.data())})
}else{
print("No Ingredients found")
}
}
}
self.tableView.reloadData()
ingredientsArray = searchedIngredientsArray
return ingredientsArray
}
Finally the code for scrolling
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching{
if !fetchMoreIngredients && !reachEnd{
beginBatchFetch()
}
}
}
I don't write the beginBatchFetch() cause its working fine and I don't think is relevant.
Thanks in advance.

The issue in your question is that Firestore is asynchronous.
It takes time for Firestore to return documents you've requested and that data will only be valid within the closure calling the function. The code outside the closure will execute way before the data is available within the closure.
So here's what's going on.
func searchIngredients(text: String) -> Array<Any>{
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
}
//the code below here will execute *before* the code in the above closure
self.tableView.reloadData()
ingredientsArray = searchedIngredientsArray
return ingredientsArray
}
what's happening is the tableView is being refreshed before there's any data in the array.
You're also returning the ingredientsArray before it's populated. More importantly, attempting to return a value from an asynchronous function can (and should) generally be avoided.
The fix is to handle the data within the closure
class ViewController: NSViewController {
var ingredientArray = [String]()
func searchIngredients(text: String) {
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
//populate the class var array with data from firebase
// self.ingredientArray.append(some string)
//refresh the tableview
}
}
Note that the searchIngredients function should not return a value - nor does it need to

Related

Firestore async issue

I'm calling a Firestore query that does come back, but I need to ensure completion before moving on with the rest of the code. So I need a completion handler...but for the life of me I can't seem to code it.
// get user info from db
func getUser() async {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
}
}
}
}
Called by:
internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
db = Firestore.firestore()
Database.database().isPersistenceEnabled = true
Task {
do {
let userInfo = await getUser()
}
} return true }
I used a Task as didFinishLauncingWithOptions is synchronous and not asynchronous
However, the getUser() still isn't completed before didFinishLauncingWithOptions moves on.
I need the data from getUser as the very next step uses the data in the array, and without it I get an 'out of bounds exception' as the array is still empty.
Also tried using dispatch group within the func getUser(). Again with no joy.
Finally tried a completion handler:
func getUser(completion: #escaping (Bool) -> Void) {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
completion(true)
}
}
}
}
Nothing works. The getUser call isn't completed before the code moves on. Can someone please help. I've searched multiple times, looked at all linked answers but I can not make this work.I'm clearly missing something easy, please help
read this post: Waiting for data to be loaded on app startup.
It explains why you should never wait for data before returning from
function application(_:didFinishLaunchingWithOptions).
To achieve what you need, you could use your first ViewController as a sort of splashscreen (that only shows an image or an activity indicator) and call the function getUser(completion:) in the viewDidLoad() method the ViewController.
Example:
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
MyFirestoreDatabaseManager.shared.getUser() { success in
if success {
//TODO: Navigate to another ViewController
} else {
//TODO: Show an error
}
}
}
}
Where obviously MyFirestoreDatabaseManager.shared is the object on which you defined the getUser(completion:) method.
(In your example, I think that you defined that function in the AppDelegate. In that case, you should mark your getUser(completion:) method and all related variables as static. Then replace MyFirestoreDatabaseManager.shared with AppDelegate).
Not 100% sure what you would like to accomplish as I can't see all your code, but try something similar to this, replacing Objects for what you are trying to return from the documents.
You don't want your user's data spread across multiple documents. With Firebase you pay for every document you have to get. Ideally you want all your user's settings within one firebase document. Then create a UserInfo struct that you can decode to using the library CodeableFirebase or the decoder of your choice.
// Create user struct
struct UserInfo: Codable {
var userId: String
var userTrust: String
var userGrade: String
var userDisclaimer: String
}
// get user info from db and decode using CodableFirebase
func getUser() async throws -> UserInfo {
let doc = try await self.db.collection("users").document("userIdHere")
let userInfo = try FirestoreDecoder().decode(UserInfo.self, from: doc.data())
return UserInfo
}
Then you can do this...
Task {
do {
let userInfo = try await getUser()
let userTrust = userInfo.userTrust
let userGrade = userInfo.userGrade
let userDisclaimer = userInfo.userDisclaimer
}
}

Download single Object of Firestore and save it into an struct/class object

I am coding since January 2019 and this is my first post here.
I am using Swift and Firestore. In my App is a tableView where I display events loaded out of a single Document with an array of events inside as [String: [String:Any]]. If the user wants to get more infos about an event he taps on it. In the background the TableViewController will open a new "DetailEventViewController" with a segue and give it the value of the eventID in the tapped cell.
When the user is on the DetailViewController Screen the app will download a new Document with the EventID as key for the document.
I wanna save this Data out of Firestore in a Struct called Event. For this example just with Event(eventName: String).
When I get all the data I can print it directly out but I can't save it in a variable and print it out later. I really don't know why. If I print the struct INSIDE the brackets where I get the data its working but if I save it into a variable and try to use this variable it says its nil.
So how can I fetch data out of Firestore and save in just a Single ValueObject (var currentEvent = Event? -> currentEvent = Event.event(for: data as [String:Any]) )
I search in google, firebaseDoc and stackoverflow but didn't find anything about it so I tried to save all the singe infos of the data inside a singe value.
// Struct
struct Event {
var eventName: String!
static func event(for eventData: [String:Any]) -> Event? {
guard let _eventName = eventData["eventName"] as? String
else {
print("error")
return nil
}
return Event(eventName: _eventName)
}
// TableView VC this should work
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowEventDetailSegue" {
if let ShowEvent = segue.destination as? DetailEventViewController, let event = eventForSegue{
ShowEvent.currentEventId = event.eventID
}
}
}
// DetailViewController
var currentEvent = Event()
var currentEventId: String?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let _eventID = currentEventId else {
print("error in EventID")
return}
setupEvent(eventID: _eventID) /* currentEvent should be set here */
setupView(event: currentEvent) /* currentEvent has after "setupEvent" the value of nil */
}
func setupEvent(eventID: String) {
let FirestoreRef = Firestore.firestore().collection("events").document(eventID)
FirestoreRef.getDocument { (document, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
SVProgressHUD.showError(withStatus: "Error in Download")
}else {
if let document = document, document.exists {
guard let data = document.data() else {return}
let eventData = Event.event(for: data as [String:Any])
print(eventData)
//here all infos are printed out - so I get them
self.currentEvent = eventData!
//Here is the error.. I can't save the fetched Data in my single current Event
} else {
SVProgressHUD.showError(withStatus: "Error")
}
}
}
}
func setupView(event: Event) {
self.titleLabel.text = event.eventName
}
I expect that the function setupEvents will give the currentEvent in the DetailViewController a SINGLEvalue cause its a SINGLE document not an array. So I can use this single Eventvalue for further actions. Like starting a new segue for a new ViewController and just push the Event there not

Populating UITableView from Firestore with query cursors (paging)

I have a UITableView subclass that I am populating from a Firestore collection. I only grab 20 documents at a time, and use the UITableViewDataSourcePrefetching delegate to get the next "page" of 20 documents from Firestore when the user nears the end of the loaded array.
The following is my code that almost perfectly achieves this, omitting/obfuscating where appropriate.
class MyCustomViewController: UIViewController {
#IBOutlet weak var myCustomTableView: MyCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
myCustomTableView.query = Firestore.firestore().collection("MyCollection").whereField("foo", isEqualTo: "bar").order(by: "timestamp", descending: true)
myCustomTableView.fetchNextPage()
}
}
extension MyCustomViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard let tableView = tableView as? MyCustomTableView else {
return
}
tableView.prefetch(indexPaths: indexPaths)
}
}
class MyCustomTableView: UITableView {
var documents = [DocumentSnapshot]()
var query: Query?
let querySize = 20
private var fetchSemaphore = DispatchSemaphore(value: 1)
public func prefetch(indexPaths: [IndexPath]) {
for indexPath in indexPaths {
if indexPath.section >= documents.count - 1 {
fetchNextPage()
return
}
}
}
public func fetchNextPage() {
guard let query = query else {
return
}
guard documents.count % querySize == 0 else {
print("No further pages to fetch.")
return
}
guard fetchSemaphore.wait(timeout: .now()) == .success else { return }
if self.documents.isEmpty {
query.limit(to: querySize).addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
return
}
self.documents.append(contentsOf: snapshot.documents)
self.reloadData()
self.fetchSemaphore.signal()
}
}
else {
// I think the problem is on this next line
query.limit(to: querySize).start(afterDocument: documents.last!).addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
return
}
for document in snapshot.documents {
if let index = self.documents.firstIndex(where: { $0.documentID == document.documentID }) {
self.documents[index] = document
}
else {
self.documents.append(document)
}
}
self.reloadData()
self.fetchSemaphore.signal()
}
}
}
}
I think I know what/where the problem is (see comment in fetchNextPage()), but I may be wrong. Even if I am correct, I have not been able to come up with a way to fix it.
My query is sorted, in descending order, by the documents' timestamp value which, as the name suggests, represents the time at which the document was created. This means that, in the table view, the newest documents will appear at the top, and the oldest at the bottom.
Everything works great, except for...
The problem: When a new document is created, every item in the table gets shunted down a row because the new document has the newest timestamp and gets placed at the top. Great, except that when this happens, all of the query snapshot listeners (except for the first one) are no longer using the correct document to start after. The document snapshot originally retrieved with documents.last! is no longer the correct document that the query should start after. I do not know how to change the startAfter parameter of an existing snapshot listener. I could not find any method belonging to the query that I could use to alter this.
Again, this might not actually be the reason I'm seeing things out of order when new documents are added, but I think it is. If anyone has advice for how to resolve this I'd greatly appreciate it.
References I have used to get here:
https://firebase.google.com/docs/firestore/query-data/query-cursors
https://www.raywenderlich.com/5786-uitableview-infinite-scrolling-tutorial
Additional notes:
Even though it'd be much easier and solve this problem, I do not want to use getDocuments on the query. Having the near-real-time updates is important in this case.
I cannot use FirebaseUI because my use-case requires functionality not yet available. For example, you may have noticed that I used indexPath.section instead of indexPath.row, which is because the table is made up of many single-rowed sections so that vertical padding can be put between cells.

Swift 4 - DynamoDB data not showing up in TableView

I am trying to point an existing table view to the new DynamoDB database. The AWS DynamoDB call populates an array of dictionaries variable in tableview but the simulator is showing the data. I have spent several days trying with asynchronous function call with a completion closure without success. Now I got rid of the custom function and directly using the AWS closure in viewDidLoad() of table view. Any help is appreciated.
Here is the table view code:
override func viewDidLoad() {
super.viewDidLoad()
//dynamodb call
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
dynamoDBObjectMapper.scan(Employees.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
}
let paginatedOutput = task.result!
for emp in paginatedOutput.items as! [Employees] {
self.myVariables.empDict["empid"] = emp._empid
self.myVariables.empDict["email"] = emp._email
self.myVariables.empDict["firstname"] = emp._firstname
self.myVariables.empDict["lastname"] = emp._lastname
self.myVariables.empDict["location"] = emp._location
self.myVariables.empDict["mobile"] = emp._mobile
self.myVariables.empDict["work"] = emp._work
self.myVariables.empDict["site"] = emp._site
self.myVariables.arrayEmployees.append(self.myVariables.empDict)
//print(self.myVariables.arrayEmployees) // this works
} // for loop
self.employeeSearch = self.myVariables.arrayEmployees
print("printing employeeSearch")
print(self.employeeSearch) // This works
// self.employee1View.reloadData()
// tried reloading here (above - showing here as commented), but getting error: UITableView.reloadData() must be used from main thread only
return nil
} // dynamoDBObjectMapper.scan
// self.employee1View.reloadData()
// Then I tried reload above (showing here as commented), but I get error : Expected ',' separator. If I accept compiler's suggestion, it puts a comma just after curly braces above and that causes more errors at the Line dynamoDBObjectMapper.scan error : Cannot invoke 'continueWith' with an argument list of type '(block: (AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any?, Void)'
) // .continueWith
// end dynamodb call
// other things in this overload function
// ..
}
}
Then, when I run the code, I add a print command in override tableview, but it is showing a blank array like [].
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("printing employeSearch from numberOfRowsInSection")
print(employeeSearch) // This returns blank like []
if isSearching {
return currentEmployeeSearch.count
}
return employeeSearch.count
}
I tried reload of table view in multiple tries, but that didn't help either.
Here is the search feature having the isSearching variable
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar.text == nil || searchBar.text == "" {
isSearching = false
view.endEditing(true)
employee1View.reloadData()
} else {
isSearching = true
employeeSearch = empListDict // getSwiftArrayFromPlist()
currentEmployeeSearch = employeeSearch.filter {($0["lastname"]?.lowercased().contains(searchText.lowercased())) ?? false}
employee1View.reloadData()
}
}
ok tried to reload your data after you get your data, for example in your Closure.
You have to reload your data in your main thread like that :
DispatchQueue.main.async {
self.employee1View.reloadData()
}

How to display CloudKit RecordType instances in a tableview controller

To my knowledge, the following code (or very close to it) would retrieve one cloudkit instance from the recordtype array...
let pred = NSPredicate(value: true)
let query = CKQuery(recordType: "Stores", predicate: pred)
publicDatabase.performQuery(query, inZoneWithID: nil) { (result, error) in
if error != nil
{
print("Error" + (error?.localizedDescription)!)
}
else
{
if result?.count > 0
{
let record = result![0]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.txtDesc.text = record.objectForKey("storeDesc") as? String
self.position = record.objectForKey("storeLocation") as! CLLocation
let img = record.objectForKey("storeImage") as! CKAsset
self.storeImage.image = UIImage(contentsOfFile: img.fileURL.path!)
....(& so on)
However, how and when (physical location in code?) would I query so that I could set each cell to the information of each instance in my DiningType record?
for instance, would I query inside the didreceivememory warning function? or in the cellforRowatIndexPath? or other!
If I am misunderstanding in my above code, please jot it down in the notes, all help at this point is valuable and extremely appreciated.
Without a little more information, I will make a few assumptions about the rest of the code not shown. I will assume:
You are using a UITableView to display your data
Your UITableView (tableView) is properly wired to your viewController, including a proper Outlet, and assigning the tableViewDataSource and tableViewDelegate to your view, and implementing the required methods for those protocols.
Your data (for each cell) is stored in some type of collection, like an Array (although there are many options).
When you call the code to retrieve records from the database (in this case CloudKit) the data should eventually be stored in your Array. When your Array changes (new or updated data), you would call tableView.reloadData() to tell the tableView that something has changed and to reload the cells.
The cells are wired up (manually) in tableView(:cellForRowAtIndexPath:). It calls this method for each item (provided you implemented the tableView(:numberOfRowsInSection:) and numberOfSectionsInTableView(_:)
If you are unfamiliar with using UITableView's, they can seem difficult at first. If you'd like to see a simple example of wiring up a UITableView just let me know!
First, I had to take care of the typical cloudkit requirements: setting up the container, publicdatabase, predicate, and query inputs. Then, I had the public database perform the query, in this case, recordtype of "DiningType". Through the first if statement of the program, if an error is discovered, the console will print "Error" and ending further action. However, if no run-time problem is discovered, each result found to be relatable to the query is appended to the categories array created above the viewdidload function.
var categories: Array<CKRecord> = []
override func viewDidLoad() {
super.viewDidLoad()
func fetchdiningtypes()
{
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "DiningType", predicate: predicate)
publicDatabase.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil
{
print("Error")
}
else
{
for result in results!
{
self.categories.append(result)
}
NSOperationQueue.mainQueue().addOperationWithBlock( { () -> Void in
self.tableView.reloadData()
})
}
}
}
fetchdiningtypes()