Why UITableViewCell displays previous value at first and just later updates self? - ios

I have a UITableView with a UIImageView on it. On these imageViews I'm downloading images from the server and display them. The problem is that when I scroll my UITableView, at first it shows another cell's image and just later(in 2-3 seconds) it updates the cell's imageView to the properly image.
How can I fix it? I've tried to use background and main thread mix, like that:
extension UIImageView {
func getDataFromUrl(url:String, completion: ((data: NSData?) -> Void)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) in
completion(data: NSData(data: data!))
}.resume()
})
}
func downloadImage(url:String){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
self.getDataFromUrl(url) { data in
dispatch_async(dispatch_get_main_queue()) {
self.contentMode = UIViewContentMode.ScaleAspectFill
self.image = UIImage(data: data!)
}
}
})
}
}
and use these extension on my cellForRowAtIndexPath as:
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
but it doesn't help me. It works as previously. What I do wrong? If you can, please give a description(explanation) to your answer. It will be more useful! Thanks!
UPDATE
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: newsCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! newsCell
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
return cell
}

What is actually happening in your code is that all cells are given a reuse identifier, meaning that they all get put into the same object pool. This means that when you use [tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier]; that you are retrieving a cell from this one pool. It already has a image set up which you need to clear.
Before setting up the cell to download Image you need to clear the image before.
cell.coverImage.image = nil
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")

Let me explain the problem.
Imagine you have the first row, i call it ROW_A object. ROW_A should download and display image with URL_1, by using a thread THREAD_1. When it's in you cellForRow function, you started THREAD_1 to download the image for ROW_A
When you scroll, the first row become invisible, so the ROW_A view object will be reused for displaying another row, for example, row 11th. In the cellForRow, the same process as before, you started thread THREAD_11 to download the image with URL_11 and display that image using ROW_A object (as i said before, the ROW_A object is reused)
The problem is, after a long time downloading IMAGE_1 from URL_1, THREAD_1 finishes its works and set IMAGE_1 to ROW_A. You will see IMAGE_1 being showed.
After that, THREAD_11 finishes too, it set IMAGE_11 to the ROW_A. But ROW_A is displaying IMAGE_1, so that why you see IMAGE_1 then IMAGE_11 on row 11th.
To fix this problem, you should check the image view to see if it's assigned to other downloading thread. I mean, if THREAD_1 finish, THREAD_1 should know that ROW_A is not belong to THREAD_1 anymore. You can create a UIImageView subclass with a url:String property, then when the downloading thread finishes, you compare the url string of downloaded image with value of url property, if it's different, don't show the image.
Another option is using AFNetworking for downloading and showing the remote images. That's very simple and you can find many tutorial on Google.

Related

Message from debugger: Terminated due to memory issue, when i reload my tableview

My app is crash with the message : "Message from debugger: Terminated due to memory issue", when my tableview is reload with row counts are increased.
I have fetch the data on row from document directory path, and show the image of path on imageview of particular cell index, when the row count is reached at 10, it terminate with above mentioned message.
My code of cellForRowAt IndexPath is given below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : signatureCell = tableView.dequeueReusableCell(withIdentifier: "signatureCell") as! signatureCell
cell.selectionStyle = .none
do
{
let imageURL = SignImagesURL[indexPath.row]
let image = UIImage(contentsOfFile: imageURL.path)
cell.imgSignature.image = image
}
catch {
}
return cell
}
Please give me the solutions to avoid it.
Probably there may be memory leaks or may be the memory is getting overload (like view controller is remain in memory even after pop/dismiss due to strong reference to it.)
To fix issue:
You need to build app with profile (cmd + i) and select Allocation from the options and then test you app.
You will see the all the view controllers that is being allocates and deallocates during testing.
Just identify the view controllers that are taking too much memory and try to optimise its memory.
Also you can identify the view controller that remains in memory (not deallocates) even after pop/dismiss.
To check the details of the Error Description, You can do the Following.
Goto Product -->Scheme -->Edit Scheme there you can enable the Checkbox of Zombie Object.
They you can able to see in console about which ViewController is deallocated.

