Script crashes app


#1

Hey,

I was writing a script to display a list and choose some items from it, like the Choose from List action in Shortcuts. While testing it, the Scriptable app crashes… I don’t know why and I can’t debug it then…

Script
/**
Shows a list of items to pick from

@param {UITableCell[][]|object[]|object[][]|string[]} items
	Either ["Item 1", "Item 2"]
	or [[UITableCell, UITableCell, ...], [UITableCell, UITableCell, ...]]
	or [{rowHeight: 20, cells: [UITableCell, <cellObject>]}, {rowHeight: 20, text: "Item 2"}]
	or [[{widthWeight: 20, text: "Item", subtitle: "We don't need that subtitle", aligned: "left"}, {widthWeight: 80, image: Image, aligned: "right"}]] (these are called <cellObject>)
	or a mixture of these

@param {object} [options]
	Some more options

@param {string} [options.message]
	The message you want to show at the top of the list

@param {number} [options.max=0]
	Specify the maximum number of selectable items. 0 or not specified means that all items are selectable

@param {Boolean|Boolean[]} [options.initialSelection=false]
	If it is a Boolean: true = all selected, false = none selected
	If it is an array of Booleans: each item at each index is selected if the value at that index is true

@param {Boolean} [options.repeatInitialSelection=false]
	If `options.initialSelection` is an array, this specifies if the array should be repeated if it is shorter than there are items

@param {Boolean} [options.showSeparators=false]
	Whether or not to show seperators between items

@param {string} [options.returns="filter"]
	"filter" to return an array where the item at each index is selected if the value at that index in the array is true
	"!filter" to return the inverted array of "filter"
	"selected" to return the selected items
	"!selected" to return the not selected items
	"index" to return the index of the selected items
	"!index" to return the index of the not selected items
 */
