iOS Data Grid Crash When Reloading Data


#1

Hi there. I’m encountering a weird issue with the data grid control for iOS.

 

I have a sample project that loads 91 records (from a self-contained array). My application view contains the Shinobi Data Grid control and a UISearchBar. Very simple - and as minimal as I could get. If I search for “ASDF” (a value that will return 2 results from the sample data source) when the grid loads, it functions as expected and the grid is reloaded to contain only two rows. However…If I scroll all the way down to the bottom of the grid and then search for “ASDF”, it results in a fatal crash when I call the reload method on my data grid view. 

 

Any idea why that may be? Is the issue that the main view contains two subviews - one for the UISearchBar, and one for the data grid? For the life of me I can’t understand why it won’t allow me to scroll all the way down and search against a known result. Could it be that when scrolling to the tail end of the data grid that it’s displaying the last few items in the data array (indices 78 - 90 in my example on the iPhone 6 Plus) - resulting in a fatal crash because the grid reloads the data but is trying to find it/display cells from those indices which are no longer valid?

 

Shouldn’t a reload of the grid result in a fresh build of the grid and bring the user back to the top?

 

 


#2

Greetings Program!

I was able to replicate it in the GettingStarted app.

Wg


#3

Yeah, I’m definitely filtering the data source appropriately. First, I have a UISearchBarDelegate method handling when the user presses the search button:

- (void)searchBarSearchButtonClicked:(UISearchBar*)searchBar
{
    NSString *searchTerm = [searchBar text];
    [searchBar resignFirstResponder];
    
    // Search against our array
    NSLog(@"...searching for [%@]", searchTerm);
    NSArray *results = [_sortedData filteredArrayUsingPredicate:[InventoryItem generatePredicateToSearchStringAgainstAllStringProperties:searchTerm]];
    
    if(results.count > 0){
        _sortedData = [results mutableCopy];
    } else {
    }
    
    // Clear our search bar and reload the grid
    searchBar.text = nil;
    [self reloadDataGrid];
    
}

The reloadDataGrid method for my view controller is:

- (void)reloadDataGrid{
    NSLog(@"Preparing to reload data grid");
    self.navigationItem.title = [NSString stringWithFormat:@"Inventory Results (%lu)", (unsigned long)_sortedData.count];
    NSLog(@"Inventory Results (%lu)", (unsigned long)_sortedData.count);
    
    [_shinobiDataGrid reload];
    
}

