How to get a smooth select/deselect animation in ios UITableView - ios

I'm working on an iOS app using Xamarin and MVVM Cross.
I have a table view from which one row can be selected at a time, which is indicated using the Checkmark accessory on the UITableViewCell. I'm essentially following the answer from this: ✔ Checkmark selected row in UITableViewCell
The problem is that Reloading the table data (or even reloading just the two rows that changed) seems to interrupt the animation of De-selecting the row. I would like it to fade out nicely. For example, on an iPad, the behavior in Settings -> Notes -> Sort Notes By is exactly what I want. Touching a row highlights it, letting go moves the checkmark, and then the highlight fades out.
After messing with this for several hours, the only solution I found was to Reload the rows to update the checkmark, then manually Re-select and De-select the row. This works fine, but feels like a big hack. Is there a cleaner way to do this?
Here's the relevant snippets:
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
SetCellCheckmark(tableView, indexPath);
base.RowSelected(tableView, indexPath); //This handles de-select via the MvxTableViewSource DeselectAutomatically property
}
private void SetCellCheckmark(UITableView tableView, NSIndexPath newSelection)
{
if (newSelection == null)
return;
var rowsToUpdate = new NSIndexPath[_checkedRow == null ? 1 : 2];
rowsToUpdate[0] = newSelection;
if (_checkedRow != null)
rowsToUpdate[1] = _checkedRow;
_checkedRow = newSelection;
tableView.ReloadRows(rowsToUpdate, UITableViewRowAnimation.None);
tableView.SelectRow(newSelection, false, UITableViewScrollPosition.None); //This feels like a hack
}
private UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var cell = TableView.DequeueReusableCell(MyCellType, indexPath);
if (cell == null)
return null;
if (indexPath == _checkedRow)
{
cell.Accessory = UITableViewCellAccessory.Checkmark;
}
else
{
cell.Accessory = UITableViewCellAccessory.None;
}
return cell;
}
If you have an answer in Objective-C, I could probably translate it into Xamarinland.

Related

UItableView scrolled to bottom after InsertRows and animate inserting above existing rows in ios Xamarin

I'm trying to insert 30 items in a UITableView, but when new items are inserted while scrolling, the content automatically scrolls down. How to fix it? You can reply using swift
public async void Update()
{
NSIndexPath[] arr = null;
Items[] news = null;
await Task.Run(() =>
{
news = DownLoadService.GetNewItems();
if (news != null)
{
var l = list.Length;
arr = new NSIndexPath[news.Length];
for (var i = 0; i < arr.Length; i++)
{
arr[i] = NSIndexPath.Create(0, i + l);
}
}
});
TableData.BeginUpdates();
list = list.Union(news).ToArray();
TableData.InsertRows(arr, UITableViewRowAnimation.None);
TableData.LayoutIfNeeded();
TableData.EndUpdates();
}
Call to Update
public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
if (indexPath.Row == itemclick.getLength()-10)
table.Update();
}
Visualiation
Cause:
iOS has NSRunLoop and it has five modes ,as we can seen in following image
The default mode is NSDefaultRunLoopMode .When you scroll the tableview , it will change to UITrackingRunLoopMode .However , the download event still works on default mode.So it will be conflict with scrolling .
Solution:
You can download the data firstly ,and when scroll to bottom, insert them to the dataSource . For example
public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
if (indexPath.Row == itemclick.getLength()-10)
// download the data
if (indexPath.Row == itemclick.getLength()-1)
{
//insert the data to source and reload the tableview .
}
}
Thanks #Lucas Zhang
Solution is download new items when tenths item is visible, and insert new items when last item become visible

ForceUpdateSize ListView issue on iOS