request.cancel() for reused cell Alamofire swift

I have a collection view with images, I am using Alamofire and AlamofireImage for the networking code.
At the beginning of the cellForItemAtIndexPath I set the image to nil and cancel any in-flight request if one exists, using request?.cancel().
But doing so, some cells won't be populated with images at all.
Can you show me what I am doing wrong?
See my cellForItemAtIndexPath code below:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! StoreCell
let entry = self.dataCell.addCell[indexPath.section][indexPath.item]
stringURL = "https://website.com/stories_db/images/\(entry.filename)"
cell.title.text = entry.title
cell.subTitle.text = entry.author
cell.cost.text = entry.cost
// Reset the cell
cell.storyImage.image = nil
// request?.cancel()
if let image = dataCell.cachedImage(stringURL) {
cell.storyImage.image = image
} else {
request = dataCell.getNetworkImage(stringURL) { image in
cell.storyImage.image = image
}
}
// Fix the collection view cell in size
cell.contentView.frame = cell.bounds
cell.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
return cell
}
It looks like you're reusing a single var called request to store the image request. If more than one of your collection view cells is visible at a time, you may need to make more that one async image request at a time, so you need a way to store multiple in-progress requests.
For example, if your collection view has three cells and all are visible, when it reloads it will call cellForItemAtIndexPath to get the first cell. Your code creates the cell and kicks off the async request for "Image 1" and stores it in your request var.
Then the collection view immediately calls cellForItemAtIndexPath again to get the second cell. If the request for "Image 1" is still in progress, calling request?.cancel() interrupts it so "Image 1" will never finish downloading. The code then creates a new request for "Image 2" and assigns it to request.
The collection view immediately calls cellForItemAtIndexPath to get the third cell...
One solution would be to store an array (or dictionary) of active requests. Add a request when you make it, remove it when the request completes (failure or success). Then if you determine that there's a request in progress for a cell you're about to reuse, cancel that request and remove it from the list.
Another way to do it would be to create a custom cell subclass and have it store the active request. This has the advantage of making it simpler to correlate requests to cells.
Using either approach, you may want to consider cancelling the active request for a cell in UICollectionViewCell.prepareForReuse() instead of cellForItemAtIndexPath.

Swift: Pagination in PFQueryTableViewController

