Titanium JIRA Archive
Alloy (ALOY)

[ALOY-1248] iOS: ListView blank during redraw

GitHub Issuen/a
TypeBug
PriorityHigh
StatusReopened
ResolutionUnresolved
Affected Version/sn/a
Fix Version/sn/a
Componentsn/a
Labels3.4.0, TCSupport, iphone, listView
ReporterPeter Tippett
AssigneeFeon Sua Xin Miao
Created2014-10-19T21:48:54.000+0000
Updated2015-07-14T20:58:55.000+0000

Description

Problem Description

After a large model update, find that the listview display only shows what was on displayed, and the rest is blank. The scroll bar shows that I have new records, but is displaying blank lines. If I load a new screen with the same ListView code or flip to another screen and come back, the data display is correct. If I quit and come back in, it is clean and all data is displayed. Is clearly a screen redraw problem I'm having. Have even done a simple code test version, within my App, and still have the same problem. Was working perfectly with 3.3. This same problem is happening in other screens with ListView as well of my App.

STEPS TO TEST:

- Create a simple alloy project. - Update project with test code - Link: https://github.com/PeterTippett/ListViewBugTest - Run on iOS device/simulator -List view doesn’t refresh/load data (screenshot 1) but update it’s count value. If we run the project again, then data loads as expected (screenshot 2). But when we refresh/load data again, then a visible section of screen only shows data, others can’t (screenshot 3&4). Screenshot Link: http://oi57.tinypic.com/ve17rp.jpg

EXPECTED RESULT:

It’s not working as expected.

Extra info

An error appears in Xcode - TiUIListViewProxy.m (marked in x) Thread 1: EXC_BAD_ACCESS(code=1, address=Oxe) TiUIListViewProxy.m Thread 1: EXC_BAD_ACCESS(code=1, address=Oxe) - marked with xxxx
-(void)processUpdateActions
{
    UITableView *tableView = self.listView.tableView;
    BOOL removeHead = NO;
    while (YES) {
        void (^block)(UITableView *) = nil;
        pthread_mutex_lock(&_operationQueueMutex);
        if (removeHead) {
            [_operationQueue removeObjectAtIndex:0];
        }
        if ([_operationQueue count] > 0) {
            block = [_operationQueue objectAtIndex:0];
            removeHead = YES;
        }
        pthread_mutex_unlock(&_operationQueueMutex);
        if (block != nil) {
            [tableView beginUpdates];
            block(tableView);
xxxx            [tableView endUpdates];
            Block_release(block);
        } else {
            [self.listView updateIndicesForVisibleRows];
            [self contentsWillChange];
            return;
        }
    }
}
{quote} Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-3318.0.1/UITableView.m:1582 {quote} Have built a version that repeats this and done a clean rebuilt of Titanium as well downloading all the latest versions. https://github.com/PeterTippett/ListViewBugTest - sample code that fails Have attached three screen shots, one is before an update. http://developer.appcelerator.com/question/178522/listview-redraw-after-a-large-model-update#comment-220169 {quote} [INFO] : Found Titanium plugin id=ti.alloy version=1.0 [INFO] : Deploy type: development [INFO] : Building for target: simulator [INFO] : Building using iOS SDK: 8.0 [INFO] : Building for iOS Simulator: iPhone 6 [INFO] : Building for device family: universal [INFO] : Minimum iOS version: 6.0 {quote}

Attachments

FileDateSize
iOS Simulator Screen Shot 19 Oct 2014 4.09.13 pm.png2014-10-19T21:48:54.000+0000167300
iOS Simulator Screen Shot 19 Oct 2014 4.09.43 pm.png2014-10-19T21:48:54.000+0000130179
iOS Simulator Screen Shot 19 Oct 2014 4.09.50 pm.png2014-10-19T21:48:54.000+0000116916

