Delete dynamic cell in GetCell method - ios

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.

Related

Simultaneously change display parameters on all table view cells

I am trying to implement a table view design where a user can click a button outside of a table view cell and the display mode of all the buttons should change. However this is not the 'selected' mode for a given cell (that will be yet a third state that becomes accessible via switching to this second state). What's the proper way to accomplish this?
I am using dequeueReusableCellWith so I don't want to simply cycle through every cell because some that are out of sight probably shouldn't be modified. I simply want any cell that is visible, or becomes visible, while the table view cell is in this second display mode to follow a second design rather than the first design.
The second design, for now, is being modified via a method I added to a subclass of UITableViewCell like so:
- (void) p_refreshDisplay {
if (self.editing) {
self.buttonToClearWidth.constant = 20;
self.buttonToClearLeadingWidth.constant = 20;
} else {
self.buttonToClearWidth.constant = 0;
self.buttonToClearLeadingWidth.constant = 0;
}
}
However, I'm not sure how to trigger this p_refreshDisplay for every visible (and to become visible) cell. It seems unwise to call this many times and refresh the table view. What would be the proper way to accomplish what I want to do?
You do what should be done for any table view change:
Update your data model or some flag as needed.
Either call reloadData on the table view or call reloadRowsAtIndexPaths:withRowAnimation: passing in indexPathsForVisibleRows as the list of rows to reload.
Implement cellForRowAtIndexPath to provide appropriate cells for the given data/flags.
It sounds like you should have a custom cell class that has one or more properties that can be set on the cell in cellForRowAtIndexPath so the cell can render itself properly based on the specified state.
You can achieve this by doing three things:
Establish some property that indicates the "mode" of the table, either a boolean or perhaps an enum if there are more than three states
Ensure that cellForRowAtIndexPath configures the cell appropriately based on the value of this property. This will ensure that newly displayed cells are configured correctly.
When the "mode" changes you can use the tableview's visibleCells property to update any currently visible cells:
for cell in tableview.visibleCells {
if let myCell = cell as? MyCustomCellClass {
myCell.setButtonStyle()
}
}

updating UITabelView cells efficiently

I'm writing this app that has a table view, showing data about stock market. The app uses SignalR (this lib) for updating the data in real-time. Each of the table view cells have 10 labels representing some information about the respective instrument.
As I said, the app works in real time and sometimes gets as much as 20 updates per second which need to appear on UI. Each of the SignalR notifications contain a string that after parsing it I know which row, and which labels on that row(not all of them are changed every time) need to be updated.
The question is: which of the following ways is better performance wise?
updating the model and then calling
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
getting a reference to that specific row and updating the labels with changed values:
let indexPath = NSIndexPath(forRow: i, inSection: 0)
let cell = self.tableView.cellForRowAtIndexPath(indexPath)!
if self.watch[i]["bestBidQuantity"].string != list[3] {
let bestBidQuantityLabel = cell.viewWithTag(7) as! UILabel
bestBidQuantityLabel.text = StringManipulation.localizeDecimalNumber(Int64(list[3])!)
}
one important thing to note is that the cell in question may not be visible at the time of updating. As far as I know calling reloadRowsAtIndexPaths updates the row only if it's visible, but I'm not sure about my second solution regarding out of the view cells.
I'm not sure why you're worried about updating cells that aren't on screen? If you're dequeueing a cell (as you should) in cellForRowAtIndexPath:, your cell will be dequeued and setup with the correct information (from your model) when it's needed.
When you get your SignalR notification, update your model from the notification. When the user scrolls, a cell will be dequeued and setup with the latest information from your model.
For cells that are already in view, I like the second option, but still update the model for when the cell goes off screen and needs to be set up again.
Have you also not created a UITableViewCell subclass? I recommend using a subclass with IBOutlets instead of viewWithTag. You can then include a function in your cell subclass to update it's UI components. Something like this -
class StockCell: UITableViewCell
{
#IBOutlet weak var bestBidQuantityLabel: UILabel?
func update(notification: SignalIR) {
bestBidQuantityLabel?.text = notification.bestBidQuantity
}
}
When you get a new notification you could do something like this:
updateModel(notification)
if let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: ..., inSection: ...)) as? StockCell {
cell.update(notification)
}
You can also reuse the update(...) function to setup cells from cellForRowAtIndexPath:

iOS tableview cell is empty at random

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.

How to create UITableViewCells with complex content in order to keep scrolling fluent

