UITableView Lags when user scrolling - ios

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

Related

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:

Custom swipeable Table View Cell, Close other once new one open

I'm following tutorial from raywenderlich (How To Make A Swipeable Table View Cell With Actions) site, that shows you how to create custom cell with layers and delegate.
Now I got everything working correctly, buy I would like one of my cells to close if other cell open, how can I achieve this? or Like Messenger app, don't allow users to open another cell option unless they close the current one.
I can't wrap my head around this, I see few other people also ask the same question in comments, but no one reply.
Anyone with Objective-C knowledge, it's okay, I can translate it to swift myself.
The reason I'm using this tutorial, is because the Apple API doesn't allow custom button (using PaintCode) to be used as Action button.
I find a very simple solution for anyone else who trying to achieve the same method.
Create a Method - closeOtherCells:
Since we store all cells in NSMutableSet we can see which ones are available for closing.
func closeOtherCells(close close: Bool){
if close{
//Check For Available Cell
for cells in cellsCurrentEditing {
//Get Table Cells at indexPath
var cellToClose: CustomTableViewCell = self.tableView.cellForRowAtIndexPath(cells as! NSIndexPath) as! CustomTableViewCell
//Call Reset Method in our Custom Cell.
cellToClose.resetConstraintContstantsToZero(true, notifyDelegateDidClose: true)
}
}
}
Now Simply in your cellDidOpen: delegate method call closeOtherCells: with true parameter.
func cellDidOpen(cell: UITableViewCell) {
//Close Other Cells
closeOtherCells(close: true)
//Store Open Cell in NSMutableSet
let indexPath: NSIndexPath = self.tableView.indexPathForCell(cell)!
self.cellsCurrentEditing.addObject(indexPath)
}
I hope this help others. :)

TableView with Image SubView performance Issue

