Inserting posts into a UICollectionView grid? - ios

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.

Related

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

Firebase storage images random order in collection view

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.

How to know when all tasks are completed after HTTPRequest (Swift)

I am currently facing an issue that has been bothering me for days. I have tried several solutions but don't seem to be able to fix the issue. Let me illustrate what is going on.
I have a feed with posts in a UITableView. As soon as the user selects one of the cells, he is shown a detailed view of this post in a new UIViewController. In there, I set up all views and call a function loadPostDetails() in the viewDidLoad() method. As soon as the view shows up, a custom loading indicator is presented which is set to disappear (hide) when the details are loaded and then the UITableView of this UIViewController (let's call it PostController) is shown.
In my loadPostDetails()function, I make a HTTPRequest which acquires the JSON data I need. This returns three kinds of info: the post details themselves, the likes and the comments. I need to handle each of these three elements before I reload the UITableView and show it. Currently, I do it like this:
HTTP.retrievePostDetails(postID: postID, authRequired: true) { (error, postDetails) in
if(error != nil) {
self.navigationController?.popViewController(animated: true)
} else {
if let postDetails = postDetails, let postInfo = postDetails["postInfoRows"] as? [[String: Any]], let postLikesCount = postDetails["postLikesCount"] as? Int, let postLikesRows = postDetails["postLikesRows"] as? [[String: Any]], let postCommentsRows = postDetails["postCommentsRows"] as? [[String: Any]] {
DispatchQueue.main.async {
let tableFooterView = self.postTableView.tableFooterView as! tableFooterView
if let postTitle = postInfo[0]["postTitle"] as? String, let postText = postInfo[0]["postText"] as? String {
self.postTitleLabel.text = postTitle
self.postTextLabel.text = postText
}
for (index, postCommentRow) in postCommentsRows.enumerated() {
tableFooterView.postComments.append(Comment(userID: postCommentRow["userID"] as! Int, userProfilePicURL: postCommentRow["userProfilePicURL"] as! String, userDisplayName: postCommentRow["userDisplayName"] as! String, commentTimeStamp: postCommentRow["commentTimeStamp"] as! TimeInterval, commentText: postCommentRow["commentText"] as! String))
}
var likeDisplayNames = [String]()
for postLikeRow in postLikesRows {
likeDisplayNames.insert(postLikeRow["userDisplayName"] as! String, at: 0)
}
if(postLikesCount > 2) {
tableFooterView.likesLabel.text = "\(likeDisplayNames[0]), \(likeDisplayNames[1]) and \(postLikesCount - 2) others"
} else {
tableFooterView.likesLabel.text = "\(postLikesCount) likes"
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.screenDotsLoader.isHidden = true
self.screenDotsLoader.stopAnimating()
self.postTableView.isHidden = false
self.postTableView.reloadData()
})
}
}
}
Note: I add more text to UILabels, like the date and the profile picture of the user, but I have removed a couple of lines to make it more readible and because the extra code is irrelevant for this problem.
Now, as you might already see, I call the reload stuff 1 second later, so in 95% of the cases it works just fine (but still, it is not perfect, as it is a "hack"). In the other 5%, the layout can't figure out the right constraints, resulting in a very bad layout.
I have in the last days tried to play with DispatchGroups(), but I couldn't figure out how to do it. I am in fact trying to know when all tasks have been performed, so when all UILabels have been updated, all UIImageViews have been updated etc. Only then, I want to reload the UITableView.
I was hoping someone could point me in the right direction so I can enhance my user experience a bit more. Thank you!
DispatchGroups is used when you do a bunch of asynchronous tasks together , and need to be notified upon finish of all , but currently you don't do this as when you receive the response , all are inside the main thread which is synchronous ( serial ) which means all the stuff before reloading the table will happen before it's reload , so you're free to use dispatch after if this will make sense to your UX

Issue when scrolling through a tableview while feeding the cells with external data

I am trying to recreate a browser within an app. I created a section where the user could add his/her favourite websites, so they can be displayed in a tableview. I am using a third-party api to get and display the title, description and the favicon of the selected website. This is how it works. There are two view controllers, the first one the browser itself and the other one the favourite tableview controller where the user can see its favourite websites. When the user taps in the "+" sign in the first view controller, the current url is saved in an array with UserDefaults (I encountered some problems saving it with Codable) and then in the second view controller, the tableview is populated in a for-loop with each url that the user has saved.
The problem is the following:
When the user has already added various websites, it has to scroll to find all of what he/she saved. However every tableview cell gets recycled so it gets loaded again every time the user scrolls up and down.
This is when the user first opens the favourite section
When the user scrolls
When the user scrolls up to a tableview cell that allegedly already loaded up
The two arrays that are used are the following:
var modifiedURLS = defaults.object(forKey: "modifiedURLS") as! [String]
var nonModifiedURLS = defaults.object(forKey: "nonModifiedURLS") as! [String]
I am using two arrays because the api that I'm using (SwiftLinkPreview) only works when the requested url is a normal one (nothing else after .com)
so I am saving a modified url to access the Title, description and favicon properties with this api, and a nonModified url that stores exactly the url that the user wanted to save
The code when the tableviewcontroller is populated is the following:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "favoritesCell", for: indexPath) as! BookmarkTableViewCell
let slp = SwiftLinkPreview(session: .shared, workQueue: SwiftLinkPreview.defaultWorkQueue, responseQueue: DispatchQueue.main, cache: DisabledCache.instance)
cell.titleWebLabel.text = "Loading title..."
cell.descriptionWebLabel.text = "Loading description..."
cell.urlLabel.text = nonModifiedURLS[indexPath.row]
for _ in 0...modifiedURLS.count - 1 {
// The following is an asynchronous request
slp.preview(modifiedURLS[indexPath.row], onSuccess: { result in
let titleIndex = result.index(forKey: SwiftLinkResponseKey.title) as! Dictionary<SwiftLinkResponseKey, Any>.Index
let title:String = result[titleIndex].value as! String
cell.titleWebLabel.text = title
let descriptionIndex = result.index(forKey: SwiftLinkResponseKey.description) as! Dictionary<SwiftLinkResponseKey, Any>.Index
let description:String = result[descriptionIndex].value as! String
cell.descriptionWebLabel.text = description
let favIconIndex = result.index(forKey: SwiftLinkResponseKey.image) as! Dictionary<SwiftLinkResponseKey, Any>.Index
let favIcon = result[favIconIndex].value
cell.favIconImageView.sd_setImage(with: URL(string: "\(favIcon as! String)"))
},
onError: { error in print("\(error)")})
}
return cell
}
The project can be found in the following GitHub link: https://github.com/francisc112/IssueBrowser

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.

Resources