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

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

Related

Looped delegate method returning repeats (multithreading)

So I'm trying to make an app that lists the times the ISS is scheduled to Pass over the user's location. It works more or less smoothly, with one hiccup.
What I expect: Around 5 different times listed in a UITableView
What I get: 5 times listed in a UITableView with randomly repeated values. Sometimes all 1 value, sometimes the last one is also the second and/or 3rd to last, sometimes 2 values repeat themselves, any number of incorrect combinations. A small portion of tests return correctly.
What bugs me most is that the wrong results are inconsistent, so I can't see a way to brute force a crude solution.
Relevant code:
First the network manager class/delegate
import Foundation
import CoreLocation
//Delegate for thread communication
protocol NetworkManagerDelegate: class {
func didGetPass(lastPass: Bool, pass: Pass)
//Flag last model object to limit tableview reloads
}
//Using struct as manager class is not going to change state
struct NetworkManager {
weak var delegate: NetworkManagerDelegate?
//Set a base URL as far along as possible, will finish with data passed from function
let baseURL = "http://api.open-notify.org/iss-pass.json?"
func getPasses(coordinate: CLLocationCoordinate2D) {
//complete URL
let lat = coordinate.latitude
let lon = coordinate.longitude
let requestURL = URL(string: "\(baseURL)lat=\(lat)&lon=\(lon)")
//begin task
let task = URLSession.shared.dataTask(with: requestURL!) { (data, response, error) in
if error != nil {
print(error as Any)
//Generic report on failure to connect
print("Could not reach API")
} else {
do {
//Get JSON from data to parse
if let resultJSON = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any] {
//Get list of passes from JSON
let passes = resultJSON["response"] as! [[String: Int]]
//Set default parameters for delegate method
var testPass: Pass
var lastPass = false
//loop through passes
for pass in passes {
//determine if last pass
if pass == passes.last! {
lastPass = true
}
testPass = Pass()/*This seems terribly inefficient to me
However, attempting to create the object outside the loop and simply modify it
leads to the same exact object being passed multiple times to the main thread, so
the tableview's cells are all identical.*/
testPass.durationInSeconds = pass["duration"] ?? 0
//Convert date to Date object and set to testPass
testPass.riseTime = Date(timeIntervalSince1970: (Double(pass["risetime"] ?? 0)))
//Inform main thread via delegate
DispatchQueue.main.async {
self.delegate?.didGetPass(lastPass: lastPass, pass: testPass)
}
}
}
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
And on the main thread:
extension TrackingController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return passes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Get cell, pass default if nil
guard let cell = tableView.dequeueReusableCell(withIdentifier: "passCell") as? PassCell else {
return PassCell()
}
//Retrieve data from passes, assign to labels
let pass = passes[indexPath.row]
cell.timeLabel.text = "\(dateFormatter.string(from: pass.riseTime))"
cell.durationLabel.text = "Duration: \(pass.durationInSeconds) Seconds"
return cell
}
}
extension TrackingController: NetworkManagerDelegate {
func didGetPass(lastPass: Bool, pass: Pass) {
passes.append(pass)
if lastPass { //reload only once api calls are done
passList.reloadData()
}
}
}
That should be all code that triggers from the moment the Network Manager's one function is called. Can anyone explain this to me? I have manually checked the API, I should not be receiving duplicate outputs here.
Edit: I just tried changing DispatchQueue.main.async to DispatchQueue.main.sync, and while it seems to fix the issue my understanding of multithreading leads me to believe this defies the point of running the call on another thread. That said, my understanding of multithreading isn't very practical, so I'm open to correction on that or to better solutions to my problem.
Ideally, on the main thread, you don't do anything with the data until the last object has been received. So, passing the whole array in one go is a better approach.
You can do this instead:
func didGetPasses(_ passes: Passes) {
passes.append(passes);
passList.reloadData();
}
And call
self.delegate?.didGetPasses(passes)
once the for loop is completed.
PS: You should also consider using closures. It helps in handling logic and makes code more 'in-place' at the call site.
Is
guard let cell = tableView.dequeueReusableCell(withIdentifier: "passCell") as? PassCell else {
return PassCell()
}
correct? seems you are returning an uninitialised PassCell there
I decided on an answer inspired by advice from Kunal Shah. I stopped looping the delegate call and instead put together a collection of Pass objects to send to the main thread.
I didn't want to do this as my previous experience with this led to empty collections being sent for some reason, but it seems to work here. This way I still only reload the tableview once, but also only call the delegate method once.

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.

Alamofire requst taking around 10 seconds

I've run this code on my mac with good wifi and my phone using wifi / data and I'm getting an extremely slow load time on the code below. It takes about 7-10 seconds for my UI to update which I do in my view controller in the second block of code where the ...'s are.
This isn't the first time I've dealt with web requests and I've never had this kind of slow behavior. It is the first time I've used Alamofire and this kind of completion handler stuff I'm doing in my downloadProfileInformation where DownloadComplete is a typealais for () -> ().
I figure maybe the problem lies there. It's not updating the UI until everything is completely downloaded I guess, but should a picture be taking 10 seconds to download?
https://blzgdapipro-a.akamaihd.net/game/unlocks/0x0250000000000BB0.png
This is an example of a link that could be grabbed from the API.
I also don't know if it's possible the issue is with the API? I've never heard of something like that but I don't know what's possible.
https://api.lootbox.eu/documentation
EDIT:
//Called with IBAction
PROFILE_URL = "\(URL_BASE)\(Info.sharedInstance.platform)/\(Info.sharedInstance.region)/\(Info.sharedInstance.battletag)/profile"
Alamofire.request(PROFILE_URL).responseJSON { response in
let result = response.result
if let dict = result.value as? Dictionary<String,Any> {
if let error = dict["error"] as? String {
print(error)
} else {
if let data = dict["data"] as? Dictionary<String,Any> {
//Assigning variables to that value
if let username = data["username"] as? String {
DispatchQueue.main.async {
self.battletagNameOutlet.text = username
print("battletag update happening")
}
}
if let level = data["level"] as? Int{
...
}
if let avatarURL = data["avatar"] as? String {
...
}
if let competitive = data["competitive"] as? Dictionary<String,Any> {
....
}
}
}
}
}
}
}
Usually the loading takes more time if the Per_Page is more than 50 for your API link. This can cause a delay when dealing with API and requests that contain images and videos.

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.