Ive got one question about using a subView in my custom TableViewCells. On certain rows i want to one or more images, and i am doing this programmatically with:
func addImageToCell(image: UIImage, initialYCoordinate: CGFloat, initialHeight: CGFloat, initialXCoordinate: CGFloat,imageUUID: String) {
imageButton = UIButton.buttonWithType(UIButtonType.Custom) as? UIButton
.....
imageButton!.addTarget(formVC, action: "imageButtonPressed:", forControlEvents: .TouchUpInside)
self.contentView.addSubview(imageButton!)
}
That works fine. But, when i scroll my TableView and it comes to an row with an Image, there is a small "lag". Normally it is totally smooth, but when he is trying to load this cell, there is a (maybe 0,1 seconds) lag in the software.
Is there a better way to do this programmatically without any lags? Ill load my Images on initializing the UITableViewController from a CoreData Fetch.
This is my cellForRowAtIndexPath
if(cellObject.hasImages == true) {
cell.qIndex = indexPath.row
var initialXCoordinate:CGFloat = 344.0
let questionImages = formImages[cellObject.qIndex] as [String: Images]!
for (key,image) in questionImages {
let thumbnailImage = UIImage(data: image.image)
cell.addImageToCell(thumbnailImage, initialYCoordinate: 44.00 ,initialHeight: cellObject.rowheight + cellObject.noticeHeight!, initialXCoordinate: initialXCoordinate, imageUUID: image.imageUUID)
initialXCoordinate += 130
}
}
Any Ideas? Thanks in advance
Edit: Ill use this to prevent the Buttons to get reused:
override func prepareForReuse() {
super.prepareForReuse()
if(self.reuseIdentifier != "CraftInit") {
for item in self.contentView.subviews {
if(item.isKindOfClass(UIButton)) {
item.removeFromSuperview()
}
}
}
The lag is most probably caused by the allocation of new memory for UIButton, adding it to cell's contentView and loading an image into it at the time of the cell being reused. That's one problem.
The other problem is that you're creating a button per every reuse of the cell. Basically, every time you scroll the cell out of the visibility range and scroll it back - you add another button with another image, that also consumes memory and performance.
To make this work properly you need to create a custom table view cell with a maximum amount of images (buttons) pre-rendered and hide the ones you don't need to show.
Do not add / remove subviews while reusing the table view cell, hide and show them instead, it's much faster.
Where are you removing these subviews? Because if you're not removing them, as the user scroll, you'll keep on adding subviews to the same cell over and over again, degrading performance.
My approach usually is to to have such views constructed when the cell is created rather than in cellForRowAtIndexPath, but I'm guessing you can't do that as you don't know the number of images. That said, you could still formulate a strategy where these subviews are added at cell creation, and then reused as per your model.
Another suggestion is to have the UIImage objects created all at once, outside cellForRowAtIndexPath.

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.

Clicking a SegmentedControl to UITableViewCell in Monotouch causes SIGABRT

In my Monotouch application for the iPhone, I'm trying to add a segmented controller to a UITableViewCell to act as a toggle for a value in each row. Everything displays correctly, and the first rows will initially work correctly when clicked. However, after the row has left the screen and is reloaded, if I clicked the segmented controller again I receive an exception:
Terminating runtime due to unhandled exception
[ERROR] FATAL UNHANDLED EXCEPTION: System.Exception: Selector invoked from objective-c on a managed object of type MonoTouch.UIKit.UIControlEventProxy (0x16D793C0) that has been GC'ed ---> System.MissingMethodException: No constructor found for MonoTouch.UIKit.UIControlEventProxy::.ctor(System.IntPtr)
I have heard of similar issues with people using Monotouch try to put buttons in table cells. The general recommendation has been to add each table cell to a List to prevent it from being garbage collected, but this has not worked for me.
Is there a solution to get around this problem?
Here is a somewhat simplified version of my GetCell code:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
Hashtable rowData = dataList [indexPath.Row];
FilterListToggleCell cell;
cell = (FilterListToggleCell)tableView.DequeueReusableCell ("FilterTogglePlusCell");
cacheList.Add(cell.Toggle);//cache only the segcontrol
//cacheList.Add(cell);//also previously tried caching the whole cell
cell.Title.Text = (string)rowData["title"];
if(additionalFields.Contains((string)rowData["key"])){
cell.Toggle.SelectedSegment = 0;
} else {
cell.Toggle.SelectedSegment = 1;
}
cell.Toggle.ValueChanged += (sender, e) => {
UISegmentedControl toggle = (UISegmentedControl)sender;
if(toggle.SelectedSegment == 0){
Console.WriteLine("Toggle YES");
} else {
Console.WriteLine("Toggle NO");
}
};
cell.Toggle.Tag = indexPath.Row;
return cell;
}
You're right about the basic condition. This condition happens because there's no managed reference to the UITableViewCell that you return from the GetCell method. As such the GC can collect and free it (and that will cause issues for things like events that wants to call back to the managed instance). See this answer for more details.
Now I'm not sure why this approach did not work for you. However your code above seems incomplete, e.g.
cell = (FilterListToggleCell)tableView.DequeueReusableCell ("FilterTogglePlusCell");
can return null which is when you should be creating more cells (in fact there's no condition where you actually create any new cell, so there's nothing that can be re-used).
Your next line will throw a NullReferenceException when cell is null (because of cell.Toggle).
Note that this does not explain your crash - I suspect there's some missing code that make it works (create cells) and that might not cache them correctly.
I was able to get this working without needing the extra cache based on information from this question: How do I remove/unregister event handler from an event?
The ValueChanged line was replaced with:
cell.Toggle.AddTarget(this, new Selector("ToggleChange"),UIControlEvent.ValueChanged);
and another method was added to my UITableViewDataSource
[Export ("ToggleChange")]
void OnChange(UISementedControl toggle){
if(toggle.SelectedSegment == 0){
Console.WriteLine("Toggle YES");
} else {
Console.WriteLine("Toggle NO");
}
}

Resources