I have a custom ListView using custom ViewCells with radio buttons. On clicking of each of the radio buttons the ListView dynamically resizes its height to hide/show a comment box.
On using ForceUpdateSize in iOS platform, the ListView performance slows down rapidly on clicking the radio buttons. The app eventually hangs and stops responding.
Is there an alternative solution to use instead of ForceUpdateSize to dynamically expand the ListView row at runtime?
Define a ViewCell Size Change event wherever you need to change your ViewCell size
public static event Action ViewCellSizeChangedEvent;
In your case, it should be triggered by your radio button. Call it like this:
ViewCellSizeChangedEvent?.Invoke();
It will then use the ListView renderer to update the iOS TableView.
public class CustomListViewRenderer : ListViewRenderer
{
public CustomListViewRenderer()
{
WhatEverContentView.ViewCellSizeChangedEvent += UpdateTableView;
}
private void UpdateTableView()
{
var tv = Control as UITableView;
if (tv == null) return;
tv.BeginUpdates();
tv.EndUpdates();
}
}
It should solve your performance problem while keep using your Xaml instead of creating a custom ViewCell which is not needed.
My solution is: Try to use custom renderer. When the button clicked, I use tableView.ReloadRows() to dynamically change the size of a cell.
Firstly, define a bool list whose items equal to Rows you want to show in the Source. I initialize its items with false at first time.
List<bool> isExpanded = new List<bool>();
public MyListViewSource(MyListView view)
{
//It depends on how many rows you want to show.
for (int i=0; i<10; i++)
{
isExpanded.Add(false);
}
}
Secondly, Construct the GetCell event(I just put a UISwitch in my Cell for testing) like:
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
MyListViewCell cell = tableView.DequeueReusableCell("Cell") as MyListViewCell;
if (cell == null)
{
cell = new MyListViewCell(new NSString("Cell"));
//This event is constructed in my Cell, when the switch's value changed it will be fired.
cell.RefreshEvent += (refreshCell, isOn) =>
{
NSIndexPath index = tableView.IndexPathForCell(refreshCell);
isExpanded[index.Row] = isOn;
tableView.ReloadRows(new NSIndexPath[] { index }, UITableViewRowAnimation.Automatic);
};
}
cell.switchBtn.On = isExpanded[indexPath.Row];
return cell;
}
At last, we can override the GetHeightForRow event. Set a large or small value depending on the item in the isExpanded:
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
if (isExpanded[indexPath.Row])
{
return 80;
}
return 40;
}
Here is some part of my cell for you referring to:
//When switch's value changed, this event will be called
public delegate void RefreshHanle(MyListViewCell cell, bool isOn);
public event RefreshHanle RefreshEvent;
switchBtn.AddTarget((sender, args) =>
{
UISwitch mySwitch = sender as UISwitch;
RefreshEvent(this, mySwitch.On);
}, UIControlEvent.ValueChanged);

How to prevent moving of a UITableViewCell to a specific index

In my application, the user has the ability to move UITableViewCell rows around using the edit button and drag and drop.
I do not want the user to be able to move a cell to row 0. I already have this working for my NSMutableArray where if the row is 0 then don't rearrange the objects in collection. But even with that in place, the visible table still shows the cell at row 0.
How can I prevent this graphically?
I've tried the following:
-(NSIndexPath*)tableView:(UITableView*)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath*)sourceIndexPath toProposedIndexPath:(NSIndexPath*)proposedDestinationIndexPath
{
if(proposedDestinationIndexPath.row == 0)
{
return 1;
}
return proposedDestinationIndexPath;
}
But it crashes with a EXC_BAD_ACCESS error when I try to move cell at row 1 to row 0.
Your approach is correct. The problem is that tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath method should return NSIndexPath*, but you return an integer. The fix is simple:
if(proposedDestinationIndexPath.row == 0)
{
return [NSIndexPath indexPathForRow:1 inSection: proposedDestinationIndexPath.section];
}
Swift Version
if(proposedDestinationIndexPath.row == 0){
return IndexPath.init(row: 1, section: proposedDestinationIndexPath.section)}

iOS : Expandable list view not drawing newly added control

