I have an Xcode Swift application that is using a UITableView to display a list of results.
Each row has a value which is calculated to be the difference of a value in the next row.
When the data loads, all is good. However I noticed that, as I have now learnt with the behaviour of the TableView, is that each cell is calculated when it is about to be presented to view.
So originally I had in my code logic that said,
// Global variable
var previousX = 0;
// Building up cell (this happens for each cell in the table view)
----------------------------
var x = 5;
var difference = x - previousX;
previousX = x;
----------------------------
So this works fine when the load lists for the first time, and you scroll down.
However as soon as you scroll up, it loads the cells again. What this means is that my 'previousX' isn't neccessaily the cell above the current cell that I am building, but could be below to. So the values are sort of unpredicatble.
How can I avoid this? How can my cell ALWAYS get the 'previous/above' cell value when building the cell up? Clearly using 'previousX' being the value of the last cell that was populated is not the ideal solution.
If you need any more info from me, let me know.
thanks!
To Achive This you have to maintain array for This:
// instance variable
var previousX:[Int] = []
// Give default to array
previousX = [Int](count: 3, repeatedValue: 0) // create array as per you need
// Building up cell (this happens for each cell in the table view)
var x = 5
var difference = x - previousX[indexPath.row - 1]
previousX[indexPath.row] = difference
Use indexPath.row value as the data for each row which is unique and increase/decrease automatically depending on the cell position. It might help.
Related
//Sample sheet here
Hi,
I am using formulas to calculate an array N:R. Once calculated, I want to determine the last row of the array with a non-empty cell (the empty cells are not blank).
What I can do so far:
Return the last non-empty cell of a column
=INDEX(FILTER(O:O,O:O<>""), ROWS(FILTER(O:O,O:O<>"")))
or the row of the filter selection (in my case 25 in the filter selection vs 38 in the sheet)
=ROWS(FILTER(O:O,O:O<>""))
What I haven't figured out is how to:
Do this search for the whole array and not just one row at a time
Return the row of the last non-empty cell in the array
Cheers
For a formulaic approach, you can try
=max(filter(row(N2:N), MMULT(N(N2:R<>""), transpose(column(N2:R2)^0))>0))
This custom function will do it. Sometimes scripts are way easier than some of the bizarre formulas that arise (IMHO). It just loops through the data row by row and notes the row number if it finds data ie cell.value() != ""
function findHighestNonEmptyRow(dummyRange){
var sheet = SpreadsheetApp.getActive();
var range = sheet.getRange("N:R");
var valuesRC = range.getValues();
var numRows = range.getNumRows();
var numCols = range.getNumColumns();
var highestNonEmptyRow = 0;
for (var row = 0; row < numRows; row++) {
for (var col = 0; col < numCols; col++) {
if (valuesRC[row][col] != ""){
highestNonEmptyRow = row+1; // +1 to offset loop variable
}
}
}
Logger.log(highestNonEmptyRow);
return highestNonEmptyRow;
}
Log show correct value of 38. You can delete the Logger.log(highestNonEmptyRow); line when you have tested.
I put the formula in W44 in your test sheet....
EDIT: Due to feedback that all was not as expected...
There was a typo in the first script: This line var range =
sheet.getRange("N:D"); should have been var range =
sheet.getRange("N:R");
I found out that Google scripts caches the result of custom
formulas, and just returns the cached value, even if things on the
sheet have changed. This is bizarre behavior, but is intended to
reduce CPU time. The workaround is to pass in a range that is likely
to change, and this causes the function to recalculate. I updated
the formula and the called the function like this:
=findHighestNonEmptyRow(N2:R42)
and hey it all works!
Stick to the formula... however, we both learned a lot from your
question I think, so thanks for that!
How my sheet works
I'm making a spreadsheet to show how much parts I have. By using a dropdown, am I able to show that I created a product. With conditional formatting I am showing that having 0 items isn't an issue when the product is created. Created products with 0 items change from red to purple. Purple means it doesn't matter to have 0 items from this product.
My issue
My issue starts with my dropdown. If I merge cells, The value will go into the upperleft cell. This means other cells inside the merged cell are blank. This gives me a problem with conditional formatting.
My conditional formatting code example:
=if($D2=0;$E2="Created")
I have to change this code for every cell because of the condition combined with a dropdown. Having more than 250 rows would be inhumanly hard to do by hand.
My questions
Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
Is there a better way to make my conditional formatting code applyable to merged cells?
This is my sheet
Product items collected sheet link (Shows the problem and solution!)
Product items collected sheet image (Version 1)
Product items collected sheet image (Version 2)
At the heart of this question is the operation of merged cells. When a cell is merged, say over several rows, only the cell at the top left of the merged cell can contain data, respond to conditional formatting, and so on. In a manner of speaking the other cells cease to exist and values CANNOT be assign to them.
The questioner asks:
Q: Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
A: No. Not just in an "efficient" way; it's just not possible.
Q: Is there a better way to make my conditional formatting code applicable to merged cells?
A: No and yes ;)
No. In so far as a merged cell is concerned, everything is driven by the value in the top cell of the merged range. There are no other options for the "rest" of the merged cell.
Yes. I'd create a "helper" cells in Column F as in this screenshot
The code to achieve this is dynamic - it will automatically adapt to adding more products, more items, etc.
The logic is fairly simple: Start in F2, test whether E2 has a value (that is, is it the top of the merged cell?). If yes, then assign the value of E2 to F2 AND put that value in a variable for the following cells. If no, the cell in Column E must be part of a merged cell, so assign the value for Column F to the variable that was saved earlier.
function so5270705902() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = ECell.getValue();
//Logger.log("offsetrange#"+i+" range value = "+ECellVal);
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBUG
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
if (ECell.isBlank()) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); ??DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 22 October 2018
For development purposes, I used a small range of only 14 rows in Column E. However the questioner's data covers over 250 rows, so I expanded development testing to cover 336 rows (yeah, I know, but I was copy/pasting and I ended up with 336 and was too lazy to delete any rows. OK?). I found that the code took over 81 seconds to process. Not good.
The primary reason (about 80 seconds worth) for the long processing time is that there is a getValue statement within the loop - var ECellVal = ECell.getValue();. This costs about 0.2 seconds per instance. Including getValue in a loop is a classic performance mistake. My bad. So I modified the code to get the values of Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();.
I was surprised when the execution time stayed around the same mark. The reason was that the isBlank evaluation - if (ECell.isBlank()) { which previously took no time at all, was now consuming #0.2 second per instance. Not good++. So after searching Stack Overflow, I modified this line as follows:
if (!Evals[(i-dataStart)][0]) {.
Including setValues in a loop is also asking for trouble. An option would have been to write the values to an array and then, after the loop, update the Column E values with the array. However in this case, the execution time doesn't seem to have suffered and I'm leaving the setValues inside the loop.
With these two changes, total execution time is now 1.158 seconds. That's a percentage reduction of , um, a LOT.
function so5270705903() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[2];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
// get the values for Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = Evals[(i - dataStart)][0];
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBU
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
// instead is isblank, which was talking 0.2 seconds to evaluate, this if is more simple
if (!Evals[(i - dataStart)][0]) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); //DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 3 March 2019
The questioner made his final changes to the code. This code is the final solution.
function reloadCreatedCells() {
// Basic declarations.
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var D_vals = sheet.getRange("D1:D").getValues();
var D_last = D_vals.filter(String).length;
// First row with data.
var dataStart = 2;
// Set up to loop through the rows of Column H - K.
var mergedcellvalue = "";
// Get the values for Column H - K BEFORE the loop.
var H_K_vals = sheet.getRange("H2:K").getValues();
// How many people we have.
var people = 4;
// The first vertical row.
var rowStart = 12;
// Horizontal rows.
for (var h = 0; h < people; h++) {
// Vertical rows.
for (var v = dataStart; v < D_last; v++) {
// Set the range for the row.
var range = sheet.getRange(v, rowStart + h);
// Logger.log(range.getA1Notation()); //DEBUG
// Get the value in column H - K.
var H_K_Cell = range.offset(0, -people);
// Adding Created and not created values inside L - O.
var H_K_CellVal = H_K_vals[(v - dataStart)][h];
// Logger.log(H_K_Cell.getA1Notation() + ': ' + H_K_CellVal); //DEBUG
// When a row is merged, the value is only inside the top row.
// Therefore, you need to check if the value is empty or not.
// If the value is empty. Place the top value of the merged cell inside the empty cell.
if (!H_K_vals[(v - dataStart)][h]) {
// Logger.log(H_K_Cell.getA1Notation() + ": is blank. We're below the top cell of the merged cell."); //DEBUG
// Set the value to the top cell of the merged cell with "mergedcellvalue".
range.setValue(mergedcellvalue);
} else {
// Logger.log(H_K_Cell.getA1Notation() + ": has a value. We're at the top of the merged cell."); //DEBUG
// Paste the H_K_CellVal into this range.
range.setValue(H_K_CellVal);
// Update the "mergedcellvalue" variable, so that it can be applied against lower cells of this merged cell.
mergedcellvalue = H_K_CellVal;
} // end of the if isblank.
} // End of the vertical row loop.
} // End of the horizontal row loop.
}
I am trying to tidy up a dataset. It contains country names stretched over two lines. For easier analysis, I am trying to have each year correspond to one country.
This is what I am going for. Each cell contains one country.
Is there a function that would transform the above table to the table below?
Thanks!
There's no function I know of to do this, but you could do it with a relatively simple Google App Script (overview and installation instructions). This script will unmerge all merged cells and replace empty cells with the value of the cell immediately above.
function unmergeAndExpand() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getDataRange();
// unmerge all of the cells in the sheet
range.breakApart();
var data = range.getValues();
var width = range.getLastColumn();
// walk over your spreadsheet column by column
for(var column = 0; column < width; column++){
var last = "";
// walk down the rows
for(var row = 0; row < data.length; row++){
// if the current cell is empty and
if(last && ! data[row][column]){
sheet.getRange(row+1, column+1).setValue( last );
}
// store the value of the current cell
// so that we can fill the next cell, if it is empty
last = data[row][column];
}
}
}
I am new to google scripts. I have a range where the cells in a column change. If the number of a cell from that column is above zero then the script has to send an email with the message being the content from a cell in the same row with the updated cell(that is, different column) Let's say If a value in column I changes then automatically it has to send an email with the message contained in the same row but column F for example. Please see below, I would appreciate some help.
function SendValue() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("Dividend");
var values = sheet.getRange("I2:I").getValues();
//var values1 = sheet.getRange("G2:G").getValues();
var results = [];
for(var i=0;i<values.length;i++){
if(values[i]>0){
results.push(sheet.getIndex([+i+2]+"G"));
// +2 because the loop start at zero and first line is the second one (I2)
}
}
MailApp.sendEmail('sxxxxxx#gmail.com', 'EX-DIVIDEND', results );
};
getValues() returns a 2 dimensional array, or more accurately an array of arrays, even if one of the dimensions is 1. values[i] is not a value but rather an array representing the i row of the range. You will need to acess that array to get at the cell at a particular column in that row.
replace if(values[i]>0) with if(values[i][0]>0)
I suspect that results.push(sheet.getIndex([+i+2]+"G")); should be results.push(sheet.getRange("G"+(i+2)).getValue()); as getIndex() doesn't take arguments and that looks like it is meant to be A1 notation.
However there is a better option than trying to construct A1Notation.
getRange() supports using numbers to refer to rows and columns. getRange(7, i+2) is a better choice.
https://developers.google.com/apps-script/reference/spreadsheet/range#getvalues
https://developers.google.com/apps-script/reference/spreadsheet/sheet#getRange(Integer,Integer)
Ok guys, I am in a bit of a dilemma, so I got a collectionview and I am trying to find which image the user is on so i decided to create a variable x which is equal to the collectionviewcontentsize / view.frame.width. Ok so then I made and if statement to check if the image is above image 0. Right now I am just trying to fix it before writing other code. My problem is when I say print(x) it prints 1 like 40 times before the image finally hits 2. Oh and this collection view has a cell with an image that fills most the screen and is horizontal scrolling, with paging enabled. So I was wondering if someone could help me print just one number in the output per image. Here is a good image to see the problem
So in that image I only scrolled to the next image or 1st image (2nd in collectionview) (img 1 is considered 0). Please ask in the comments any question or if you are confused. Thanks guys!
You can set a variable to hold the current number and print the number only if the number changes. Define the variable at the top of your class:
var currentImage = 0
Then in your scrollViewDidScroll, do the following:
func scrollViewDidScroll(_ scrollView:UIScrollView) {
let x = floor(myCollectionView.contetOffset.x / view.frame.width)
if Int(x) != currentImage {
currentImage = Int(x)
print(currentImage)
}
}