I'm having trouble figuring out how Parse's (supposedly built in) pagination works. I have done hours of research, and almost everything I find is written in Objective-C, and when I try to convert it to Swift the app does not run.
I am running a PFQueryTableViewController to load videos in MPMoviePlayerControllers.
First, I'm unsure what to put in numberOfRowsInSection. Parse's AnyPic tutorial in Obj-C suggests to return objects.count * 2 + (self.paginationEnabled ? 1 : 0). To be frank, I have no idea what that second part means, but either way, if I return anything in numberOfRowsInSection, such as objects.count, the app crashes immediately.
If I simply leave out numberOfRowsInSection, the first page of videos is completely successful. Once I scroll down all the way however, I get to the "Load More..." cell which was added by Parse automatically. Then the app freezes. The log simply reads (lldb) during this crash.
I suspect that there is not a cell loaded even though this other cell is appearing, but again I'm unsure what to do since whatever I put in numberOfRowsInSection crashes the app.
Next, I'm totally at a loss of what to put in cellForNextPageAtIndexPath. Parse's tutorial suggests:
PAPLoadMoreCell *cell = [tableView dequeueReusableCellWithIdentifier:LoadMoreCellIdentifier];
if (!cell) {
cell = [[PAPLoadMoreCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:LoadMoreCellIdentifier];
cell.selectionStyle =UITableViewCellSelectionStyleNone;
cell.hideSeparatorBottom = YES;
cell.mainView.backgroundColor = [UIColor clearColor];
}
return cell;
I am unable to mock this code in Swift, specifically the third line. I have tried running the app, skipping the if-statement, to no success.
Perhaps this method needs to be complete in order for the pagination to work?
That is basically it as far as my questions, but here are some of the endeavors I've taken trying to solve this.
In cellForRowAtIndexPath, I tried the following:
if indexPath.row == objects.count - 1 { //Does NOT work if '-1' is omitted
//Extremely glitchy
println("Load next page")
loadNextPage()
}
This is moderately successful. It actually successfully loads more pages when the indexPath is close to the bottom. However, it is very glitchy. It crashes occasionally, saying fatal error: Array index out of range (lldb) This seems to be if I scroll fast enough to actually view the "Load More..." cell, so it seems like the same problem as above.
If somebody can help me with this, I will be (almost) forever thankful.
Edit: Code included for how I am working with data.
Here is my query for the table:
override func queryForTable() -> PFQuery! { //Same code as when normally loaded videos
var query = PFQuery(className:"post")
query.orderByDescending("createdAt")
return query
}
The important aspects of the "post" class are a videoFile and label. These are successfully loaded into PFTableViewCell.
Next, how I handle the cellForRowAtIndexPath:
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!, object: PFObject!) -> PFTableViewCell! {
super.tableView.cellForRowAtIndexPath(indexPath)
let myCell = tableView.dequeueReusableCellWithIdentifier("tableViewCell", forIndexPath: indexPath) as TableViewCell
println("Current indexPath: \(indexPath.row)")
if indexPath.row == objects.count - 1 {
println("Last current object")
//loadNextPage()
}
//Parse: Assigning values(url, user, count) to cells
let vidFile = object.valueForKey("videoFile") as PFFile
let vidStr = vidFile.url
let vidURL = NSURL(string: vidFile.url)
myCell.videoURL = vidURL
myCell.usernameLabel.text = object.valueForKey("user") as? String
return myCell
}
This all works fine. Interestingly, println("Current indexPath: \(indexPath.row)") will not print ANYTHING when the "Load More" cell from Parse enters the view, which is making me suspicious.
I have some methods that make sure not to load a new video unless the cell is taking up a majority of the view. This would cause a crash if it tried to load a video in the "Load More" cell, but I have implemented checks to disallow this and it still crashes when the cell comes into view.
For example:
if indexPathToLoad.row != objects.count {
//Display video at proper indexPath
cell.displayVideo()
}
else { // Does not fire
println("Do not try to load new video: Load next page")
}

UITableView Loading Images Slowly

