here is relevant for my question part of my code and it works almost fine, except one thing. The data is always sorted randomly. About code: i should receive data (name of CoffeeShop and .jpg) from firebase and display the foto in tableView
var coffeeShopLists: [CoffeeShopList] = []
struct CoffeeShopList {
let name: String
let shopImage: UIImage
}
func loadShops () {
coffeeShopLists = []
db.collection("coffeeShop").getDocuments { (querySnapshot, error) in
if let e = error {
print("Error\(e)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
print(doc.data())
if let shop = data["name"] as? String {
print(shop)
self.getFoto(named: shop)
// self.coffeeShopLists.append(shopList) //retrieving Data from firebase
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
}
func getFoto (named: String) {
let storage = Storage.storage().reference().child("CoffeeShops/\(named).jpg")
storage.getData(maxSize: 1 * 1024 * 1024) {data, error in
if let error = error {
return print(error)
} else {
if let image = UIImage(data: data!) {
let newList = CoffeeShopList(name: named, shopImage: image)
self.coffeeShopLists.append(newList)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CoffeeShopCell", bundle: nil), forCellReuseIdentifier: "ReusableCell")
loadShops()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell", for: indexPath) as! CoffeeShopCell
cell.shopImage.image = coffeeShopLists[indexPath.row].shopImage
return cell
}
How i said: it works. but it should be sorted how it sorted in firebase collection.But its not. And sorry for my english.(
after editing code like in comments bellow ordering by Position.Here is the result of print statements:
["position": 1, "name": soren]
soren
["position": 2, "name": ramozoti]
ramozoti
["position": 3, "name": coffeesoul]
coffeesoul //until this part the result is always the same(correct) and it should be by this order. But the results bellow is always in random order. That part belong to function getFoto()
coffeesoul
[CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: <UIImage:0x600000e17600 anonymous {1080, 1080}>)]
ramozoti
[CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: <UIImage:0x600000e17600 anonymous {1080, 1080}>), CoffeeShop.CoffeeShopList(name: "ramozoti", shopImage: <UIImage:0x600000e17840 anonymous {621, 621}>)]
soren
[CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: <UIImage:0x600000e17600 anonymous {1080, 1080}>), CoffeeShop.CoffeeShopList(name: "ramozoti", shopImage: <UIImage:0x600000e17840 anonymous {621, 621}>), CoffeeShop.CoffeeShopList(name: "soren", shopImage: <UIImage:0x600000e10000 anonymous {1080, 1080}>)]
the struct feels each time by the random order
So, im on the right way. The last question to finish it:How i can sort this struct by the third value
[CoffeeShop.CoffeeShopList(name: "ramozoti", shopImage: Optional(<UIImage:0x600003b27de0 anonymous {621, 621}>), position: 2), CoffeeShop.CoffeeShopList(name: "soren", shopImage: Optional(<UIImage:0x600003b34480 anonymous {1080, 1080}>), position: 1), CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: Optional(<UIImage:0x600003b31b00 anonymous {1080, 1080}>), position: 3)]
This is it, its working now. i made extra function
func sort () {
sortedShopLists = coffeeShopLists.sorted {$0.position < $1.position}
}
and passt it into getFoto() function after self.coffeeShopLists.append(newList)
maybe it was somewhere more elegant way to solve my problem but i found just this.
The Firestore console orders the documents by their key. If you want the same in your code, you can
db.collection("coffeeShop")order(by: FieldPath.documentID()).getDocuments(...
Also see: Order by document id firestore?
Your update and comments point towards this being about the loading of the photo's related to those documents.
The loading of the photos starts in the correct order. But since each photo takes a different amount of time to load, it may complete in another order.
If you want the photos to appear in the order that you had the documents, you should make sure you work from a list that maintains the document order.
For example, you could add each document to coffeeShopLists before you start loading its photo, and then update the item in coffeeShopLists with the photo.
So in your loadFotos you'd do sometihng like this:
for doc in snapshotDocuments {
let data = doc.data()
if let shop = data["name"] as? String {
self.coffeeShopLists.append(shopList)
self.getFoto(named: shop, self.coffeeShopLists.length - 1)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
The change here is that we're passing the index of the document into the getFoto function, so that it knows which element in the array to update.
Then in getFoto you'd fine the array item and update that in stead of adding a new item.
If you don't specify an order of documents in the query, Firestore doesn't guarantee one. Documents are not fundamentally ordered except by what you specify in the query. So, if ordering is important to you, you'll have to arrange for that yourself.
I am a learner on Swift and was facing the same problem, i figured out the way by using .order
db.collection(Constants.FStore.collectionName).order(by: Constants.FStore.dateField).addSnapshotListener { (querySnapShot, error) in
while persisting, i persisted a date field also and I am ordering by that. You can order by a different field.
your code could be
db.collection("coffeeShop").order(by: <<your field>>).getDocuments { (querySnapshot, error)
As I said, I am learner of Swift but I hope this would help.
Related
I'm quite new to Swift and currently dealing with the Firebase-Database.
I managed to realise the functions that I want to have, but my implementation feels not right.
Most I am struggling with the closures, that I need to get data from Firebase.
I tried to follow the MVC approach and have DataBaseManager, which is getting filling my model:
func getCollectionData(user: String, callback: #escaping([CollectionData]) -> Void) {
var dataArray: [CollectionData] = []
var imageArray:[String] = []
let db = Firestore.firestore()
db.collection(user).getDocuments() { (QuerySnapshot, err) in
if let err = err {
print("Error getting documents : \(err)")
}
else {
for document in QuerySnapshot!.documents {
let album = document.get("album") as! String
let artist = document.get("artist") as! String
let genre = document.get("genre") as! String
let location = document.get("location") as! String
var a = CollectionData(album: album, artist: artist, imageArray: imageArray, genre: genre, location: location)
a.imageArray!.append(document.get("fronturl") as? String ?? "No Image")
a.imageArray!.append(document.get("backurl") as? String ?? "No Image")
a.imageArray!.append(document.get("coverlurl") as? String ?? "No Image")
dataArray.append(a)
}
callback(dataArray)
}
}
}
With this I'm getting the information and the downloadlinks, which I later use in a gallery.
Is this the right way?
I feel not, because the fiddling starts, when I fetch the data from my ViewController:
var dataArray = []
dataBaseManager.getCollectionData(user: user) { data in
self.dataArray = data
I can see, that I sometimes run into problems with timing, when I use data from dataArray immediately after running the closure.
My question is, this a valid way to handle the data from Firebase or is there a more elegant way to achieve this?
You are on the right track. However, using dataArray immediately is where the issue could be.
Let me provide a high level example with some pseudo code as a template:
Suppose you have an ToDo app; the user logs in and the first view they see is all of their current To Do's
class viewController
var dataArray = [ToDo]() //a class property used as a tableViewDataSource
#IBOutlet toDoTableView
viewDidLoad {
loadToDos()
}
func loadToDos() {
thisUsersToDoCollection.getDocuments() { documents in
self.array.append( the to Do Documents)
self.toDoTableView.reloadData()
}
}
}
With the above template you can see that within the Firebase .getDocuments function, we get the To Do documents from the colleciton, populate the dataSource array and THEN reload the tableView to display that data.
Following this design pattern will alleviate this issue
I sometimes run into problems with timing, when I use data from
dataArray immediately after running the closure
Because the user cannot interact with the data until it's fully loaded and populated within the tableView.
You could of course do a callback within the loadToDos function if you prefer so it would then be called like this - only reload the tableView once the loadToDos function has completed
viewDidLoad {
loadToDos {
toDoTableView.reloadData()
}
}
The big picture concept here is that Firebase data is ONLY VALID WITHIN THE CLOSURE following the Firebase call. Let that sequence provide pacing to your app; only display info if it's valid, only allow the user to interact with the data when it's actually available.
So I have an app that lets you search for words and gives you the definition and other details about it. I use 2 different apis so if you use a wildcard search (e.g. ?OUSE) you get all the possibilities and if you put in the whole word you just get the one result.
To cut down on API usage, when run for a single word I collect all the data in a WordDetail object. Similary, when run for a wildcard search, each word is created as a WordDetail object but with a lot of missing data.
My plan is, for wildcard search, when you select a specific word, it'll then go use the API again and retrieve the data, update the array, and then navigate you to the DetailViewController.
My problem is (I think), it's navigating to the DetailViewController before it's updated the array. Is there a way to make things wait before it has all the information before navigating?
The did select function looks like this...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var word: WordDetails?
word = results[indexPath.row]
if word?.results.count == 0 {
fetchWords(word: word!.word, updating: true)
}
print(word?.word)
print(word?.pronunciation)
let detailVC = TabBarController()
detailVC.selectedWord = word
detailVC.navigationItem.title = word?.word.capitalizingFirstLetter()
tableView.deselectRow(at: indexPath, animated: true)
navigationController?.pushViewController(detailVC, animated: true)
}
and the update to the array happens here...
func parseJSON(resultData: Data, update: Bool){
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WordDetails.self, from: resultData)
if update {
if let row = self.results.firstIndex(where: {$0.word == decodedData.word}) {
self.results[row] = decodedData
}
} else {
results.append(decodedData)
DispatchQueue.main.async {
self.resultLabel.text = "\(self.results.count) results found"
self.resultsView.reloadData()
}
}
} catch {
print(error)
}
}
could be a timing problem or might be a stupid approach. My alternative is to just run the api call again in the DetailViewController for words that are missing data as they came from a wildcard search.
UPDATE
Found a solution using DispatchSemaphore
Define it globally
let semaphore = DispatchSemaphore(value: 0)
Ask the code to wait for a signal before proceding, which I added to the didSelectRowAt function
_ = semaphore.wait(timeout: .distantFuture)
And then send a signal when the code has done what it needed to do e.g.
if update {
if let row = self.results.firstIndex(where: {$0.word == decodedData.word}) {
self.results[row] = decodedData
DispatchQueue.main.async {
self.resultsView.reloadData()
}
semaphore.signal()
}
}
Which then allows the rest of the code to carry on. Has worked perfectly in the few test cases I've tried.
I'm trying to populate the Sections and Rows of my tableview using Firestore data that I've parsed and stored inside of a dictionary, that looks like this...
dataDict = ["Monday": ["Chest", "Arms"], "Wednsday": ["Legs", "Arms"], "Tuesday": ["Back"]]
To be frank, I'm not even sure if I'm supposed to store the data inside of a dictionary as I did. Is is wrong to do that? Also, since the data is being pulled asynchronously, how can I populate my sections and rows only after the dictionary is fully loaded with my network data? I'm using a completion handler, but when I try to print the results, of the dataDict, it prints out three arrays in succession, like so...
["Monday": ["Chest", "Arms"]]
["Tuesday": ["Back"], "Monday": ["Chest", "Arms"]]
["Tuesday": ["Back"], "Monday": ["Chest", "Arms"], "Wednsday": ["Legs", "Arms"]]
Whereas I expected it to return a single print of the array upon completion. What am I doing wrong?
var dataDict : [String:[String]] = [:]
//MARK: - viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
vcBackgroundImg()
navConAcc()
picker.delegate = self
picker.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
tableView.tableFooterView = UIView()
Auth.auth().addStateDidChangeListener { (auth, user) in
self.userIdRef = user!.uid
self.colRef = Firestore.firestore().collection("/users/\(self.userIdRef)/Days")
self.loadData { (done) in
if done {
print(self.dataDict)
} else {
print("Error retrieving data")
}
}
}
}
//MARK: - Load Data
func loadData(completion: #escaping (Bool) -> ()){
self.colRef.getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
completion(false)
}
else {
//Appending all Days collection documents with a field of "dow" to daysarray...
for dayDocument in snapshot!.documents {
self.daysArray.append(dayDocument.data()["dow"] as? String ?? "")
self.dayIdArray.append(dayDocument.documentID)
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
completion(false)
}
else {
//Assigning all Workouts collection documents belonging to selected \(dayDocument.documentID) to dictionary dataDict...
for document in snapshot!.documents {
if self.dataDict[dayDocument.data()["dow"] as? String ?? ""] == nil {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""] = [document.data()["workout"] as? String ?? ""]
} else {
self.dataDict[dayDocument.data()["dow"] as? String ?? ""]?.append(document.data()["workout"] as? String ?? "")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
// print(self.dataDict)
}
completion(true)
}
}
}
self.dayCount = snapshot?.count ?? 0
}
}
}
I think it is just the flow of your program. Every time you loop through the collection, you add what it gets to the dictionary. So on the first pass, it will print that the dictionary has 1 item. On the second pass, it adds another item to the dictionary, and then prints the dictionary, which now has 2 items in it, so 2 items are printed. I don't think you are seeing unexpected behavior, it is just how you have your log statement ordered with how you are looping.
In other words, it makes sense that it is printing like that.
I agree with #ewizard's answer. The problem is in the flow of your program. You iterate through the documents and you fetch the documents in the collection for every iteration. You also reload the tableView and call completion closure multiple times, which you don't want to do.
To improve the flow of your program try using DispatchGroup to fetch your data and then put it together one once, when all the data is fetched. See example below to get the basic idea. My example is a very simplified version of your code where I wanted to show you the important changes you should preform.
func loadData(completion: #escaping (Bool) -> ()) {
self.colRef.getDocuments { (snapshot, err) in
// Handle error
let group = DispatchGroup()
var fetchedData = [Any]()
// Iterate through the documents
for dayDocument in snapshot!.documents {
// Enter group
group.enter()
// Fetch data
Firestore.firestore().collection("/users/\(self.userIdRef)/Days/\(dayDocument.documentID)/Workouts/").getDocuments { (snapshot, err) in
// Add your data to fetched data here
fetchedData.append(snapshot)
// Leave group
group.leave()
}
}
// Waits for until all data fetches are finished
group.notify(queue: .main) {
// Here you can manipulate fetched data and prepare the data source for your table view
print(fetchedData)
// Reload table view and call completion only once
self.tableView.reloadData()
completion(true)
}
}
}
I also agree with other comments that you should rethink the data model for your tableView. A much more appropriate structure would be a 2d array (an array of arrays - first array translates to the table view sections and the inner array objects translate to section items).
Here's an example:
// Table view data source
[
// Section 0
[day0, day1],
// Section 1
[day2, day3],
// Section 2
[day4, day5],
]
Example of usage:
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].count
}
}
I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX") inside my "Wishlists" - collection.
I tried this but that's not allowed:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").document(list.name).collection("wünsche").getDocuments()
This is my function:
func getWishes (){
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
var counter = 0
for list in self.dataSourceArray {
print(list.name) // -> right order
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments() { ( querySnapshot, error) in
print(list.name) // wrong order
if let error = error {
print(error.localizedDescription)
}else{
// DMAG - create a new Wish array
var wList: [Wish] = [Wish]()
for document in querySnapshot!.documents {
let documentData = document.data()
let wishName = documentData["name"]
wList.append(Wish(withWishName: wishName as! String, checked: false))
}
// DMAG - set the array of wishes to the userWishListData
self.dataSourceArray[counter].wishData = wList
counter += 1
}
}
}
}
This is what I actually would like to achieve in the end:
self.dataSourceArray[ListIDX].wishData = wList
Update
I also have a function that retrieves my wishlists in the right order. Maybe I can add getWishesin there so it is in the right order as well.
func retrieveUserDataFromDB() -> Void {
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
db.collection("users").document(userID).collection("wishlists").order(by: "listIDX").getDocuments() { ( querySnapshot, error) in
if let error = error {
print(error.localizedDescription)
}else {
// get all documents from "wishlists"-collection and save attributes
for document in querySnapshot!.documents {
let documentData = document.data()
let listName = documentData["name"]
let listImageIDX = documentData["imageIDX"]
// if-case for Main Wishlist
if listImageIDX as? Int == nil {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: UIImage(named: "iconRoundedImage")!, wishData: [Wish]()))
// set the drop down menu's options
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(UIImage(named: "iconRoundedImage")!)
}else {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: self.images[listImageIDX as! Int], wishData: [Wish]()))
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(self.images[listImageIDX as! Int])
}
// // create an empty wishlist
// wList = [Wish]()
// self.userWishListData.append(wList)
// reload collectionView and tableView
self.theCollectionView.reloadData()
self.dropDownButton.dropView.tableView.reloadData()
}
}
self.getWishes()
}
For a better understanding:
git repo
As #algrid says, there is no sense to order the collection using order() if you are going to get an specific element using list.name at the end, not the first or the last. I would suggest to change your code to:
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments()
I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX")
The following line of code will definitely help you achieve that:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").getDocuments() {/* ... */}
Adding another .document(list.name) call after .order(by: "ListIDX") is not allowed because this function returns a Firestore Query object and there is no way you can chain such a function since it does not exist in that class.
Furthermore, Firestore queries are shallow, meaning that they only get items from the collection that the query is run against. There is no way to get documents from a top-level collection and a sub-collection in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use the properties of documents in a single collection. So the most simple solution I can think of would be to use two different queries and merge the results client-side. The first one would be the above query which returns a list of "wishlists" and the second one would be a query that can help you get all wishes that exist within each wishlist object in wünsche subcollection.
I solved the problem. I added another attribute when saving a wish that tracks the index of the list it is being added to. Maybe not the smoothest way but it works. Thanks for all the help :)
In my WalletTableViewController I have two functions, used to calculate the Wallet Value:
A. updateCellValue() Is called by reloadData() with the tableView and uses indexPath.row to fetch a value (price) and an amount (number of coins) corresponding to the cell and make a calculation to get the total value of that coin (amountValue = value * amount). That is then saved with Core Data.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell
cell.delegate = self
cell.amountTextField.delegate = self
updateCellValue(cell, atRow: indexPath.row)
return cell
}
func updateCellValue(_ walletTableViewCell: WalletTableViewCell, atRow row: Int) {
var newCryptos : [CryptosMO] = []
var doubleAmount = 0.0
if CoreDataHandler.fetchObject() != nil {
newCryptos = CoreDataHandler.fetchObject()!
}
cryptoPrice = cryptos[row].code!
guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else { return }
let selectedAmount = newCryptos[row]
guard let amount = selectedAmount.amount else { return }
var currentAmountValue = selectedAmount.amountValue
doubleAmount = Double(amount)!
let calculation = cryptoDoublePrice * doubleAmount
currentAmountValue = String(calculation)
CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)
updateWalletValue()
}
B. updateWalletValue() Is a function that fetches all the amountValue objects in Core Data and adds them together to calculate the Wallet Value.
func updateWalletValue() {
var items : [CryptosMO] = []
if CoreDataHandler.fetchObject() != nil {
items = CoreDataHandler.fetchObject()!
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
In my MainViewController, the Wallet Value is displayed too, but how can I refresh it's value?
func updateMainVCWalletLabel() {
//... what can I do here??
}
This works great for the WalletViewController of course with the TableView and indexPath, but how can I call updateCellValue from the MainViewController to keep the value updated?
The WalletViewController is instantiated and pushed from the MainViewController :
#IBAction func walletButtonTapped(_ sender: Any) {
let walletViewController = self.storyboard?.instantiateViewController(withIdentifier: "walletTableViewController")
self.present(walletViewController!, animated: true)
}
If you want to use a single method in multiple view controllers you should implement that method where you can call that method from anywhere. For example you can use singleton class here.
Create a swift file and name it as your wish (like WalletHelper or WalletManager)
Then you will get a file with the following format
class WalletHelper: NSObject
{
}
Create a shared instance for that class
static let shared = WalletHelper()
Implement the method you want
func getWalletValue() -> Float {
// write your code to get wallet value`
// and return the calculated value
}
Finally call that method like
let walletValue = WalletHelper.shared. getWalletValue()
WalletHelper.swift looks like
import UIKit
class WalletHelper: NSObject
{
static let shared = WalletHelper()
func getWalletValue() -> Float {
// write your code to get wallet value
// and return the calculated value
}
}
Update (old answer below)
To me it is absolutly unclear what you want to achieve: Which value do you want to be updated? The staticTotal?
Seems a litte like an XYProblem to me. As #vadian commented yesterday, please clearly describe where the data is stored, how the controllers are connected, what you want to update when in order to achieve what. You could also provide a MCVE which makes clear what you are asking, instead of adding more and more code snippets.
And, even more interesting: Why do you modify CoreData entries (CoreDataHandler.editObject) when you are in the call stack of tableView(_: cellForRowAt:)? Never ever ever do so! You are in a reading case - reloadData is intended to update the table view to reflect the data changes after the data has been changed. It is not intended to update the data itself. tableView(_: cellForRowAt:) is called many many times, especially when the user scrolls up and down, so you are causing large write impacts (and therefore: performance losses) when you write into the database.
Old Post
You could just call reloadData on the table view, which then will update it's cells.
There are also a few issues with your code:
Why do you call updateWalletValue() that frequently? Every time a cell is being displayed, it will be called, run through the whole database and do some reduce work. You should cache the value and only update it if the data itself is modified
Why do you call fetchObject() twice (in updateWalletValue())?
You should do this:
func updateWalletValue() {
guard let items:[CryptosMO] = CoreDataHandler.fetchObject() else {
WalletTableViewController.staticTotal = 0.0
return
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}