async function choose(items, options) {
	function isCellObj(i) {
		return i instanceof UITableCell || (typeof i === "object" && (i.text || i.subtitle || i.image instanceof Image));
	}
	if (!items || !Array.isArray(items) || !items.length) {
		return Promise.reject("Please pass at least one item in the array to show");
	}
	if (
		!items.every(i => 
			typeof i === "string" ||
			(Array.isArray(i) && i.every(isCellObj)) ||
			(typeof i === "object" && Array.isArray(i.cells) && i.cells.every(isCellObj)) ||
			(typeof i === "object" && typeof i.text === "string")
		)
	) {
		return Promise.reject("The items have to be either an array of strings, an array of arrays of UITableCells, an array of objects with the key \"cells\" that holds an array of UITableCells or objects with at least one of the keys \"text\", \"subtitle\" or \"image\", an array of objects with the key \"text\" that holds a string or an array of arrays of objects with at least one of the keys \"text\", \"subtitle\" or \"image\"");
	}
	
	let ui = new UITable();
	const checkmarkWidth = 8;
	let selectionOrder = [];
	
	opions = options || {};
	options.max = options.max || 0;
	options.initialSelection = options.initialSelection || false;
	options.repeatInitialSelection = options.repeatInitialSelection || false;
	options.showSeparators = options.showSeparators || false;
	
	if (!Array.isArray(options.initialSelection)) {
		options.repeatInitialSelection = true;
		options.initialSelection = [options.initialSelection];
	}
	options.initialSelection = options.initialSelection.map(s => !!s);
	
	ui.showSeparators = options.showSeparators;
	
	let selected = options.initialSelection;
	if (selected.length < items.length) {
		selected.length = items.length;
		for (let i = options.initialSelection.length; i < selected.length; i++) {
			selected[i] = options.initialSelection[i % options.initialSelection.length];
		}
	} else if (seleted.length > items.length) {
		selected = selected.slice(0, items.length);
	}
	
	
	function createRows() {
		ui.removeAllRows();
		let row, cell;
		
		if (selected.filter(s => s).length > options.max) {
			let sum = 0;
			if (selectionOrder.length) {
				let i = selectionOrder.shift();
				selected[i] = false;
			} else {
				selected = selected.map((s, i) => (s && ++sum <= options.max) ? s : false);
			}
		}
		
		if (options.message) {
			row = new UITableRow();
			row.dismissOnSelect = false;
			row.addText(options.message);
// TODO: measure text height. Maybe with WebView?
			row.height = 60;
			ui.addRow(row);
		}
		
		// selection things & cancel button
		row = new UITableRow();
		row.dismissOnSelect = false;
		// invert selection
		cell = row.addButton("\u25e9 \u2192 \u25ea");
		cell.onTap = () => {
			selected = selected.map(s => !s);
			selectionOrder = [];
			createRows();
		};
		// select all
		cell = row.addButton("\u2714\u0336");
		cell.onTap = () => {
			selected = selected.map(s => false);
			selectionOrder = [];
			createRows();
		};
		// cancel
		cell = row.addButton("Cancel");
		cell.dismissOnTap = true;
		cell.onTap = () => {
			selected = undefined;
		};
		
		items.forEach((item, i) => {
			row = new UITableRow();
			row.dismissOnSelect = false;
			row.onSelect = (number) => {
				selected[i] = !selected[i];
				selectionOrder = selectionOrder.filter(s => s !== i);
				if (selected[i]) {
					selectionOrder.push(i);
				}
				createRows();
			}
			
			// checkmark
			let checkmarkCell = UITableCell.text(selected[i] ? "\u2611" : "\u2610");
			checkmarkCell.centerAligned();
			row.addCell(checkmarkCell);
			
			totalWidth = [];
			
			if (typeof item === "object" && !Array.isArray(item)) {
				if (item.height) row.height = item.height;
				if (item.text) item = text;
				else if (Array.isArray(item.cells)) item = item.cells;
			}
			
			if (typeof item === "string") {
				cell = row.addText(item);
				cell.widthWeight = 100;
				totalWidth.push(cell.widthWeight);
			} else {
				item.forEach(c => {
					if (c instanceof UITableCell) {
						row.addCell(c);
						totalWidth.push(c);
					} else if (c.image) {
						cell = row.addImage(c.image);
						c.widthWeight && (cell.widthWeight = c.widthWeight);
						c.align && cell[c.align + "Aligned"]();
						totalWidth.push(cell);
					} else {
						cell = row.addText(c.text, c.subtitle);
						c.widthWeight && (cell.widthWeight = c.widthWeight);
						c.align && cell[c.align + "Aligned"]();
						totalWidth.push(cell);
					}
				});
			}
			// calculate the average width of all cells if there are cells with default width 0
			// if all cells have the default width 0, calculate the average width of 100 - checkmarkWidth
			let sum = totalWidth.reduce((acc, cur) => acc + cur.widthWeight);
			let avg = sum / totalWidth.filter(i => i.widthWeight).length;
			if (sum === 0) {
				sum = 100;
				avg = sum / totalWidth.length;
			} else {
				sum += totalWidth.filter(i => !i.widthWeight).length * avg;
			}
			totalWidth.forEach(i => {
				if (!i.widthWeight)
					i.widthWeight = avg;
			});
			
			// calculate weight value of checkmarkWidth as pixels
			let deviceWidth = Device.screenSize().width * Device.screenScale()
			checkmarkCell.widthWeight = sum / (deviceWidth - checkmarkWidth) * checkmarkWidth;
			
			ui.addRow(row);
		});
		ui.reload();
	}
	
	createRows();
	await ui.present();
	
	if (selected == null) return selected;
	
	let invert = options.returns && options.returns[0] === "!";
	if (invert) {
		options.returns = options.returns.substr(1);
		selected = selected.map(s => !s);
	}
	switch (options.returns) {
		case "filter":
		default:
			break;
		case "index":
			selected = selected.map((s, i) => s && i).filter(i => i !== false);
			break;
		case "selected":
			selected = items.filter((item, i) => selected[i])
	}
}


let res = await choose(["a","b","c","d","e"], {
	message: "This should be short"
})
log(res)

@simonbs Can you please look into this?


#2

I just tried running your script and I’m also seeing the crash. I can see that at least one of your width weights is an infinite number. It may be that you have a division by zero somewhere in your script.

I’ve made a small change to UITableCell that will interpret infinite values as zero (the default value) in the zero to prevent the app from crashing.


#3

Thank you, that really helped!

I noticed in this script that the autocomplete is really slow. It hangs behind by about 4 to 5 seconds. In my other script AppWatcher that has even more lines of code, it has no delay, which means that this shouldn’t be the problem…