Firebase storage images random order in collection view - ios

In my firebase database i have several items that have a name and an image each. In my app I would like to display said parameters in a collectionview with an imageview and a label in each cell. I'm able to display each items name in the label, and I'm also able to display the items image in the imageview, however the images downloaded from firebase keep appearing in a random order every time. The labels show the item names in a correct fixed order every time. Each item has a child named "image" with a child named "storage" which stores the image url from firebase storage. The code for downloading images:
func downloadImg() {
let ref = Database.database().reference().child(itemNumber1)
ref.observeSingleEvent(of: .value) { (snapshot) in
for item in snapshot.children {
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "image/storage")
if let url = imageSnap.value as? String {
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
self.imageArray.append(image!)
self.collectionView?.reloadData()
}
}
}
}
}
}
As I said, the code works fine, but I'd like the images to show up in the same order everytime, and the same order as they are stored in the storage/database. I tried appending the url constant to an empty array and printing it, and this array prints the storage urls in the order I want. However something seems to happen when the images themselves are appended to the imageArray.

Related

Swift - Firebase - order collection

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 :)

Firebase storage image won't append to array

So I'm trying to download a set of images to display in a collectionview in my app. I have the images in the firebase storage, and their url in my database under the node "img" in each item node with a child "storage" with the url as the key.
I started by creating an empty array of UIImage: var somePicArray = [UIImage]() and then appending the pictures to this array through this funciton:
func test() {
let penis = Database.database().reference().child("kategorier").child("brus")
penis.observeSingleEvent(of: .value) { (snapshot: DataSnapshot) in
for item in snapshot.children {
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storage")
if let url = imageSnap.value as? String {
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
self.testPicArray.append(image!)
self.collectionView?.reloadData()
}
}
}
}
}
}
up until the "if let url" line everything is ok, as I have tried printing the url within this if statement, and it prints the all the urls as strings perfectly.
however I'm a little more uncertain about the seccond part. I have used this block of code elsewhere in my app to display pictures from firebase storage and this works fine. However it does not seem to work within this funciton, and I'm not sure what I'm doing wrong.
In order to display the images in the cells I tried using the following code in the collectionview cellforitematindexpath method:
cell.cellimage.image = somePickArray[2]
I just chose a random number in the array to see if i can display any picture but it just crashes saying index out of range at this line, which I assume is because the array is empty.
When the vc appears the array is empty so
cell.cellimage.image = somePickArray[2]
will crash before array is filled with data , you need to make sure you do
return somePickArray.count
inside numberOfItemsAtRow

Collection view not showing images properly

In view B I have an imageview which can have an image from gallery. The image is stored in coredata as NSData
Now if I click on a submit button & return back to a view A, I have a collectionview where I want to display the image. If I again go to view B and add another image, click on submit & return to view A I should see 2 different images.
But when I return, what I’m seeing is the first image twice instead of 2 different images. But when I run the app again I see all different images
as required...
In viewWillAppear of view A this is what I’ve done…
//Fetching from Database
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Customers2")
do {
customerDetails2 = try managedContext.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as! [Customers2]
for result in customerDetails2 {
if let imageData = result.value(forKey: "image") as? NSData {
}
}
collectionview2.reloadData()
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
In numberOfItemsInSection of view A….
count1 = customerDetails2.count
In cellForRowAtIndexPath of view A…
let dfg = customerDetails2[indexPath.row]
let asd = dfg.image
if let image = UIImage(data: asd! as Data) {
imageArray.append(image)
cell1.customerImageView.image = imageArray[indexPath.row]
}
Issue is With the below line
imageArray.append(image)
As you mentioned the issue occurs after adding the second image
So you might be reloading the collection view again to show the images.
The above code will be executed again and it will append the image for the row zero at the index 1 and row 1 at index 2 so when you try to show the image for row1, it will be same as row 0 ..
Use a dictionary if you want to cache the images
like below
//Declare a poperty
var imageDict = Dictionary<String,UIImage>()
//At cellForRow
imageDict["\(indexpath.row)"] = image
also check if the image exists in cache
if let image = imageDict["\(indexpath.row)"] as? UIImage {
//Load Image
} else {
//Initialise from data
}

How can I stop UICollectionView from showing duplicate items after Firebase update

I have two UICollection views on a page that displays data about a Room. It includes photos of the room in one UICollection View and another UICollection View which contains a list of items in that room. There's a link to edit the Room. When a user clicks on the link, they then segue to another view that let's them update it including adding additional photos.
After adding a photo, and hitting submit, in the background the photo is uploaded to Firebase storage and in the Firebase database, the record is updated to include the name of the file that was just uploaded. Meanwhile, the user is segued back to the Room view.
There's a watched on the record of the room in Firebase and when it updates, then the view is refreshed with new data. This is where the problem occurs. It appears, based on a lot of debugging that I've been doing, that the Observe method fires twice and what ends up happening, is the UICollection view that holds the images of the room will show duplicates of the last photo added.
For example, if I add one photo to the room, that photo will appear in the collection 2x. I've attempted to clear the array before the array is updated with the images, and from my analysis, it appears that the array only contains two items, despite showing three in the view. I'm not sure what is happening that would cause this?
Here's a link to the entire file, because I think it might help.
Here's the loadData() method in case this is all that's important:
func loadData() {
self.ref = Database.database().reference()
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
guard let userID = Auth.auth().currentUser?.uid else { return }
let buildingRef = self.ref.child("buildings").child(userID)
buildingRef.keepSynced(true)
buildingRef.child(self.selected_building as String).observe(DataEventType.value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
if ((value) != nil) {
let building_id = value?["id"] as! String
let saved_image = value?["imageName"] as! String
let user_id = userID as! String
let destination = "/images/buildings/\(userID)/\(building_id)/"
let slideShowDictionary = value?["images"] as? NSDictionary
if ((slideShowDictionary) != nil) {
self.slideShowImages = [UIImage]()
self.slideShowCollection.reloadData()
var last_value = ""
slideShowDictionary?.forEach({ (_,value) in
print("are they different? \(last_value != (value as! String))")
if (last_value != value as! String) {
print("count: \(self.slideShowImages.count)")
print("last_value \(last_value)")
print("value \(value)")
last_value = value as! String
CloudStorage.instance.downloadImage(reference: destination, image_key: value as! String, completion: { (image) in
self.slideShowImages.append(image)
self.slideShowCollection.reloadData()
})
}
})
CloudData.instance.getBuildingById(userId: user_id, buildingId: building_id, completion: { (building) in
self.title = building.buildingName as String
self.roomsCollection.reloadData()
})
}
}
})
// User is signed in.
self.getRooms()
}
I am not completely familiar with the Firebase API but if you are having issues with the observation I would suspect the following:
#IBAction func unwindToRoomsVC(segue:UIStoryboardSegue) {
loadData()
}
Triggering loadData a second time looks like it would add a second observation block. As best I can tell the .observe method probably persists the block it is given and triggers it on all changes.