How can I prevent TableView cells from duplicating? Swift

So I am building a notes app and have tried everything but can not figure this issue out. I have 3 UIViewController's. When you click a button on the first UIViewController, it shoots you over to the third one which consist of a UITextView, UITextField and a Static UILabel which gets updated with some information. When you fill out these fields and tap the back button it brings you to the second view controller which is a table that gets updated with this information.
The issue is: when I tap the UITableViewCell it loads the information back to the third view controller so the user can edit his notes but when I come back to the UITableView it creates a brand new cell instead of just updating the old one.
If I could just update my array with the same object I sent back to be edited by the user I think this issue would be solved but I have no idea how to do this. Thanks for the help!
VC2 - this is how I am sending my data from the tableView to the textView Back
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let nextView = segue.destinationViewController as! TextViewController
guard let indexPath = self.tableView.indexPathForSelectedRow else {
print("Didnt work")
return
}
let dataToSendBackToBeEdited = textViewObjectData[indexPath.row]
print(dataToSendBackToBeEdited.dreamText)
print(dataToSendBackToBeEdited.dreamTitle)
print(dataToSendBackToBeEdited.dreamType)
print(dataToSendBackToBeEdited.description)
print(dataToSendBackToBeEdited)
nextView.objectSentFromTableViewCell = dataToSendBackToBeEdited
}
This is how I am saving the information the the user taps back to go to the tableView
func determineSave() {
guard var savedDreamArray = NSKeyedUnarchiver.unarchiveObjectWithFile(TextViewController.pathToArchieve.ArchiveURL.path!) as? [Dream] else {
//First dream object saved
let dreamObject = Dream(dreamTitle: titleForDream.text!, dreamType: typeOfDreamLabel.text!, dreamText: textView.text, currentDate: NSDate())
dreamArray.append(dreamObject)
NSKeyedArchiver.archiveRootObject(dreamArray, toFile: pathToArchieve.ArchiveURL.path!)
return
}
//print(savedDreamArray.count)
//print(savedDreamArray.first!.dreamTitle)
let dreamObject = Dream(dreamTitle: titleForDream.text!, dreamType: typeOfDreamLabel.text!, dreamText: textView.text!, currentDate: NSDate())
savedDreamArray.append(dreamObject)
NSKeyedArchiver.archiveRootObject(savedDreamArray, toFile: pathToArchieve.ArchiveURL.path!)
}
I was having this issue as well. Came in here, and got the answer. The array!
I was appending the array as well, which apparently was causing duplicate cells to appear.
I just reset my array back to empty before I retrieved the data again and reloaded the table.
I'm using Firebase so my code looks like this:
DataService.instance.dateProfileRef.observeEventType(FIRDataEventType.Value) { (snapshot: FIRDataSnapshot)
in
//have to clear the array here first before reloading the data. otherwise you get duplicates
self.posts = [] //added this line
if let snapshot = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshot {
let snapUserID = (snap.childSnapshotForPath("userID").value!)
if snapUserID as? String == USER_ID {
if let profileDict = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let post = Post(postKey: key, postData: profileDict)
self.posts.append(post)
You do a .append() on your array, this will add a new cell.
You must find the index of the object you want to update and then assign it:
array[index] = newObject

Resources