The TableView fetches images from parse api. However, when scrolling through the tableView, there is lag while the image is loading.
Is there is an efficient way to decrease lag? Some way to load images before (or even after) the user scrolls to the cell? I'm not too familiar with dealing with asynchronous calls.
Fetch Image Function
func fetchImage(restaurantArray: PFObject!, completionHandler: ImageCompletionHandler!){
var imageReference = restaurantArray["PhotoUploaded"] as PFFile
imageReference.getDataInBackgroundWithBlock{
(data, error) -> Void in
if (error != nil){
completionHandler(image: nil, error: error)
}else{
let image = UIImage(data: data)
completionHandler(image: image, error: nil)
}
}
}
TableView Cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
cell.nameLabel.text = restaurantNames[indexPath.row]
// check if image is empty
if foodPhotoObjects.isEmpty {
} else {
self.fetchImage(self.foodPhotoObjects[indexPath.row] as? PFObject, completionHandler: {
(image, error) -> () in
if image != nil {
cell.mainRestaurantImageView.image = image
cell.mainRestaurantImageView.contentMode = .ScaleAspectFill
cell.mainRestaurantImageView.clipsToBounds = true
}else{
//alert user: no image or put placeholder image
}
})
}
return cell
}
You can get this functionality for free by using PFImageView, less code for you to write to achieve the desired effect. Just call loadInBackground: after setting the file property.
https://parse.com/docs/ios/api/Classes/PFImageView.html
As for UI lag, profile in Instruments to determine what is so expensive that it is causing significant noticeable lag.
A couple of thoughts:
You should confirm whether this completion block is running on the main thread (examine NSThread.isMainThread) and if not, make sure to dispatch the update of the image view back to the main thread, e.g. dispatch_async(dispatch_get_main_queue()) { ... }. Trying to perform updates of UIKit controls from a background thread can end up with inconsistent behavior, so make sure you're only updating the UI form the main thread.
You should consider the implications of the user quickly scrolling down and back up in the table view. Your code will result in re-fetching images previously retrieved.
Sometimes the underlying NSURLCache will handle this gracefully, caching the images for you. In other cases it will not. (Sadly, the caching of network requests is dictated by some fairly opaque criteria, and depending upon the server implementation, you might not have a lot of control over that.)
You might want to confirm your behavior, and implement your own caching (e.g. NSCache) of previously downloaded images and checking that before retrieving images again.
I would make sure to test this process in conjunction with the network link conditioner so you can confirm the behavior of the app in less than ideal environments (i.e. real-world situations).
Unrelated to your question there are two issues with updating the cell asynchronously:
If the cell scrolled out of view by the time the image retrieval completed, the cell could have been reused, and the cell may have been reused for another row in the table. (Test this by running the app with the network link conditioner degrading the network response time to something more lke a poor cellular connector.) This problem can manifest itself as a "flickering" of images or replacing images with images associated with other cells.
You generally solve this by calling tableView.cellForRowAtIndexPath(indexPath) (a UITableView method, not to be confused with the similarly named UITableViewDataSource method), which will return nil if this index path has scrolled off of the screen, and will return the UITableViewCell if the cell is visible. Use this rather than the previously established cell value when doing the final asynchronous population of the cell.
If there is any possibility that the table could have been reloaded (i.e. the data source changed) during the process of retrieving the image asynchronously, you might actually want to go back to the model, identifying the appropriate index path associated with the image you just downloaded asynchronously.
Bottom line, imagine some degenerate situation where it took a minute or so to retrieve the image (unlikely, just imagine). Think through the contingencies associated with that (the cell scrolled off and subsequently reused, the indexPath for the image might not longer be valid, etc.). Make sure you handle these contingencies gracefully.

Delete dynamic cell in GetCell method

I am trying trying to remove a cell from the UITableView if the image for the cell is a bad image. Basically my code does a threadPool call for each cell's image to make the flow of binding the data as a user scroll smooth as so inside the GetCell method:
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
//other stuff
ThreadPool.QueueUserWorkItem(new WaitCallback(RetrieveImage),(object)cell);
}
within that asynchronous method I call the cell.imageUrl property and bind it to an NSData object as so:
NSData data = NSData.FromUrl(nsUrl); //nsUrl is cell.imageUrl
From there I know that if data==null then there was a problem retrieving the image so I want to remove it. What I currently do is set the hidden property to true, but this leaves a blank space. I want to remove the cell entirety so the user won't even know it existed.
I can't set the cell height to 0 because I don't know if the imageUrl is bad until that indexrowpath initiates getcell call. I don't want to just check all images from the data thats binded to the UITableView because that would be a big performance hit since it can be easily a hundred items. I am currently grabbing the items from a webservice that gives me the first 200 items.
How can I remove the cell altogther if the data==null example
NSUrl nsUrl = new NSUrl(cell.imageUrl);
NSData data = NSData.FromUrl(nsUrl);
if (data != null) {
InvokeOnMainThread (() => {
cell.imgImage = new UIImage (data);
//cell.imgImage.SizeToFit();
});
} else {
//I tried the below line but it does't work, I have a cell.index property that I set in the getCell method
_data.RemoveAt(cell.Index);//_data is the list of items that are binded to the uitableview
InvokeOnMainThread (() => {
//cell.Hidden = true;
});
}
You remove cells by removing them from your source and then you let UITableView know that the source has changed by calling ReloadData(). This will trigger the refresh process. UITableView will ask your source how many rows there are and because you removed one row from your data, the cell representing that row will be gone.
However ReloadData() will reload your entire table. There is a methods which allows you to remove cells specifically, named DeleteRows(). You can find an example here at Xamarin.
When deleting rows it is important that you update your model first, then update the UI.
Alternatively, I recommend you to check out MonoTouch.Dialog which allows interaction with UITableView in a more direct way. It also includes support for lazy loading images.

Resources