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.
Related
I'm working on a instant messaging app. The user can send text messages, for which I'm using a UITextView inside the custom tableview cells. The user should be able to send all kinds of multimedia data, such as images, documents and videos.
Inside my table view cell I have both a textView and a UIImageView in a stackView and I can send the respective kinds of data. If there is no text, I hide the TextView. If there is no Image, I hide the image.
The problem is: to scale the app, I'd have to add a new container for documents, another one for videos, another one for audio files and hide all the other containers that have no data added to it. It's a garbage solution, but it was the only one that I found.
Knowing from the backend what kind of data is sent, how could I programatically add a container view in which I make the setup on the spot? I was thinking of having a textView as default and an empty View and inside cellForRowAt just add the needed elements.
I would try to create several cells which you dequeue in cellForRowAt by checking for a criteria, e.g.:
if userPost.images != nil {
let cell = tableView.dequeueReusableCell(withReuseIdentifier: "Identifier1", for: indexPath) as! UITableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withReuseIdentifier: "Identifier2", for: indexPath) as! UITableViewCell
return cell
}
With this solution, you can create a cell that nicely fits the data a user sent another user. Just check for any criteria you want and return the according cell.
Hope it helped!
Something that bother me for a long time.
Is there a way to preload UICollectionViewCell ?
something to prepare before loading the data, so the cell will not be create while user is scrolling.
Is there a way to take Full control on WHEN to create the UICollectionViewCell and WHEN to destroy the UICollectionViewCell.
From Apple documentation:
You typically do not create instances of this class yourself. Instead, you register your specific cell subclass (or a nib file containing a configured instance of your class) with the collection view object. When you want a new instance of your cell class, call the dequeueReusableCell(withReuseIdentifier:for:) method of the collection view object to retrieve one.
The cell itself is not a heavy resource, so it makes a little sense to customize its lifetime.
I think that instead of searching for a way to take a control over cell creation, you should ask yourself: why do you want to preload a cell? What kind of heavy resource would you like to preload?
Depending on the answer you can try following optimizations:
If you have complex view hierarchy in your cell, consider refactoring from Autolayout to manual setting frames
If your cell should display results of some complex computations or remote images, you would like to have a separate architecture layer for loading these resources and shouldn't do it in cell class anyway. Use caching when necessary.
You can blackout your screen while you enter into scene and navigate (scroll) to each cell type. When you did scroll each cell you should hide blackout view and present collection view on first cell (top).
It is some kind of workaround. My cells have a lot resources like images, which lag while scrolling collection view.
I can add some actual approach
If you need to preload some data from internet / core data checkout https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching/prefetching_collection_view_data
tl;dr
first you need your collection view datasource to conform UICollectionViewDataSourcePrefetching in addition to UICollectionViewDataSource protocol
then prefetch your data and store it in cache
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
// Begin asynchronously fetching data for the requested index paths.
for indexPath in indexPaths {
let model = models[indexPath.row]
asyncFetcher.fetchAsync(model.id)
}
}
and finally feed your cell with your cached data
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else {
fatalError("Expected `\(Cell.self)` type for reuseIdentifier \(Cell.reuseIdentifier). Check the configuration in Main.storyboard.")
}
let model = models[indexPath.row]
let id = model.id
cell.representedId = id
// Check if the `asyncFetcher` has already fetched data for the specified identifier.
if let fetchedData = asyncFetcher.fetchedData(for: id) {
// The data has already been fetched and cached; use it to configure the cell.
cell.configure(with: fetchedData)
else
... async fetch and configure in dispatch main asyncd
Also u can approach some workaround using delegate methods to handle willDisplay, didEndDisplay events e.g. https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618087-collectionview?language=objc
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.
Screenshot of weird behavior
The screenshot tells is quite well. I have a tableview with dynamic custom cells. I added a println for one of the contents of the cell to check, if the labels are set. I can see in the debug log, that each cell has its content. Still, on the device there are empty cells at random, which means, the row, where no content appears, is changing a lot. Even just scrolling up and down makes the second row disappear, but the third row is filled. Scrolling again turns this around again. If I close the app and start it again, every row is filled correctly.
Here is the code for the cell generation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Return a count picker cell
if countPickerTableRow == indexPath.row {
...
}
// Return a normal wish list entry cell
else {
let article = wishListEntries[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! WOSArticleCell
// Correct the order in case a count picker cell was inserted
var row = indexPath.row
if countPickerTableRow != -1 && indexPath.row > countPickerTableRow {
row--
}
cell.setThePreviewImage(UIImage(data: article.thumbnail))
cell.setArticleName(article.name)
cell.setArticleDescription(article.text)
cell.setArticleNumber(article.number)
cell.setArticleCount(article.count as Int)
cell.setOrderInTable(row)
cell.setTableViewController(self)
cell.setNeedsDisplay()
cell.setInputAccessoryView(numberToolbar) // do it for every relevant textfield if there are more than one
println(String(indexPath.row) + " " + cell.nameLabel.text!)
return cell
}
}
In the custom cell class there is nothing special. Just a few outlets to the labels.
Here is a screen of the storyboard:
Storyboard
Can anyone please help me finding out what is going on here? I can't find the reason why the debug log can output the contents of a cell, but the device is not able to render them.
You should change the logic of your code. If the PickerCell comes up just call reloadData() and reload everything in the tableview. If the amount of rows you have is small this won’t be an issue and it’s not an expensive operation as you are not doing any heavy calculating during display.
If you need to update only a single cell because of changes you made in the PickerCell then you should be calling reloadRowsAtIndexPaths:withRowAnimation: with the indexPath of the cell to be updated.
Your issue is with your subclass WOSArticleCell. Have you implemented prepareForUse()? If you have, are you setting any properties to nil?
UITableViewCell Class Reference
Discussion
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.
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.