Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-18680] iOS - Incorrect Caching of ListItems containing Textfields

GitHub Issuen/a
TypeBug
PriorityCritical
StatusClosed
ResolutionFixed
Resolution Date2015-03-17T21:21:11.000+0000
Affected Version/sRelease 3.5.1
Fix Version/sRelease 4.1.0
ComponentsiOS
LabelsListItem, ListView, index
ReporterJustin Toth
AssigneeVishal Duggal
Created2015-03-04T23:10:06.000+0000
Updated2015-06-15T08:31:41.000+0000

Description

One of our Titanium apps uses ListViews extensively, including for building forms, where each ListItem contains a form control (TextField, Picker, etc...) If the form has more ListItems than the size of the screen, as you scroll down it causes issues where the values from certain TextFields are copied into other ListItems' TextFields. This is clearly connected to the ListView's row caching, it's expected that it would cache the list rows so that you only have as many as are on the screen in memory, however it's clearly not setting the values correctly for each row as scrolling occurs. I've boiled it down to a simple reproduction case below, simply input values into the first few rows (e.g. "test 1", "test 2", "test 3") then scroll down and you'll see those 3 values populated into other rows.
var win = Ti.UI.createWindow({ backgroundColor: 'white' });

var defaultTemplate = {
    childTemplates: [
        {
            type: 'Ti.UI.Label',
            bindId: 'label',
            properties: {
                top: '5dp', left: '5dp'
            }
        },
        {
            type: 'Ti.UI.TextField',
            bindId: 'value',
            properties: {
                top: '30dp', left: '5dp', right: '5dp', bottom: '5dp',
                backgroundColor: 'gray', color: 'black', height: '40dp'
            }
        }
    ]
};

var listView = Ti.UI.createListView({
    top: '20dp',
    templates: { 'default': defaultTemplate },
    defaultItemTemplate: 'default',
    rowHeight: '70dp',
    backgroundColor: '#eff3fa',
    showVerticalScrollIndicator: true,
    separatorColor: '#e3ece7', separatorInsets: { left: 0, right: 0 }
});

var items = [];
for (var k=0;k<40;k++) {
    var label = 'Item ' + (k + 1);
    items.push({
        label: { text: label },
        value: { text: '' }
    });
}

var sections = [
    Ti.UI.createListSection({
        headerTitle: 'Only Section',
        items: items
    })
];

listView.setSections(sections);

win.add(listView);
win.open();