I am trying to create an expandable list view in iOS using Xamarin with MvvmCross.
The scenario is that I have a listview, and when a row in the listview is selected, it expands (animates) to reveal a collection view, loaded in via lazyloading.
Here is the code I have so far:
Adapter :
public class MercatoAnimatedExpandableTableSource : MvxTableViewSource
{
private readonly string _key;
private readonly List<object> items;
private Dictionary<object, bool> expandableState = new Dictionary<object, bool>();
public MercatoAnimatedExpandableTableSource(UITableView tableView, IEnumerable<object> items, UINib nib, string key)
: base(tableView)
{
_key = key;
this.items = items.ToList();
this.items.ForEach(x => expandableState[x] = true);
tableView.RegisterNibForCellReuse(nib, key);
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var cell = tableView.DequeueReusableCell(_key);
cell.Frame = new RectangleF(cell.Frame.X, cell.Frame.Y, cell.Frame.Width, GetHeightForRow(tableView, indexPath));
return cell;
}
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
base.RowSelected(tableView, indexPath);
var isExpanded = expandableState[items[indexPath.Row]];
expandableState[items[indexPath.Row]] = !isExpanded;
var row = this.GetCell(tableView, indexPath);
(row as FamilysubgroupViewCell).ExpandCells();
tableView.ReloadRows(new[] { indexPath }, UITableViewRowAnimation.Automatic);
}
public override float GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
var isExpanded = expandableState[items[indexPath.Row]];
if (isExpanded)
{
return 25;
}
return 400;
}
protected override object GetItemAt(NSIndexPath indexPath)
{
return items[indexPath.Row];
}
}
Cell :
public FamilySubgroupViewCell (IntPtr handle) : base (handle)
{
this.DelayBind(() =>
{
this.FamilyCollectionViewContainer.BackgroundColor = UIColor.Blue;
var bindingset = this.CreateBindingSet<FamilySubgroupViewCell, FamilySubTypeViewModel>();
bindingset.Bind(this.FamilyGroupTitleLabel).To(x => x.FamilySubType.FamilySubGroupDescription);
bindingset.Bind(this.FamilyGroupDescLabel).To(x => x.FamilySubType.FamilySubGroupDetail);
bindingset.Apply();
});
}
public void ExpandCells(Action onUpdate)
{
var context = this.DataContext as FamilySubTypeViewModel;
context.PopulateAndRun(() =>
{
Debug.WriteLine("Populating Models complete : now showing cells");
var collection = new FamilySubGroupModelsCollection();
collection.ViewModel = context;
Debug.WriteLine("FamilySubGroupCell : Showing {0} Items", context.FamilyModels.Count);
this.FamilyCollectionViewContainer.Add(collection.View);
if (onUpdate != null) onUpdate();
});
}
Issue
So when a cell is selected, it flips the 'isExpanded' property which gives it a new height - that works fine. The cell is then sent a message to 'expand' the cell, which in turn loads a new collection and adds it to the view (via View outlet).
However, the collection is never re-rendered on the view. It populates fine, adds to the view but it is never actually visible on the newly expanded cell. If i load it in when the cell is initially created in the DelayBind() method (ie - no lazy loading) then it works ok.
Have tried things like redrawing the cell
this.Draw(this.Bounds);
after the collection was added to the view, but to no avail.
What am I doing wrong?
This block of code feels like it might be confusing your display:
var row = this.GetCell(tableView, indexPath);
(row as FamilysubgroupViewCell).ExpandCells();
tableView.ReloadRows(new[] { indexPath }, UITableViewRowAnimation.Automatic);
What this does is:
get the current cell and tell it to expand
then ask the table to reload the row (at which point a different cell instance might be used).
I'd also worry that the line:
var context = this.DataContext as FamilySubTypeViewModel;
would link your cells sub-display to the current viewmodel - and this wouldn't update if the cell were reused (given a new viewmodel).
One quick (and only slightly dirty) way around both of these problems would be to change the override for GetOrCreateCellFor to something like:
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var cell = tableView.DequeueReusableCell(_key);
cell.Frame = new RectangleF(cell.Frame.X, cell.Frame.Y, cell.Frame.Width, GetHeightForRow(tableView, indexPath));
((MyCellType)cell).ShowExpandedState(expandableState[items[indexPath.Row]]);
return cell;
}
Where ShowExpandedState(bool) would be capable of resetting the cell's expanded area after reuse.
Other options:
Instead of this, you could simply return a different cell type for expanded versus non-expanded rows?
Or you could put the expanded/non-expanded information in the Cell's ViewModel so that it can data-bind to that expanded state and update it's own layout? (you'd still need some mechanism to tell the table about the size change though)
On:
this.Draw(this.Bounds);
I'm not too surprised this didn't work - generally you have to encourage the system to redraw views - using things like SetNeedsDisplay (and sometimes after child changes SetNeedsLayout too)

Monotouch: The correct way to reuse a UITableViewCell

I'm figuring out the correct way to reuse cells in a UITableView and I would know if the mechanism I'm using is correct.
The scenario is the following.
I've a UITableView that displays a list of data obtained from a web service.
_listOfItems = e.Result as List<Item>;
where _listOfItems is an instance variable.
This list is passed to a class that extends UITableViewSource. Obviously this class override GetCell method to visualize data in this manner:
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
UITableViewCell cell = tableView.DequeueReusableCell(_cID);
Item item = _listOfItems[indexPath.Row];
int id = item.Id;
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Value1, _cID);
cell.Tag = id;
_cellControllers.Add(id, cell);
}
else
{
bool vb = _cellControllers.TryGetValue(id, out cell);
if(vb)
{
cell = _cellControllers[id];
}
else
{
cell = new UITableViewCell(UITableViewCellStyle.Value1, _cID);
cell.Tag = id;
_cellControllers.Add(id, cell);
}
}
cell.TextLabel.Text = item.Title;
return cell;
}
where
_cID is an instance variable identifier for the cell
string _cID = "MyCellId";
_cellControllers is a dictionary to store cell and the relative id for an Item instance
Dictionary<int, UITableViewCell> _cellControllers;
I'm using the dictionary to store cells bacause when a row is tapped, I have to retrieve the id for the cell tapped (through cell.Tag value), do some other operation - i.e.retrieve other some data from the service - and then update that cell again with new values. In this case each cell has to be unique.
So, my question is:
Is this the right manner to reuse cell or is it possible to figured out another solution to reuse cell and guaranteeing each tap for a cell is unique?
I hope it's all clear :) Thank you in advance. Regards.
I think what you are doing here is a bit too much. Especially the part where you are holding extra references to cells in a Dictionary.
I would do it differently. The DequeueReusableCell method is there so that you can retrieve any existing cell, but not necessarily the values it contains. So something like this will suffice:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
int rowIndex = indexPath.Row;
Item item = _listOfItems[rowIndex];
UITableViewCell cell = tableView.DequeueReusableCell(_cID);
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Value1, _cID);
}
// Store what you want your cell to display always, not only when you are creating it.
cell.Tag = item.ID;
cell.TextLabel.Text = item.Title;
return cell;
}
Then, in the RowSelected method, change your data source:
public override void RowSelected (UITableView tableView, NSIndexPath indexPath){
// Do something here to change your data source _listOfItems[indexPath.Row] = new Item();
}
If I understand correctly what you want to do, this solution is better.
You will save yourself a lot of trouble if you use my MonoTouch.Dialog library that takes care of all of these details for you and does exactly what you want and more. It will let you focus on your app instead of focusing on the administrivia, and I believe will let you move faster, you can get it from:
http://github.com/migueldeicaza/MonoTouch.Dialog
The first problem with this code is that this hardcodes the cells to a single type. In general, you would have to first check the section/row and based on this information determine the kind of cell that you want.
Once you determine the kind of cell you want, then you use this information to call DequeueReusableCell with the token associated with this cell ID. This is different for example for a cell that contains an entry line vs a cell that contains an image. You need to dequeue the right kind of cell.
You do not need to make every cell unique, all the information that you need is in the section/row, so what you need is something that maps a section/row to your unique cell. A simple approach is that there is a single section, and the row is the index into your data array that you want to fetch more information from.
A small tutorial on best practices when creating these cells can be found here:
http://tirania.org/monomac/archive/2011/Jan-18.html
If you were using MonoTouch.Dialog, your whole code could be:
var elements = "foo, bar, baz";
var dvc = new DialogViewController ();
dvc.Root = new RootElement ("My results") {
from x in elements.Split (',')
select (Element) new StringElement (x);
};
dvc.Root.Add ("Final one");
current.PresentModalViewController (dvc, true);
The above creates a UITableView with 4 rows, one for "foo", "bar" and "baz" using Linq, and adds an extra node at the end just to show how to use the Add API.
There are also many assorted elements you can use, you are not limited to String elements.

Resources