Monotouch: The correct way to reuse a UITableViewCell - uitableview

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.

Related

Cell Index Path while scrolling does not return intended results

I have a tableview that the user edits information in using textfields, and I store that information into an array that keeps track of all the values. The issue occurs when the user scrolls back to a cell they already edited and the values the added previously are now values from other cells.
I understand that cells are reused, and as a result their data needs to be updated whenever they are being viewed again. I also learned that cellforrowat is called every time a cell is loaded into the view as opposed to just the first time a cell is created. I made a test project to figure out my problem.
My first attempt at solving the problem went like so
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
if cellInformation.count < (indexPath.row + 1) // Newly made cell
{
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row) // Cell we've seen before
{
cell.configure(Value: cellInformation[indexPath.row]!.value) // Sets the textField.text to be the same as the cells value
}
This worked better but when I scrolled back to the top of my tableview, the top most cells were still getting random data. My next attempt generated an ID tag for each cell, and then checking if the id tag of the cell at cellforrowat matched any of the one's in the array.
if cellInformation.count < (indexPath.row + 1) // 0 < 1
{
cell.idTag = idTagCounter
idTagCounter += 1
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row)
{
for i in 0...idTagCounter - 1
{
if(cell.idTag == cellInformation[i]?.idTag)
{
cell.configure(Value: cellInformation[i]!.value)
}
}
cell.configure(Value: cellInformation[indexPath.row]!.value)
}
This got pretty much the same results as before. When I debugged my program, I realized that when i scroll down my tableview for the first time, indexPath.row jumps from a value like 7 down to 2 and as I scroll more and more, the row goes further away from what it should be for that cell until it eventually stops at 0 even if there are more cells i can scroll to. Once the row hits 0, cellforrowat stops being called.
Any ideas on how i can accurately assign a cells values to the information in my array?
Your premise is wrong:
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
You should set up a model object that contains the data for the entries in your table view, and your cellForRowAt() method should fetch the entry for the requested IndexPath.
Your model can be as simple as an array of structs, with one struct for each entry in your table. If you use a sectioned table view you might want an array of arrays (with the outer array containing sections, and the inner arrays containing the entries for each section.)
You should not be building your model (array) in calls to cellForRowAt().
You also should not, not NOT be storing cells into your model. You should store the data that you display in your cells (text strings, images, etc. Whatever is appropriate for your table view.)
Assume that cellForRowAt() can request cells in any order, and ask for the same cells more than once.
Say we want to display an array of animals, and a numeric age:
struct Animal {
let species: String
let age: Int
}
//Create an array to hold our model data, and populate it with sample data
var animals: [Animal] = [
Animal(species: "Lion", age: 3),
Animal(species: "Tiger", age: 7),
Animal(species: "Bear", age: 4)
]
//...
func cellForRow(at indexPath: IndexPath) -> UITableViewCell? {
let cell = dequeueReusableCell(withIdentifier: "cell" for: indexPath)
let thisAnimal = animals[indexPath.row]
cell.textLabel.text = "\(thisAnimal.species). Age = \(thisAnimal.species)"
}
Note that for modern code (iOS >=14), you should really be using UIListContentConfigurations to configure and build your cells.

Must I keep my own UITableView "isSelected" array for multiselect?

I am trying to use a UITableview with multiple selection on and a check mark accessory view for selected rows. This is mostly working if I turn on and off the accessory view in tableView:didSelectRow.
However, I tried to build a selectAll method, and I found that the array of selected cells was being cleared after I had spun through all the cells and selected them if I then call reloadData().
I suspect reloading the table clears selection. I don't know of any other way to have all the cells drawn after I set the selected flag and accessory view.
I am wondering if I need to keep my own array of selected rows. Has anyone else built something like this? Its seems like a common scenario.
Any tips or sample code appreciated.
Take an Array and add the indexPath of each selected cell into it and put a condition in cellForRowAt... that if the Array contains that particular indexPath, set it as selected.
There are two approaches you can take. One is to track the selected row numbers. To do this, you can use an NSMutableIndexSet or its Swift counterpart IndexSet.
Essentially, when a row is selected, add it to the set. When you deselect it, remove it from the set. In cellForRowAtIndexPath you can use containsIndex to determine if a check mark should be shown or not.
Since you explicitly mention an issue with selection when you reload the table, it is worth considering the issue with storing row numbers (whether in a set or an array), and that is that row numbers can change.
Say I have selected rows 4,7 and 9 and these values are stored in the index set. When I reload the data, new data may have been inserted after the "old" row 8, so now I should be selecting rows 4,7 and 10, but I will be selecting 4,7 and 9 still.
A solution to this is to store some sort of unique identifier for the data that should be selected. This will depend on your data, but say you have a string that is unique for each item. You can store this string in a NSMutableSet or Swift Set, which again makes it easy to check if a given item is selected using contains
you need add some functionality in cellForRowAtIndexPath method like this ang your view controller code like this
let we take one example of photo gallery application
class CreateEvent: UIViewController,UITableViewDataSource,UITableViewDelegate {
var yourArray : [Photo] = [Photo]()
//MARK: - Content TableView Methods
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath)
let objPhoto = yourArray[indexPath.row]
if objPhoto.isPhotoSelected == true
{
cell.accessoryType = .Checkmark
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let objPhoto = yourArray[indexPath.row]
objPhoto.isPhotoSelected = true
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell.accessoryType = .Checkmark
}
//MARK: Action Method
func selectAllPhoto()
{
for objPhoto in yourArray
{
objPhoto.isPhotoSelected = true
}
tableView.reloadData()
}
}
and one more thing you need to create your custom object like
class Photo: NSObject {
var photoName:String = ""
var isPhotoSelected = false
}
hope this will help you
The best approach for multiple selection is
Take a model object, in that take all your attributes and one extra boolean attribute (like isSelected) to hold the selection.
In case of selecting a row
Fetch the relevant object from the array
Then update the isSelected boolean (like isSelected = !isSelected) and reload table.
In case of select all case
Just loop through the array.
Fetch the model object from array.
make the isSelected = true.
After completion of loop, reload the table.

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

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.