Comments

  1. Pedro Enrique 2015-03-11

    Hi Justin, What you are seeing is the default behavior. To update the content of the textfield, you need to update the data. There's documentation here: http://docs.appcelerator.com/titanium/latest/#!/guide/ListViews For example, I changed the template to make it more readable, value is now called textField. I also added an event to the textfield.
       var defaultTemplate = {
           childTemplates: [
               {
                   type: 'Ti.UI.Label',
                   bindId: 'label',
                   properties: {
                       top: '5dp', left: '5dp'
                   }
               },
               {
                   type: 'Ti.UI.TextField',
                   bindId: 'textField',
                   properties: {
                       top: '30dp', left: '5dp', right: '5dp', bottom: '5dp',
                       backgroundColor: 'gray', color: 'black', height: '40dp'
                   },
                   events: {
                   	blur: onTextfieldBlur
                   }
               }
           ]
       };
       
    Then declare the event function to be called, and there update the listview data:
       function onTextfieldBlur(e) {
           var item = e.section.getItemAt(e.itemIndex);
           item.textField.value = e.value;
           e.section.updateItemAt(e.itemIndex, item);  
       }
       
    Resolving, this is not a bug
  2. Justin Toth 2015-03-11

    It seems like you didn't test your code, as it still has the same issue as in my original sample... The issue is that rows are cached and reused, simply updating the textfield's value manually on blur does nothing to address that issue. It also leaves a nasty side effect where if you click from one textfield to another, the second one won't gain focus, as the section.updateItemAt will recreate the section and focus is lost. Please reopen the ticket.
       var win = Ti.UI.createWindow({ backgroundColor: 'white' });
       
       var defaultTemplate = {
           childTemplates: [
               {
                   type: 'Ti.UI.Label',
                   bindId: 'label',
                   properties: {
                       top: '5dp', left: '5dp'
                   }
               },
               {
                   type: 'Ti.UI.TextField',
                   bindId: 'textField',
                   properties: {
                       top: '30dp', left: '5dp', right: '5dp', bottom: '5dp',
                       backgroundColor: 'gray', color: 'black', height: '40dp'
                   },
                   events: {
                       blur: onTextfieldBlur
                   }
               }
           ]
       };
       
       var listView = Ti.UI.createListView({
           top: '20dp',
           templates: { 'default': defaultTemplate },
           defaultItemTemplate: 'default',
           rowHeight: '70dp',
           backgroundColor: '#eff3fa',
           showVerticalScrollIndicator: true,
           separatorColor: '#e3ece7', separatorInsets: { left: 0, right: 0 }
       });
       
       var items = [];
       for (var k=0;k<40;k++) {
           var label = 'Item ' + (k + 1);
           items.push({
               label: { text: label },
               textField: { text: '' }
           });
       }
       
       var sections = [
           Ti.UI.createListSection({
               headerTitle: 'Only Section',
               items: items
           })
       ];
       
       listView.setSections(sections);
       
       win.add(listView);
       win.open();
       
       function onTextfieldBlur(e) {
           var item = e.section.getItemAt(e.itemIndex);
           item.textField.value = e.value;
           e.section.updateItemAt(e.itemIndex, item);
       }
       
  3. Vishal Duggal 2015-03-12

    A couple of issues with the sample code. One the property to be used with the textField bindId is value not text. Second the reason the keyboard disappears on selecting a second text field is because the updateItemAt does not specify an animated parameter which implicitly sets animated to false. In this scenario we have to reload the whole table. Either ways there is an issue with ListView's reuse of Proxies and underlying views. Will setup a PR to fix this. Test case
       var win = Ti.UI.createWindow({ backgroundColor: 'white' });
       
        
       
       var defaultTemplate = {
       
           childTemplates: [
       
               {
       
                   type: 'Ti.UI.Label',
       
                   bindId: 'label',
       
                   properties: {
       
                       top: '5dp', left: '5dp'
       
                   }
       
               },
       
               {
       
                   type: 'Ti.UI.TextField',
       
                   bindId: 'textField',
       
                   properties: {
       
                       top: '30dp', left: '5dp', right: '5dp', bottom: '5dp',
       
                       backgroundColor: 'gray', color: 'black', height: '40dp'
       
                   },
       
                   events: {
       
                       blur: onTextfieldBlur,
       
                   }
       
               }
       
           ]
       
       };
       
        
       
       var listView = Ti.UI.createListView({
       
           top: '20dp',
       
           templates: { 'default': defaultTemplate },
       
           defaultItemTemplate: 'default',
       
           rowHeight: '70dp',
       
           backgroundColor: '#eff3fa',
       
           showVerticalScrollIndicator: true,
       
           separatorColor: '#e3ece7', separatorInsets: { left: 0, right: 0 }
       
       });
       
        
       
       var items = [];
       
       for (var k=0;k<40;k++) {
       
           var label = 'Item ' + (k + 1);
       
           items.push({
       
               label: { text: label },
       
               textField: { value: '' }
       
           });
       
       }
       
        
       
       var sections = [
       
           Ti.UI.createListSection({
       
               headerTitle: 'Only Section',
       
               items: items
       
           })
       
       ];
       
        
       
       listView.setSections(sections);
       
        
       
       win.add(listView);
       
       win.open();
       
        
       
       function onTextfieldBlur(e) {
          var item = e.section.getItemAt(e.itemIndex);
       
           item.textField.value = e.value;
       
           e.section.updateItemAt(e.itemIndex, item, {animated:true});
       
       }
       
  4. Vishal Duggal 2015-03-12

    Pull pending master - https://github.com/appcelerator/titanium_mobile/pull/6718
  5. Justin Toth 2015-03-29

    I've tested and confirmed that the row caching issue is resolved when using master and the two changes that Vishal suggested (value instead of text and animated: true). Thanks Pedro and Vishal for looking into the issue!
  6. Satyam Sekhri 2015-06-15

    The issue is fixed. No caching of list items. Verified using the code mentioned by Vishal. Verified on: Mac OS 10.10.1 Ti SDK: 4.1.0.v20150614181235 Appc Studio: 4.1.0.201506120823 Appc CLI Registry: 4.0.2 Appc CLI NPM: 4.0.1 Device: iPhone 6plus (v8.3), iPhone 5S simulator (v7.1)

JSON Source