problem
There's no reliable way to know when you remove all the children from a view if the children are actually gone from the view hierarchy in iOS. The remove() call in JS executes synchronously, but behind the scenes in iOS, the actual removal of the elements from UI appears to happen asynchronously. Take this sample code:
var win = Ti.UI.createWindow({
backgroundColor: '#fff',
fullscreen: false,
exitOnClose: true
});
win.open();
// add a stack of views
for (var i = 0; i < 25; i++) {
win.add(Ti.UI.createView());
}
// timeout is necessary since the add() appears to be async too
setTimeout(function() {
// remove all, showing the child number
while(win.children.length) {
win.remove(win.children[0]);
Ti.API.info('kids left: ' + win.children.length);
}
}, 2000);
The output will vary, but often times you will see that the number of children on the
win
has not been reduced yet, because the child hasn't actually yet been removed from the
win
on the native side. Here's a sample output, notice the staggering of the decrementing around 23 and 10 due to the asynchronous removal:
[INFO] : kids left: 25
[INFO] : kids left: 23
[INFO] : kids left: 23
[INFO] : kids left: 21
[INFO] : kids left: 20
[INFO] : kids left: 19
[INFO] : kids left: 18
[INFO] : kids left: 17
[INFO] : kids left: 16
[INFO] : kids left: 15
[INFO] : kids left: 14
[INFO] : kids left: 13
[INFO] : kids left: 12
[INFO] : kids left: 11
[INFO] : kids left: 10
[INFO] : kids left: 10
[INFO] : kids left: 8
[INFO] : kids left: 7
[INFO] : kids left: 6
[INFO] : kids left: 5
[INFO] : kids left: 4
[INFO] : kids left: 3
[INFO] : kids left: 2
[INFO] : kids left: 1
[INFO] : kids left: 0
It's worth noting that a safer way to remove these children would be in reverse order, as it won't be necessary for the index to change:
for (var i = win.children.length-1; i >= 0; i--) {
win.remove(win.children[i]);
}
This doesn't solve the problem, though, of not knowing when these operations complete. If I was to remove 100 views from
win
and then add a bunch of new ones to it right after, chances are that some of those views will be added before all of the prior views were actually removed, causing some seriously weird runtime behavior. I've seen this in practice implementing data binding in Alloy. When a Collection of data changes, I need to re-render a section of the UI, removing all its views, and replacing them with the updated data. This is where this problem surfaces constantly for me. I'm sure there are many other use cases.
It is also worth nothing that this problem does not occur on Android. Android appears to process these remove and add calls synchronously making for a simple, safe "remove all" operation.
proposed solution
* Make the remove() and add() calls synchronous. I'm going to assume this isn't going to be possible in concern for performance. That said, it is a parity issue with Android as the remove() API behaves differently on the separate platforms.
* Provide some means of knowing when the remove() call is actually complete, perhaps a callback that can fire when the operation is done.
* A new API, a synchronous setChildren() function that can take an array of child elements. Not only would this allow you to safely remove all elements then add more directly after, but it would also increase overall performance by allowing you to make fewer jumps into native-land when establishing a view hierarchy.
All of these are off the top of my head, or based on a conversation with Blain Hamon. I'm open to anything that allows me to empty a view of all children and know when that operation is complete, preferably in a cross-platform manner.
For those reading along, win.children is expensive and would actually starve the asynch remove from working: var mychildren = win.children; for (var i = mychildren.length-1; i >= 0; i--) { win.remove(mychildren[i]); } For testing purposes, use the description's code, but in practice, for performance reasons, caching (on iOS, reading causes a copy) is very helpful. Anyways, other possible solutions:
Implement setChildren as a synchronous command
Implement remove as synchronous and also accepting an array
document and make synchronous removeAllChildren
Personally proposing #1 and #2.#2 would solve my problems. I'm gonna have to wait a hell of a lot longer for #1 since it will create a parity issue and all platforms would need to implement it, but I would love that interface. I would particularly love it if it would allow an entire view hierarchy to be established. And don't establish just establish the view hierarchy from existing proxies, but let me build the initial proxies in the call so all of it can be pushed to the native side in a single call. This is a off-the-top-of-my-head-no-concern-for-details snippet:
Crazy improvement in UI construction performance if all of that can be passed in one call and executed on the native side. Granted, I'd have to rewrite all the UI construction code generation in Alloy to support it, but so be it with the speed increase this would likely provide. If #2 is pursued, it should likely be created in its own ticket as it would be a pretty huge undertaking all platforms would need to support.
@Tony - in last example, how would you do anything with children of some UI element (Button for example)? Like add event listeners, update it after rendering, etc. *** Could performance be better if array of elements were passed to particular methods (like "add", "remove")?
With a few projects I'm involved with that essentially build UI elements dynamically from data definitions, Tony Lukasavage's proposed implementation could be very useful. It would also be nice if there could be a mapping of event listeners and methods to UI elements in the same vein as "off-the-top-of-my-head-no-concern-for-details" ;)
Pull pending against master https://github.com/appcelerator/titanium_mobile/pull/4838
Closing ticket as fixed. While using the description test code, was able to verify there were no duplicate kids in the console. Tested on: SDK build: 3.2.0.v20131024120843 Ti CLI: 3.2.0 (72f7426b4ee6c2d2883c666d5b7e03906a16012f) Devices: iphone 5 (6.0), ipad 2 (7.0.2)