Pass table cells textlabel data to array in swift

I want to pass table cell's textLabel data of UITableViewController to NSArray. Those cell have identifier name Cells and accessory type checkmark
Code that does exactly what you asked is here:
func getCellsData() -> [String] {
var dataArray: [String] = []
for section in 0 ..< self.tableView.numberOfSections() {
for row in 0 ..< self.tableView.numberOfRowsInSection(section) {
let indexPath = NSIndexPath(forRow: row, inSection: section)
let cell = self.tableView.cellForRowAtIndexPath(indexPath)!
if cell.reuseIdentifier == "Cells" && cell.accessoryType == UITableViewCellAccessoryType.Checkmark {
dataArray.append(cell.textLabel!.text!)
}
}
}
return dataArray
}
But I would like to recommend you find different approach, because this is the rude traverse of tableView. You probably have your dataSource model that can give you ll data you need. Additionally, this code doesn't check for errors, for example if there is no text in cell at some indexPath
Two things I'd advise.
First, looks like you're using the checkmark accessory to indicate multiple selections. It's really intended for single selection, like a radio button. Better to use the allowMultipleSelectionsoption on the tableview. This will allow...
...the second thing. Copying text from cells into an array is the wrong way round to do it. Better to ask the table view and call it's - (NSArray *)indexPathsForSelectedRows this will give you an array of index paths to selected cells then you can ask for each cell and grab any data you want from it. This gives you better live data and prevents you unknowingly creating a circular reference from the view controller of the tableview and the content in the tableview cells.

Create and use custom prototype table cells in xamarin ios using storyboard

Is it possible to create and use 'Custom' prototype table cells in xamarin ios (monotouch) using storyboard?
I could only find this by Stuart Lodge explaining a method using xib/nibs:
http://www.youtube.com/watch?feature=player_embedded&v=Vd1p2Gz8jfY
Let's answer this question! I was also looking for this one. :)
1) Open Storyboard where you have your ViewController with TableView:
Add prototype cell (if there is no cell added before):
Customize cell as you want (in my case there is custom UIImage and Label):
Remember to set height of the cell. To do it select your whole TableView and from the Properties window select "Layout" tab. On the top of the properties window you should see "row height" - put the appropriate value:
Now select prototype cell once again. In the Properties window type the name of the class (it will create code-behind class for it). In my case this is "FriendsCustomTableViewCell". After that provide "Identifier" for your cell. As you can see my is "FriendCell". Last thing to set is "Style" property set to custom. "Name" field should be empty. Once you click "enter" after typing "Class" code-behind file will be automatically created:
Now code behind for the cell should look like below:
public partial class FriendsCustomTableViewCell : UITableViewCell
{
public FriendsCustomTableViewCell (IntPtr handle) : base (handle)
{
}
public FriendsCustomTableViewCell(NSString cellId, string friendName, UIImage friendPhoto) : base (UITableViewCellStyle.Default, cellId)
{
FriendNameLabel.Text = friendName;
FriendPhotoImageView.Image = friendPhoto;
}
//This methods is to update cell data when reuse:
public void UpdateCellData(string friendName, UIImage friendPhoto)
{
FriendNameLabel.Text = friendName;
FriendPhotoImageView.Image = friendPhoto;
}
}
In UITableViewSource you have to declare cellIdentifier at the top of the class (in my case it is "FriendCell") and in "GetCell" method you have to cast cells and set data for them:
string cellIdentifier = "FriendCell";
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
FriendsCustomTableViewCell cell = (FriendsCustomTableViewCell) tableView.DequeueReusableCell(cellIdentifier);
Friend friend = _friends[indexPath.Row];
//---- if there are no cells to reuse, create a new one
if (cell == null)
{ cell = new FriendsCustomTableViewCell(new NSString(cellIdentifier), friend.FriendName, new UIImage(NSData.FromArray(friend.FriendPhoto))); }
cell.UpdateCellData(friend.UserName, new UIImage(NSData.FromArray(friend.FriendPhoto)));
return cell;
}
That's it, now you can use your custom cells. I hope that it will help.

Resources