How to reload section when there is only one row in section [Xamarin.iOS]? - uitableview

I have 1 row in section of UITableView and I want to remove that row trying to update table then it gives this exception:
NSInternalInconsistencyException Reason: attempt to delete and reload
the same index path (<NSIndexPath: 0x60000021a020> {length = 2, path =
1 - 9223372036854775807})
Code:
public void DeleteContact(NSIndexPath indexPath, UserData userData)
{
DatabaseService.DeleteItem(userData.ID);
listData();
ListView.Source = new TableSource(_itemdata, OnAddButtonClick, this);
ListView.BeginUpdates();
NSIndexPath[] rowsToReload = new NSIndexPath[]
{
NSIndexPath.FromRowSection(indexPath.Row, indexPath.Section)
};
var numberOfRowsInSection = ListView.NumberOfRowsInSection(indexPath.Section);
if (numberOfRowsInSection == 1)
{
ListView.ReloadSections(NSIndexSet.FromIndex(indexPath.Section), UITableViewRowAnimation.Fade);
ListView.DeleteSections(NSIndexSet.FromIndex(indexPath.Section), UITableViewRowAnimation.Fade);
ListView.EndUpdates();
}
else
{
ListView.ReloadSections(NSIndexSet.FromIndex(indexPath.Section), UITableViewRowAnimation.Fade);
ListView.EndUpdates();
}
}
public void listData()
{
List<UserData> listData = DatabaseService.GetAll();
var sorted = listData.OrderBy(item => item.Name).ToList();
_itemdata = sorted;
}
Then I have tried to update table only without deleting section:
ListView.ReloadSections(NSIndexSet.FromIndex(indexPath.Section), UITableViewRowAnimation.Fade);
ListView.EndUpdates();
With this it gives this exception:
NSInternalInconsistencyException Reason: attempt to insert section 1
but there are only 1 sections after the update
In last I have tried to call ListView.ReloadData(); but this also not updates UITableView.
Anyone know how to achieve this?

Related

How to identify object that is deleted from a collection (array)?