Comments

  1. Hetal 2014-11-11

    We are seeing the same issue where the entire tableview just doesn't load data. The view is blank. Its only happening on iOS 8.0 Ti 3.4.0. Works fine on iOS 7.1 Ti 3.3.0. Is there a date around when this issue will be resolved?
  2. Christian Brousseau 2014-11-21

    It is important to note that the bug is also present on iOS 7.1 using Titanium 3.4.0.
  3. Pedro Enrique 2015-02-09

    To reproduce

    1. Download the code of the app found here: https://github.com/appcelerator/alloy/tree/master/test/apps/models/binding_listview 2. Go to index.js inside the controllers folder and create a for-loop at line 32 encapsulating the logic, like this
       for(var i = 0; i < 10; i++) {
           // create a model for the listitem
           var model = Alloy.createModel('info', e.modelObj);
           // add model to collection which will in turn update the UI
           info.add(model);
       }
       
    3. Run the app.

    The problem

    The problem is that every time the data model is added to the collection, internally it calls the "setItems" method of the listview section, thus calling it many times (10 in this case) in a row causing an internal race condition.

    Simpler test case:

    To see the problem, create a new classic app and paste this code in app.js
       var win = Ti.UI.createWindow({
       	backgroundColor: 'white'
       });
       var section1 = Ti.UI.createListSection({
       	headerTitle: 'This Section'
       });
       var listView = Ti.UI.createListView({
       	sections: [section1],
       	top: 40
       });
       var btn = Ti.UI.createButton({
       	top: 0,
       	height: 40,
       	title: 'setItems'
       });
       
       win.add(btn);
       win.add(listView);
       win.open();
       
       btn.addEventListener('click', function(){
       	for(var i = 0; i < 10; i++) {
       		section1.setItems([
       			{
       				properties: { title: 'Title 1'}
       			},
       			{
       				properties: { title: 'Title 2'}
       			}
       		]);
       	}
       });
       
  4. Pedro Enrique 2015-02-09

    Resolving the issue as "Won't fix". Please reopen if needed. The way to solve the issue is to create an array of models and adding that to the collection, instead of adding the one by one. For example, using the code in the comment above:
       // THIS IS BAD (inserting models one by one)
       for(var i = 0; i < 10; i++) {
           // create a model for the listitem
           var model = Alloy.createModel('info', e.modelObj);
           // add model to collection which will in turn update the UI
           info.add(model);
       }
       
    Versus this:
       // THIS IS GOOD (inserting all the models at the same time)
       var data = [];
       for(var i = 0; i < 10; i++) {
           // create a model for the listitem
           var model = Alloy.createModel('info', e.modelObj);
           // add the model to a javascript array for later use
           data.push(model);
       }
       // add the array of models to collection which will in turn update the UI
       info.add(data);
       
  5. Pedro Enrique 2015-02-09

    Disregard my last comment. Adding an array to the collection still calls the "setItems" method per model in that array. This seems to be an Backbone issue where it calls "trigger" on every model that is added to a collection.
  6. Tim Poulsen 2015-02-10

    I think the solution is to to pass \{silent: true} to each call to Collection.add() then trigger the update at the end.
       for(var i = 0; i < 10; i++) {
           // create a model for the listitem
           var model = Alloy.createModel('info', e.modelObj);
           // add model to collection which will in turn update the UI
           info.add(model, {silent: true});
       }
       info.trigger('add');
       
  7. Ingo Muschenetz 2015-02-13

    [~pec1985] to move to ALOY.
  8. Tim Poulsen 2015-02-13

    I don't believe this to be an Alloy issue, though it may be true that it's easier to reproduce the condition in Alloy. If the code I provided above works around the problem, it indicates that many updates to the listview's data in quick succession is the cause of the blank items. Those same conditions could also happen with Classic code. I'll also point out that "_This seems to be an Backbone issue where it calls "trigger" on every model that is added to a collection._" is a characteristic of Backbone, not Alloy. Perhaps this is a documentation issue ... that you should suppress the add event when adding items to a view-bound collection.
  9. Pedro Enrique 2015-02-17

    Adding "silence: true" and "trigger('add');" does help with performance, but the issue of calling listView's setItem is still there. This is the generated Alloy code:
       function Controller() {
           function __alloyId19(e) {
               if (e && e.fromAdapter) return;
               var opts = __alloyId19.opts || {};
               var models = filterInvites(__alloyId18);
               var len = models.length;
               var __alloyId14 = [];
               for (var i = 0; len > i; i++) {
                   var __alloyId15 = models[i];
                   __alloyId15.__transform = doTransform(__alloyId15);
                   var __alloyId17 = {
                       // More code here
                   };
                   __alloyId14.push(__alloyId17);
               }
               opts.animation ? $.__views.invites.setItems(__alloyId14, opts.animation) : $.__views.invites.setItems(__alloyId14);
           }
       
    The function "__alloyId19" get's called by the backbone event emitter for every model in the collection array, thus calling "setItems" more times than needed and replacing the list view data. In this case it should only be called once. It is definitely better practice to call "trigger('save')" after adding all the models to the collection. If the developer then wants to add more items, we should compare the new array of items with the previous one and either call "insertItemsAt" or "appendItems"
  10. Tim Poulsen 2015-02-19

    [~penrique] I can't reproduce this. Furthermore, in my tests, the generated function (e.g. __alloyId19") is getting called just once. Tested with Alloy 1.5.1 and 1.7.0-dev, SDK 3.5.0.GA and 4.0.0. Steps: 1. Create a new Alloy project and copy/import the the binding/listview sample app as you describe above. 2. Update index.js like this:
        function doButtonClick(e) {
        	if (_.isEmpty(e.modelObj)) {
        		var db = Ti.Database.open('_alloy_');
        		db.execute('DELETE FROM info;');
        		db.close();
        		info.fetch();
        	} else {
        		// create a model for the listitem
        		for(var i = 0; i < 100; i++) {
        		    var model = Alloy.createModel('info', e.modelObj);
        		    info.add(model, {silent: true});
        		}
        		info.trigger('add');
        	}
        }
        
    3. Build for the simulator. The app works as expected; I can add as many rows as I want and I don't see the blank items. 4. Create a new Classic app. 5. Copy the contents of the Alloy app's Resources folder to the Classic app's Resources folder (so you can fiddle with generated code) 6. Update the classic app's iphone/alloy/controllers/index.js to add logging statements as follows:
        function __alloyId27(e) {
            if (e && e.fromAdapter) return;
            var opts = __alloyId27.opts || {};
            var models = __alloyId26.models;
            var len = models.length;
            var __alloyId22 = [];
            for (var i = 0; len > i; i++) {
                // ***** ADD LOG OUTPUT HERE *****
                console.log('Looping: ' + i);
                var __alloyId23 = models[i];
                __alloyId23.__transform = doTransform(__alloyId23);
                var __alloyId25 = {
                    properties: {
                        backgroundColor: "transparent"
                    },
                    template: "undefined" != typeof __alloyId23.__transform["template"] ? __alloyId23.__transform["template"] : __alloyId23.get("template"),
                    title: {
                        text: "undefined" != typeof __alloyId23.__transform["title"] ? __alloyId23.__transform["title"] : __alloyId23.get("title")
                    },
                    subtitle: {
                        text: "undefined" != typeof __alloyId23.__transform["subtitle"] ? __alloyId23.__transform["subtitle"] : __alloyId23.get("subtitle")
                    },
                    image: {
                        image: "undefined" != typeof __alloyId23.__transform["image"] ? __alloyId23.__transform["image"] : __alloyId23.get("image")
                    }
                };
                __alloyId22.push(__alloyId25);
            }
            // ***** ADD LOG OUTPUT HERE *****
            console.log('Calling section.setItems()')
            opts.animation ? $.__views.section.setItems(__alloyId22, opts.animation) : $.__views.section.setItems(__alloyId22);
        }
        
    7. Build the classic app for the simulator. Click a button. You'll see 100 "Looping" log messages followed by one "Calling section.setItems()" log message.
  11. Feon Sua Xin Miao 2015-07-14

    I can't reproduce the issue reported. Environment: Ti SDK 4.1.0.GA, CLI: 4.1.0, iOS 8.3 Tried the test code at https://github.com/PeterTippett/ListViewBugTest, the server that hosts the data seems down, modified the function
        function loadinvites(){
        	for (var i = 0; i < 50; i++) {
        		createinvite({
        			id: i,
        			company: 'company',
        			modified: '0',
        			name: 'id -' + i
        		});
        	}
        }
        

JSON Source