Inserting posts into a UICollectionView grid?

Need some guidance on how to approach this. I'm not sure if I'm completely over complicating it or if it is a mess.
I've got a UICollectionView that I want to always have 9 cells and I'm wanting users to be able to insert posts by tapping on a cell..so if they press on cell 8, they make their post, and it shows up there in the 8th cell with potentially 8 other empty cells around it.
I'm posting/pulling these posts from firebase...so I was thinking the flow needed to look something like:
1) Make an empty array of 9 empty [Posts] so that the cells appear and are clickable.
2) On the firebase observer .. if there are say 3 posts able to be returned, it inserts them into the post array / replaces 3 empty posts.
3) I want the posts to show up sort of randomly throughout the grid, so I figured I'd shuffle the array before reloading the data?
4) I don't really care if the posts back from firebase are in the same spot as they were placed by the user, but I want when a user puts a post for it to stay in the same spot as they placed it, so I figured I'd save a variable to firebase like "position : 8" and I'd say something like "If your user uid = the uid of the post, grab the position number and insert that post at that index of the array.
Is this flow way off base or is there a better way to accomplish this? I haven't seen much about inserting items into certain positions into a table view/collection view.
Edit:
func fillWithBlanks() {
for i in 0 ..< 9 {
let blankPost = Post(timestamp: 99999999999999999, picURL: nil, postKey: "") // 100% need a better timestamp there, just filler for now.
postsArray.append(blankPost)
}
}
and then
DataService.ds.REF_POSTS.queryOrderedByChild("timestamp").queryStartingAtValue(cutoff).observeEventType(.ChildAdded, withBlock: { (snapshot:FIRDataSnapshot) in
let time = snapshot.value!["timestamp"] as? Int
let text = snapshot.value!["text"] as? String
let picURL = snapshot.value!["imageURL"] as? String
let key = snapshot.key
let post = Post(timestamp: time!, picURL: picURL!, postKey: key)
self.postsArray[self.selectedPostIndex] = post
self.collectionView.reloadData()
})
So basically on viewDidLoad I'm calling fillWithBlanks(), and then when I select a cell I set the selectedPostIndex to the index clicked on, create a post, and insert it in where I selected.
This works and I tested it by printing out the cell's timestamp when I press on it, and sure enough when I make a post the timestamp is appropraite to the post and not the 99999999.
My massive issue I'm having are the pictures. When I load up I get 9 cells that are empty but when I make a post it sets the picture I set for the post to all 9 cells, and then if I make 2 posts, I get a very Simon-says'ie flashing in all of my cells between the first picture, second picture, and the blank background from the blank cell.
When I make a post I'm saving a download url to firebase and then I have a caching system that downloads the posts or grabs them in. It all worked before trying to implement the blank posts.
Any ideas what would be causing that? I don't understand why appending a new post would do anything to the blank posts I'm making when the app loads up. I'm assuming I'm having some misunderstanding with classes and my Post array/ Post objects aren't what they need to be.
Edit 2:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let post = postsArray[indexPath.row]
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MainCell", forIndexPath: indexPath) as! PostCell
if let imageURL = post.picURL {
cell.cellImage.loadImageUsingNSCache(imageURL)
}
return cell
}
and the .loadimagesUsingNSCache comes from :
extension UIImageView {
func loadImageUsingNSCache(urlString: String){
if let cachedImage = imageCache.objectForKey(urlString) as? UIImage {
self.image = cachedImage
return
}
let url = NSURL(string: urlString)
NSURLSession.sharedSession().dataTaskWithURL(url!, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) in
if error != nil {
print(error)
return
}
dispatch_async(dispatch_get_main_queue(), {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString)
self.image = UIImage(data: data!)
}
})
}).resume()
}
About the problem with the images, remember collection view cells are reusable items, so you need to tell what's the content of every item inside every time you call collectionView(cellForItemAtIndexPath).
That means you need to provide a value for cell.cellImage every time, even if post.picURL is nil. In that case you can say cell.cellImage = nil or show a default empty picture.

Resources