I have following code to handle pull to refresh response
let copyData = data.reversed() // it is pull to refresh response (load page 1)
for (_,element) in copyData.enumerated() {
let foundElement = allObjects.filter{$0.id == element.id} // Find element in main array
if let firstElement = foundElement.first, let index = allObjects.index(of: firstElement) {
allObjects[index] = element // Replace if found
} else {
allObjects.insert(element, at: 0) // Insert if not found
}
}
self.arrayPosts = allObjects
Where data is codable class which is API response of pull to refresh. allObjects is preloaded data with pagination
Question : Suppose In allObjects i have 50 Object (5 Pages of 10 ID is (1 to 50))
User pull to refresh And I load first Page from API (ID 1,2,3,4,5,6,7,10,11) then how to identify which object is deleted (8,9) ?
Should I compare allObjects 's 10th index with data's 10th index object's ID ?
Is it better way to handle this ? Please suggest
Don't compare pages (i.e. 10 items at a time) - if an item is added/deleted the pages will get out of sync, and you'll end up with missing / duplicate objects.
Presumably your objects are sorted by some key / date, etc.
Take the value of the key in your last downloaded object.
Copy all the existing objects with keys <= that last key into a new array.
Compare your downloaded array against this sub-array.
Objects in the downloaded array that are not in the sub-array should be removed.
Here How I handle this. Code is quite complex for first time read but added comments to understand it
func handleResponse(page:Int,isForRefersh:Bool = false, data:Array<InspirePost>) {
guard data.count != 0 else {
// Check if we are requesting data from pull to referesh and First page is empty then we don't have data to show change state to empty
if self.arrayPosts.count == 0 || (isForRefersh && page == 1) {
self.state = .empty
self.tableView.reloadData()
} else {
self.state = .populated(posts: self.arrayPosts)
}
return
}
// Now we need to check if data called by referesh control then
//1) Replace object in array other wise just append it.
var allObjects = self.state.currentPost
if isForRefersh {
// If both array Has same number of element i.e both has page one loaded
if data.count >= allObjects.count {
allObjects = data
} else {
let copyData = data.reversed()
for (_,element) in copyData.enumerated() {
if let index = allObjects.firstIndex(where: {$0.id == element.id}) {
allObjects[index] = element // Replace if found
} else {
allObjects.insert(element, at: 0) // Insert if not
}
}
let minID = data.min(by: {$0.id ?? 0 < $1.id ?? 0})?.id
// DELETE item
let copyAllObject = allObjects
for (_,element) in copyAllObject.enumerated() {
guard let id = element.id, id >= minID ?? 0 else {
continue
}
if !data.contains(element) {
if let indexInMainArray = allObjects.index(where: {$0.id == id}) {
allObjects.remove(at: indexInMainArray)
}
}
}
}
//When we pull to refersh check the curent state
switch self.state {
case .empty,.populated : // if empty or populated set it as populated (empty if no record was avaiable then pull to refersh )
self.state = .populated(posts: allObjects)
case .error(_, let lastState) : // If there was error before pull to referesh handle this
switch lastState {
case .empty ,.populated: // Before the error tableview was empty or popluated with data
self.state = .populated(posts: allObjects)
case .loading,.error: // Before error there was loading data (There might more pages if it was in loading so we doing paging state ) or error
self.state = .paging(posts: allObjects, nextPage: page + 1)
case .paging(_,let nextPage): // Before error there was paging then we again change it to paging
self.state = .paging(posts: allObjects, nextPage: nextPage)
}
case .loading: // Current state was loading (this might not been true but for safety we are adding this)
self.state = .paging(posts: allObjects, nextPage: page + 1)
case .paging(_,let nextPage): // if there was paging on going don't break anything
self.state = .paging(posts: allObjects, nextPage: nextPage)
}
self.arrayPosts = allObjects
} else {
allObjects.append(contentsOf: data)
self.isMoreDataAvailable = data.count >= self.pageLimit
if self.isMoreDataAvailable {
self.state = .paging(posts: allObjects, nextPage: page + 1)
} else {
self.state = .populated(posts: allObjects)
}
self.arrayPosts = self.state.currentPost
}
self.tableView.reloadData()
}
Where I have
indirect enum PostListStatus {
case loading
case paging(posts:[InspirePost],nextPage:Int)
case populated(posts:[InspirePost])
case error (error:String,lastState:PostListStatus) // keep last state for future if we need to know about data or state
case empty
var currentPost:[InspirePost] {
switch self {
case .paging(let posts , _):
return posts
case .populated( let posts):
return posts
case .error( _, let oldPost) :
switch oldPost {
case .paging(let posts , _):
return posts
case .populated( let posts):
return posts
default:
return []
}
default:
return []
}
}
var nextPage : Int {
switch self {
case .paging(_, let page):
return page
default:
return 1
}
}
}
You may make set for both allObjects and data. Then use subtracting(_:) method in set to find the missing one.
Remove those missing ones from the main array and use it. Once you have correct main array elements, page them while displaying.

Xamarin iOS: Dynamic update for UICollectionView Cell