In the prepareCellForDisplay method (based on the other examples) I have it go through and find the appropriate item from the data source (a simple array in this case):

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid prepareCellForDisplay:(SDataGridCell *)cell{
    // Both columns use a SDataGridTextCell, so we are safe to perform this cast
    SDataGridTextCell *textCell = (SDataGridTextCell *)cell;
    NSString *columnTitle = cell.coordinate.column.title; // Column title for this cell
    NSUInteger index = [_columns indexOfObject:columnTitle]; // Get the zero-based index of this column title

    NSLog(@"Preparing cell for display at row %lu [col %@]", cell.coordinate.row.rowIndex, cell.coordinate.column.title);

    // Locate the item that to be rendered for this row
    if (kUseExistingProcessing) {
        NSString *item = _sortedData[cell.coordinate.row.rowIndex]; // Extract the object from our row value
        // Parse the long tab separated string into an array we can work with
        NSArray *itemDetail = [[NSArray alloc] initWithArray:[item componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\t"]]];
        
        // Set the text value of this cell
        textCell.textField.text = itemDetail[index];
    } else {
        NSString *textValue;
        
        // Extract the object using our row values
        InventoryItem *item = _sortedData[cell.coordinate.row.rowIndex];
        
        // What value should we display?
        switch (index) {
            case 0:
                textValue = item.locationName;
                break;
            
            case 1:
                textValue = item.locationStatus;
                break;
                
            case 2:
                textValue = item.partCode;
                break;
                
            case 3:
                textValue = item.quantity;
                break;
                
            case 4:
                textValue = item.manufacturer;
                break;
                
            case 5:
                textValue = item.manufacturingPartNumber;
                break;
            
            case 6:
                textValue = item.partDescription;
                break;
            
            default:
                NSLog(@"Uh oh. Unexpected row value passed in from our datagrid.");
                textValue = @"";
                break;
        }
        
        // Set the text value of this cell
        textCell.textField.text = textValue;
        
    }
    
}

I did some logging and confirmed that this method correctly draws the expected number of cells. In the case of my example (which filteres down an array of 91 objects to one that contains two objects), logging correctly shows that this data is being created for the grid as expected:

2014-12-02 12:47:56.254 DataGridExploration[1809:394152] Preparing cell for display at row 89 [col Part Code]
2014-12-02 12:47:56.255 DataGridExploration[1809:394152] Preparing cell for display at row 90 [col Part Code]
2014-12-02 12:47:58.251 DataGridExploration[1809:394152] ...searching for [ASDF]
2014-12-02 12:47:58.260 DataGridExploration[1809:394152] Preparing to reload data grid
2014-12-02 12:47:58.262 DataGridExploration[1809:394152] Inventory Results (2)
2014-12-02 12:47:58.268 DataGridExploration[1809:394152] Determining there are 2 number of rows in data grid section 0
2014-12-02 12:47:58.275 DataGridExploration[1809:394152] Preparing cell for display at row 1 [col Location Name]
2014-12-02 12:47:58.277 DataGridExploration[1809:394152] Preparing cell for display at row 1 [col Location Status]
2014-12-02 12:47:58.279 DataGridExploration[1809:394152] Preparing cell for display at row 1 [col Part Code]
2014-12-02 12:47:58.291 DataGridExploration[1809:394152] Preparing cell for display at row 0 [col Location Name]
2014-12-02 12:47:58.293 DataGridExploration[1809:394152] Preparing cell for display at row 0 [col Location Status]
2014-12-02 12:47:58.295 DataGridExploration[1809:394152] Preparing cell for display at row 0 [col Part Code]

Here is the fatal crash: https://www.evernote.com/shard/s8/sh/e304233a-1018-41fb-8069-58c92b1d37ac/951116677243b9e63be815a6f302fe62

I’m totally baffled by this. It seems like a fairly straightforward example.

DEBUG NOTE - I’ve tried this with both shinobigrids-standard-2.7.2-5-Purchased.dmg and shinobigrids-standard-2.7.2.hotfix1-2.dmg


#4

@ShinobiSquad:

As with rbrennan, if you scroll right down to the bottom of the grid and try to filter, on reload the following exception is thrown.

The numberOfRowsInSection returns the correct result but breaks prior to prepareCellForDisplay. The exception sometimes is thrown on the second filter (two characters).

Here’s the error I get:

2014-12-02 13:52:16.510 GettingStarted[24291:607] *** Terminating app due to uncaught exception 'Row Search', reason: 'Something went wrong when finding next row'
*** First throw call stack:
(
	0 CoreFoundation 0x01d2d1e4 __exceptionPreprocess + 180
	1 libobjc.A.dylib 0x019ee8e5 objc_exception_throw + 44
	2 GettingStarted 0x000e43ea -[ShinobiGrid findRowAfterRow:] + 362
	3 GettingStarted 0x00104421 -[ShinobiGrid bottomCellToBeAddedForCol:] + 129
	4 GettingStarted 0x00104876 -[ShinobiGrid addNewBottomRowToGrid] + 390
	5 GettingStarted 0x001054c8 -[ShinobiGrid removeRowsFromTopAddToBottomIfNeeded] + 1256
	6 GettingStarted 0x0010294b -[ShinobiGrid tidyRowAndColEdges] + 187
	7 GettingStarted 0x000ece38 -[ShinobiGrid reloadInternal:] + 616
	8 GettingStarted 0x000e3f1e -[ShinobiGrid reload] + 62
	9 GettingStarted 0x0012385a -[ShinobiDataGrid reload] + 202
	10 GettingStarted 0x000cfff1 -[ViewController searchBar:textDidChange:] + 513
	11 UIKit 0x00779c17 -[UISearchBar(UISearchBarStatic) _searchFieldEditingChanged] + 115
	12 libobjc.A.dylib 0x01a0082b -[NSObject performSelector:withObject:] + 70
	13 UIKit 0x0049a3b9 -[UIApplication sendAction:to:from:forEvent:] + 108
	14 UIKit 0x0049a345 -[UIApplication sendAction:toTarget:fromSender:forEvent:] + 61
	15 UIKit 0x0059bbd1 -[UIControl sendAction:to:forEvent:] + 66
	16 UIKit 0x0059bfc6 -[UIControl _sendActionsForEvents:withEvent:] + 577
	17 UIKit 0x00bc129d -[UITextField fieldEditorDidChange:] + 221
	18 UIKit 0x005a1db4 -[UIFieldEditor textInputDidChange:] + 58
	19 UIKit 0x00bcf1b3 -[UITextInputController _sendDelegateChangeNotificationsForText:selection:] + 118
	20 UIKit 0x00bd1090 -[UITextInputController _insertText:fromKeyboard:] + 768
	21 UIKit 0x00bd1954 -[UITextInputController insertText:] + 372
	22 UIKit 0x005a4a64 -[UIFieldEditor insertText:] + 1086
	23 UIKit 0x00bc4ffa -[UITextField insertText:] + 59
	24 UIKit 0x00684760 -[UIKeyboardImpl insertText:] + 87
	25 UIKit 0x00695ea4 -[TIKeyboardOperationInsertText(UIKeyboardImpl) main] + 83
	26 Foundation 0x014afc79 -[__NSOperationInternal _start:] + 671
	27 Foundation 0x0142c9c8 -[NSOperation start] + 83
	28 UIKit 0x00682d4d -[UIKeyboardImpl performOperations:] + 153
	29 UIKit 0x00680f8e -[UIKeyboardImpl continueHandleKeyboardInputWithOperations:] + 75
	30 UIKit 0x00680ddc __73-[UIKeyboardImpl replyHandlerForHandleKeyboardInputWithExecutionContext:]_block_invoke_2 + 44
	31 UIKit 0x00bea978 -[UIKeyboardTaskQueue continueExecutionOnMainThread] + 402
	32 libobjc.A.dylib 0x01a0082b -[NSObject performSelector:withObject:] + 70
	33 Foundation 0x0142fe48 __NSThreadPerformPerform + 285
	34 CoreFoundation 0x01cb677f __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 15
	35 CoreFoundation 0x01cb610b __CFRunLoopDoSources0 + 235
	36 CoreFoundation 0x01cd31ae __CFRunLoopRun + 910
	37 CoreFoundation 0x01cd29d3 CFRunLoopRunSpecific + 467
	38 CoreFoundation 0x01cd27eb CFRunLoopRunInMode + 123
	39 GraphicsServices 0x034ef5ee GSEventRunModal + 192
	40 GraphicsServices 0x034ef42b GSEventRun + 104
	41 UIKit 0x00498f9b UIApplicationMain + 1225
	42 GettingStarted 0x000cecbd main + 141
	43 libdyld.dylib 0x022de6d9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Here’s the sample app with the searchbar:

//
// ViewController.m
// GettingStarted
//
// Copyright (c) 2013 Scott Logic. All rights reserved.
//

#import "ViewController.h"
#import "PersonDataObject.h"
#import "PersonDataSource.h"

@interface ViewController () <SDataGridDataSource, UISearchBarDelegate, UISearchDisplayDelegate>

@property NSArray *data;
@property NSArray *searchResults;
@property ShinobiDataGrid *shinobiDataGrid;
@property UISearchBar *searchBar;

@end

@implementation ViewController

@synthesize shinobiDataGrid;

- (void)viewDidLoad {
    [super viewDidLoad];

    [ShinobiDataGrids setLicenseKey:@"your license key"]; // TODO: add your trial license key here!
    
    // Create a grid - with a 40 pixel padding
    shinobiDataGrid = [[ShinobiDataGrid alloc] initWithFrame:CGRectInset(self.view.bounds, 50,50)];
    
    // Add a name column
    SDataGridColumn* nameColumn = [[SDataGridColumn alloc] initWithTitle:@"Name"];
    nameColumn.width = @484;
    [shinobiDataGrid addColumn:nameColumn];
    
    // Add an age column
    SDataGridColumn* ageColumn = [[SDataGridColumn alloc] initWithTitle:@"Age"];
    ageColumn.width = @200;
    [shinobiDataGrid addColumn:ageColumn];
    
    // Create some data to populate the grid
    self.data = [PersonDataSource generatePeople:90];
    self.searchResults = [self.data copy];
    
    // Set the data-source, this is the class responsible for supplying data to the grid.
    shinobiDataGrid.dataSource = self;
    
    [self.view addSubview:shinobiDataGrid];
    
    self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
    self.searchBar.delegate = self;
    [self.view addSubview:self.searchBar];
}

#pragma mark - ShinobiDataGridDataSource methods

- (NSUInteger)shinobiDataGrid:(ShinobiDataGrid *)grid numberOfRowsInSection:(NSInteger)sectionIndex {
    return self.searchResults.count;
}

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid prepareCellForDisplay:(SDataGridCell *)cell {
    // Both columns use a SDataGridTextCell, so we are safe to perform this cast
    SDataGridTextCell* textCell = (SDataGridTextCell*)cell;
    
    NSLog(@"Row index: %ld", (long)cell.coordinate.row.rowIndex);
    
    // Locate the perspon that is rendered on this row
    PersonDataObject* person = _searchResults[cell.coordinate.row.rowIndex];

    // Determine which column this cell belongs to
    if ([cell.coordinate.column.title isEqualToString:@"Name"]) {
        textCell.textField.text = person.forename;
    }
    else {
        textCell.textField.text = [person.age stringValue];
    }
}

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    if ([searchText isEqualToString:@""])
    {
        self.searchResults = [self.data copy];
    }
    else
    {
        NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:@"forename contains[c] %@", searchText];
        self.searchResults = [self.data filteredArrayUsingPredicate:resultPredicate];
    }
    
    [shinobiDataGrid reload];
}

@end

Wg


#5

Hi Guys,

This is a known issue, but thanks for raising it again with us. You can work around this by scrolling your grid back to the top before you reload your grid.

[_shinobiDataGrid setContentOffset:CGPointMake(0, 0)];

I’ll put a note on our system to update this forum thread when the issue is resolved.

Thanks,
Jan