I am working on a project where I have a table view which contains a number of cells with pretty complex content. It will be between usually not more than two, but in exceptions up to - lets say - 30 of them. Each of these complex cells contain a line chart. I am using ios-charts (https://github.com/danielgindi/ios-charts) for this.
This is what the View Controller's content looks like:
The code I use for dequeuing the cells in the viewController's cellForRowAtIndexPath method is kind of the following:
var cell = tableView.dequeueReusableCellWithIdentifier("PerfgraphCell", forIndexPath: indexPath) as? UITableViewCell
if cell == nil {
let nib:Array = NSBundle.mainBundle().loadNibNamed("PerfgraphCell", owner: self, options: nil)
cell = nib[0] as? FIBPerfgraphCell
}
cell.setupWithService(serviceObject, andPerfdataDatasourceId: indexPath.row - 1)
return cell!
and in the cell's code I have a method "setupWithService" which pulls the datasources from an already existing object "serviceObject" and inits the drawing of the graph, like this:
func setupWithService(service: ServiceObjectWithDetails, andPerfdataDatasourceId id: Int) {
let dataSource = service.perfdataDatasources[id]
let metadata = service.perfdataMetadataForDatasource(dataSource)
if metadata != nil {
if let lineChartData = service.perfdataDatasourcesLineChartData[dataSource] {
println("starting drawing for \(lineChartData.dataSets[0].yVals.count) values")
chartView.data = lineChartData
}
}
}
Now the problem: depending on how many values are to be drawn in the chart (between 100 and 2000) the drawing seems to get pretty complex. The user notices that when he scrolls down: as soon as a cell is to be dequeued that contains such a complex chart, the scrolling gets stuck for a short moment until the chart is initialized. That's of course ugly!
For such a case, does it make sense to NOT dequeue the cells on demand but predefine them and hold them in an array once the data that is needed for graphing is received by the view controller and just pull the corresponding cell out of this array when it's needed? Or is there a way to make the initialization of the chart asynchronous, so that the cell is there immediately but the chart appears whenever it's "ready"?
Thanks for your responses!
What you're trying to do is going to inevitably bump into some serious performance issues in one case or another. Storing all cells (and their data into memory) will quickly use up your application's available memory. On the other hand dequeueing and reloading will produce lags on some devices as you are experiencing right now. You'd be better off by rethinking your application's architecture, by either:
1- Precompute your graphs and export them as images. Loading images into and off cells will have much less of a performance knockoff.
2- Make the table view into a drill down menu where you only show one graph at a time.
Hope this helps!

UITableView Lags when user scrolling

I'm using Xamarin to develop a mobile app in ios.
The problem is that when you scrolling the UITableView lags and its too slow.
Here my method GetCell:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
string identifier = #"PostCell";
var post = Posts [indexPath.Row];
PostCellVC postCell = tableView.DequeueReusableCell (identifier) as PostCellVC;
if (postCell == null)
{
postCell = new PostCellVC (identifier);
}
postCell.btnComments.SetTitle("0 Comments",UIControlState.Normal);
postCell.lblDescription.Text = post.PostText;
postCell.btnComments.SetTitle(string.Format("{0} Comments",post.Comments.Length.ToString()),UIControlState.Normal);
postCell.btnLikes.SetTitle(post.Likes.ToString() + " Likes",UIControlState.Normal);
postCell.btnUser.SetTitle(post.UserName.ToString(),UIControlState.Normal);
postCell.imgLike.Image = UIImage.FromBundle ("Images/likeOn.png");
var actionSheet = new UIActionSheet ("Share options");
actionSheet.CancelButtonIndex = 2;
actionSheet.AddButton ("Facebook");
actionSheet.AddButton ("Twitter");
actionSheet.AddButton ("Cancel");
postCell.lblFecha.Text = GetPrettyDate (post.Date);
//postCell.btnShare.TouchUpInside += share;
if (post.PictureUrl != null) {
postCell.imgPost.Image = LayoutHelper.ImageFromUrl (post.PictureUrl);
}
return postCell;
}
As #CRDave points out, you should try lazy loading your images. Here is a sample of how to do this in Xamarin.
Also, why are you creating a new ActionSheet for every cell (which you don't use)? Since you can only show one ActionSheet at a time, just create it once and use it for every cell.
Finally, try reusing UIImage for "likeOn.png" instead of loading it from the bundle for every cell.
You shouldn't add views or download images in your GetCell method. This method is called repeatedly as the user scrolls the table, if it does any long-running operation, it will lead to laggy scrolling.
The method implementation should be really lightweight. The only things that should be done is to dequeue a reusable cell or create a new one and update the data for that cell.
In your specific case, the lag is cause by downloading a photo from a remote URL. This image download should be lazy loaded. Ideally the download itself occurs in a background thread and once the photo is downloaded, then only thing done in the UI thread is updating the UIImageView with the photo.
A few good resources for anyone experiencing issues like this:
https://learn.microsoft.com/en-us/xamarin/ios/user-interface/controls/tables/customizing-table-appearance
https://github.com/luberda-molinet/FFImageLoading
Please check this topic Xcode table view lag when scrolling also you can check this topic iOS table view lag issue, i'm using dispatch and save cache you should save your datas on cache and also use dispatch_async

Resources