In my xamarin ios project, I am populating the images in UICollectionView cells.It has the ability to delete or rotate the image.I was wondering is there any way I could update the cells as soon as the source changes ?
Cell :
public class MyCellCollectionViewSource: MvxCollectionViewSource
{
public ObservableCollection < Animal > animals;
ViewModel1 vm;
public MyCellCollectionViewSource(ViewModel1 vm, int noOfImages) : base(collectionView)
{
this.vm = vm;
animals = new ObservableCollection < Animal > ();
for (int i = 0; i < noOfImages; i++) {
//animals.Add(i);
switch (i) {
case 0:
animals.Add(new Animal() {
indexCell = 1,
image = vm.Photo1
});
break;
case 1:
animals.Add(new Animal() {
indexCell = 2,
image = vm.Photo2
});
break;
case 2:
animals.Add(new Animal() {
indexCell = 3,
image = vm.Photo3
});
break;
case 3:
animals.Add(new Animal() {
indexCell = 4,
image = vm.Photo4
});
break;
case 4:
animals.Add(new Animal() {
indexCell = 5,
image = vm.Photo5
});
break;
case 5:
animals.Add(new Animal() {
indexCell = 1,
image = newPostDetailViewModel.Photo1
});
break;
}
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = (MyCell) collectionView.DequeueReusableCell(animalCellId, indexPath);
Cell.PopulateCell(indexPath.Row);
cell.Closebtn.TouchUpInside += delegate {
MyCellCollectionViewSource source = collectionView.DataSource as MyCellCollectionViewSource;
vm.DeleteCommand.Execute(indexPath.Row);
ReloadData();
};
cell.UserPostbtn.TouchUpInside += delegate {
MyCellCollectionViewSource source = collectioview.DataSource as MyCellCollectionViewSource;
newPostDetailViewModel.EditPhotoCommand.Execute(indexPath.Row);
ReloadData();
};
}
}
The cell has CloseBtn and UserPostBtn.
When I hit CloseBtn, I can see in my observableCollection 'animals' that the cell is deleted. But I cannot see them reflected on my collectionview, If i go back and reload the complete page again, then I can see my changes.
Same way, If I click on UserPostBtn, it would rotate the image but those are changes are not reflected dynamically, I would have to go back and reload the reload the whole page to update the cells.

how to avoid uitableview cell refresh flicker when reload cell

I used the monotouch.dialog to develop one PrivateMsg talk screen. But when I reload the cell , the cell flicker, how to avoid it ?
add new pm msg accross 2 step: 1. append a element cell: State.Insert(msg is sending)=> 2. reload the element cell status: State.Updated(msg send sucessced)
Pls find the attachment:screen recording file and the source code:
void HandleOnPMChanged(object sender, PMChangedEventArgs e)
{
if (e.PMSID != this.mPMSession.PMSID)
return;
this.BeginInvokeOnMainThread(() =>
{
if (e. State == State.Insert) //step1
{
element = this.GetNewPMElement(itemData);
pmSection.Add(element);
this.ScrollToBottomRow();
} else if (e.State == State.Update && e.PM != null) //step2
{
var element = FindElement(e.PM.Guid.GetHashCode());
if (element != null)
{
var indexPaths = new NSIndexPath [] { element.IndexPath };
this.TableView.ReloadRows(indexPaths, UITableViewRowAnimation.None); //this line will flicker
//remark: this.ScrollToBottomRow();
}
}
else if (e. State == State.Insert)
{
element = this.GetNewPMElement(itemData);
pmSection.Add(element);
this.ScrollToBottomRow(); //step1
}
});
}
public void ScrollToBottomRow()
{
try
{
if (pmSection.Count < 1)
return;
NSIndexPath ndxPath = pmSection[pmSection.Count - 1].IndexPath;
if (ndxPath != null)
this.TableView.ScrollToRow(ndxPath, UITableViewScrollPosition.Bottom, false); //Bottom, false);
}
catch (Exception ex)
{
Util.ReportMsg("PMDVC ScrollToBottomRow Exception:", ex.Message);
}
}
issue has been fixed.
EstimatedHeight return bigger than actual value.

Xamarin.iOS add rows dynamically to indexed uitableview

have an UITableView with index and when I scroll to bottom load more cell from server,the problem is that when I try to add more cells shows "assertion failed" error, this is how I was trying to add cells.
Note: Table view source its modified and without adding more cell works fine
List<ICContactTableItem>items,itemsAdd;
Dictionary<string, List<ICContactTableItem>> indexedTableItems;//parameter in tableviewsource
string[] keys;//parameter in tableviewsource
UITableView tableContact;
public void addContacts(){
InvokeOnMainThread (delegate {
try{
tableContact.BeginUpdates();
int count=items.Count;
List<NSIndexPath> tmpArray = new List<NSIndexPath>();
foreach(ICContactTableItem item in itemsAdd){
if(item.firstName.Length>0){
if (indexedTableItems.ContainsKey (item.firstName[0].ToString ())) {
indexedTableItems [item.firstName [0].ToString ()].Add (item);
NSIndexPath tmpIndexPath = NSIndexPath.FromRowSection(
indexedTableItems[item.firstName [0].ToString ()].Count-1,
getIndex(item.firstName [0].ToString ()));
tmpArray.Add(tmpIndexPath);
} else {
indexedTableItems.Add (item.firstName [0].ToString (), new List<ICContactTableItem> () { item });
tableContact.InsertSections(NSIndexSet.FromIndex(getIndex(item.firstName [0].ToString ()))
,UITableViewRowAnimation.None);
NSIndexPath tmpIndexPath = NSIndexPath.FromRowSection(
indexedTableItems[item.firstName [0].ToString ()].Count-1,
getIndex(item.firstName [0].ToString ()));
tmpArray.Add(tmpIndexPath);
}
}else{
if (indexedTableItems.ContainsKey (" ")) {
indexedTableItems [" "].Add (item);
NSIndexPath tmpIndexPath = NSIndexPath.FromRowSection(
indexedTableItems[" "].Count,
getIndex(" "));
tmpArray.Add(tmpIndexPath);
} else {
indexedTableItems.Add (" ", new List<ICContactTableItem> () { item });
indexedTableItems [" "].Add (item);
tableContact.InsertSections(NSIndexSet.FromIndex(getIndex(" "))
,UITableViewRowAnimation.None);
NSIndexPath tmpIndexPath = NSIndexPath.FromRowSection(
indexedTableItems[" "].Count,
getIndex(" "));
tmpArray.Add(tmpIndexPath);
}
}
}
keys=indexedTableItems.Keys.ToArray();
items.AddRange(itemsAdd);
tableContact.InsertRows(tmpArray.ToArray(),UITableViewRowAnimation.None);
tableContact.EndUpdates();
tableContact.ReloadData();
tableContact.SetNeedsLayout();
}catch(Exception e){
Console.WriteLine(e.StackTrace);
}
ICLayoutMgr.Get().SetBusy(false);
});
}
int getIndex (string str)
{
string[] keysOrdered = indexedTableItems.Keys.ToArray();
int res = 0;
for (int i = 0; i < keysOrdered.Length; i++) {
if (keysOrdered [i] == str) {
res = i;
}
}
return res;
}
Before you make any calls to tell the table to add or remove any rows, you must update your data source with by adding or removing data. The table will check how many sections and rows there are before and after your add/remove rows. The number of sections and rows after the change must properly reflect how much data you add/remove with how many rows you add/remove.
And of course you must implement the NumberOfRowsInSection method.

UIACollectionView cells vs visibleCells

I'm trying to write a test script using automation in xcode 4.5.
I have a UICollectionView and I want to click on some cell not currently visible.
Per documentation, I should expect cells to return all cells in the collection view, and visibleCells to return only the currently visible ones.
Instead what I'm seeing is that cells returns only the currently visible cells, and calling visibleCells stops the script on 'undefined' is not a function (evaluating 'collection.visibleCells()')
var target = UIATarget.localTarget();
var collection = target.frontMostApp().mainWindow().collectionViews()[0];
UIALogger.logMessage("Looking in collection: " + collection);
UIALogger.logMessage("Cells: " + collection.cells() + " length " + collection.cells().length);
UIALogger.logMessage("Visible cells: " + collection.visibleCells());
The code above returns the right UICollectionView, second log line prints:
Cells: [object UIAElementArray] length 12
although I have 100 items in the collection view, and third log line crashes script.
Is this a documentation/UIACollectionView bug?
Any ideas how can I tell the automation to scroll until it sees a cell with the name "My cell"?
I've tried using someCell.scrollToVisible, but I need to have the cell to do that, and I don't since I can't get it from cells.
EDIT:
As suggested by Jonathan I've implemented a scroll-till-found function.
it's a bit implementation specific, so you'll probably need to tweak isCellWithName.
I'm also looking to add a break in case we didn't find the needed cell in the while loop, if anyone has ideas, feel free to edit this.
function isCellWithName(cell, name) {
return (cell.staticTexts()[0].name() == name);
}
function getCellWithName(array, name) {
for (var i = 0; i < array.length; i++) {
if (isCellWithName(array[i], name)) {
return array[i];
}
}
return false;
}
function scrollToName(collection, name) {
var found = getCellWithName(collection.cells(), name);
while (found === false) {
collection.dragInsideWithOptions({startOffset:{x:0.2, y:0.99}, endOffset:{x:0.2, y:0},duration:1.0});
found = getCellWithName(collection.cells(), name);
}
return found;
}
The documentation is apparently incorrect. There is no visibleCells() method on UIACollectionView. I figured this out by looping over all the collection view elements properties and printing out their names:
var target = UIATarget.localTarget();
var window = target.frontMostApp().mainWindow();
var collectionView = window.collectionViews()[0];
for (var i in collectionView) {
UIALogger.logMessage(i);
}
Table view elements, on the other hand, do list all the cells with the cells() method. I'm wondering if they choose not to do this because of the much more complicated nature of collection views. It could be very expensive to actually fetch all the collection view cells, build their representations and frames, and return the elements if you had a lot of them. That's what UI Automation does when it asks table views for all the cells. They have to all be instantiated and calculated in order to get the element representations.
But, to answer your larger question, how to scroll to a specific cell. Can you consistently scroll it into view with a swipe gesture? It's not the most convenient way to do it and we're "spoiled" by the ability to scroll to non-visible elements with table views. But from a user behavior testing standpoint, swiping a certain amount is what the user would have to do anyway. Could the test be structured to reflect this and would it address your need?
I couldn't get the the #marmor dragInsideWithOptions() bit to work in a generic fashion. Instead, I'm using the collectionView's value() function to get an index of the current page vs. last page, as in "page 3 of 11". Then I use collectionView's scrollUp() and scrollDown() methods to walk through the pages until we find what we're after. I wrote an extension for TuneUp's uiautomation-ext.js that seem to do the trick, and more:
function animationDelay() {
UIATarget.localTarget().delay(.2);
}
extend(UIACollectionView.prototype, {
/**
* Apple's bug in UIACollectionView.cells() -- only returns *visible* cells
*/
pageCount: function() {
var pageStatus = this.value();
var words = pageStatus.split(" ");
var lastPage = words[3];
return lastPage;
},
currentPage: function() {
var pageStatus = this.value();
var words = pageStatus.split(" ");
var currentPage = words[1];
//var lastPage = words[3];
return currentPage;
},
scrollToTop: function() {
var current = this.currentPage();
while (current != 1) {
this.scrollUp();
animationDelay();
current = this.currentPage();
}
},
scrollToBottom: function() {
var current = this.currentPage();
var lastPage = this.pageCount();
while (current != lastPage) {
this.scrollDown();
animationDelay();
current = this.currentPage();
}
},
cellCount: function() {
this.scrollToTop();
var current = this.currentPage();
var lastPage = this.pageCount();
var cellCount = this.cells().length;
while (current != lastPage) {
this.scrollDown();
animationDelay();
current = this.currentPage();
cellCount += this.cells().length;
}
return cellCount;
},
currentPageCellNamed: function(name) {
var array = this.cells();
for (var i = 0; i < array.length; i++) {
var cell = array[i];
if (cell.name() == name) {
return cell;
}
}
return false;
},
cellNamed: function(name) {
// for performance, look on the current page first
var foundCell = this.currentPageCellNamed(name);
if (foundCell != false) {
return foundCell;
}
if (this.currentPage() != 1) {
// scroll up and check out the first page before we iterate
this.scrollToTop();
foundCell = this.currentPageCellNamed(name);
if (foundCell != false) {
return foundCell;
}
}
var current = this.currentPage();
var lastPage = this.pageCount();
while (current != lastPage) {
this.scrollDown();
animationDelay();
current = this.currentPage();
foundCell = this.currentPageCellNamed(name);
if (foundCell != false) {
return foundCell;
}
}
return false;
},
/**
* Asserts that this collection view has a cell with the name (accessibility identifier)
* matching the given +name+ argument.
*/
assertCellNamed: function(name) {
assertNotNull(this.cellNamed(name), "No collection cell found named '" + name + "'");
}
});

Resources