Spotted a leak in UITextView delegate method. Confused about solution - iphone

I've got a problem with an UITextView and one of its delegate methods in my navigation based app:
- (BOOL)textView:aView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
I managed to limit the max length of text the user can input using the above method. But I'm using a leaky array for that matter I think.
The problem is:
I want to save the amount of typed characters right in the very moment the user enters the last line of my textview. I then use that value to calculate the string length - which I compare to the textview's content size to set a limit. The code works fine - but since the method it's inside of is updating with every text input, I'm having trouble releasing the array in the right moment.
Here's some code:
if (numLines == 9)
{
if (!numCharsArray)
{
numCharsArray = [[NSMutableArray alloc] initWithCapacity:1]; // Stack trace gives this line 3,3% of the leak.
}
numChars = tView.text.length;
NSNumber *number = [[NSNumber alloc] initWithInteger:numChars]; // This line gets 77,3%.
[numCharsArray addObject:number]; // This line gets the rest, 24,3%.
[number release];
startChars = [[numCharsArray objectAtIndex:0] integerValue];
NSString *lastLine = [[NSString alloc]initWithString:[[tView text] substringFromIndex:startChars]];
CGSize lineSize = [lastLine sizeWithFont:tView.font forWidth:tView.contentSize.width lineBreakMode:UILineBreakModeWordWrap];
[lastLine release];
if (range.length > text.length)
{
return YES;
}
else if (numLines == 9 && lineSize.width >= tView.contentSize.width - 45)
{
return NO;
}
}
else
{
numCharsArray = nil;
/*
if(numCharsArray)
{
[numCharsArray release];
}
*/
}
I tried the out-commented statement above, but that gives me an app crash once I leave the last line of the textview. And as you can see in the code comments - without releasing the array I get a leak.
So how and where do I release that array correctly - keeping it safe while the user is on the last line?

Just replace with
first one
numCharsArray = [NSMutableArray array]; // you do not need to release
//explicitly as its autorelease numberWithInt
second one
NSNumber *number = [NSNumber numberWithInt:numChars]; //autorelease
NSString *lastLine = [[tView text] substringFromIndex:startChars];

Related

Int decimal count in iPhone

I need to know whatever an int64_t has decimals, and how many. This should be placed in if-else-statement. I tried this code, but it causes the app to crash.
NSNumber *numValue = [NSNumber numberWithInt:testAnswer];
NSString *string = [numValue stringValue];
NSArray *stringComps = [string componentsSeparatedByString:#"."];
int64_t numberOfDecimalPlaces = [[stringComps objectAtIndex:1] length];
if (numberOfDecimalPlaces == 0) {
[self doSomething];
} else {
[self doSomethingElse];
}
Your question doesn't make a lot of sense; you are creating the NSNumber object from an int so it will never have decimal places, as an int cannot store them. The reason your code is crashing is that it assumes that the array of components is always at least 2 elements long (as you use objectAtIndex:1).
This is better, though still not that good:
NSString *answer = ...; // From somewhere
NSArray *stringComps = [answer componentsSeparatedByString:#"."];
if ([stringComps count] == 0) {
[self doSomething];
} else if [stringComps count] == 1) {
[self doSomethingElse];
} else {
// Error! More than one period entered
}
This still isn't a very good test as it only tests if a period (.) has been entered, not a valid number.

cant able to acess variable from nsarray crash at runtime

I am crashing at runtime when i pop last element from NSMuttable array.I am getting all element fine but when i acess last element it will give error.
-(id)popOperand{
id operandObject = [self.operandStack lastObject];
if(operandObject) [self.operandStack removeLastObject];
return operandObject;
}
For eg:- if in my stack i have "8"(Its NSNumber object) "+"(Its NSString object) 2(Its NSnumber Object).
NSNumber *rightOperand = [self popOperand];
NSString *operation = [self popOperand];
NSNumber *leftOperand = [self popOperand];//when i acess 8 it shows empty array other element is getting fine;
My question is why i am not able to get last object while this function is working fine for other elememts.
EDIT : At run time when after poping 2 it show stack( 8 +) but after i poped + it show stack() empty.But The "leftOperand" is not getting value. I am not using ARC.
Please ellaborate it.
Thanks.
If you aren't using ARC then you have memory management issues:
- (id)popOperand
{
id operandObject = [self.operandStack lastObject]; // Still retained by operandStack
if(operandObject) [self.operandStack removeLastObject];// Released by operandStack
return operandObject; // Could be dealloc'd any time now!
}
Try this version, which returns a retain'd and autorelease'd object, which conforms to the naming convention of the method:
- (id)popOperand
{
id obj = nil;
if ([self.operandStack count] > 0)
{
id obj = [[[self.operandStack lastObject] retain] autorelease];
[self.operandStack removeLastObject];
}
return obj;
}

Filtering an NSArray from JSON?

I'm trying to implement a searchable tableview in my app, where when someone can search a location and get results. It looks something like this:
I'm getting my source from genomes.com which gives more then just cities, it also has parks, buildings, counties, etc. I want to just show locations which are cities.
The data is a JSON file which is parsed by JSONKit. The whole file comes in (maximum 20 objects) and then the searchable table view shows it. I'm not sure if I should parse the JSON file differently, or if I should make the table view show only the results needed. (Performance in this case is not an issue.). The JSON file gets converted to an NSArray.
Here is part of the array:
{
adminCode1 = MA;
adminCode2 = 027;
adminName1 = Massachusetts;
adminName2 = "Worcester County";
adminName3 = "";
adminName4 = "";
adminName5 = "";
continentCode = NA;
countryCode = US;
countryName = "United States";
elevation = 178;
fcl = A;
fclName = "country, state, region,...";
fcode = ADMD;
fcodeName = "administrative division";
geonameId = 4929431;
lat = "42.2000939";
lng = "-71.8495163";
name = "Town of Auburn";
population = 0;
score = "53.40083694458008";
timezone = {
dstOffset = "-4";
gmtOffset = "-5";
timeZoneId = "America/New_York";
};
toponymName = "Town of Auburn";
},
What I want to do is if the "fcl" (seen in the array) is equal to P, then I want it to show that in the table view. If the "fcl" is some other character, then I don't want it to be seen in the table view. I'm pretty sure that an if statement can do that, but I don't know how to get it so that it filters part of it.
Any help would be appreciated! Thanks
EDIT: As of now, this is the code to search:
- (void)delayedSearch:(NSString*)searchString
{
[self.geoNamesSearch cancel];
[self.geoNamesSearch search:searchString
maxRows:20
startRow:0
language:nil];
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
self.searchDisplayController.searchBar.prompt = NSLocalizedStringFromTable(#"ILGEONAMES_SEARCHING", #"ILGeoNames", #"");
[self.searchResults removeAllObjects];
// Delay the search 1 second to minimize outstanding requests
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSelector:#selector(delayedSearch:) withObject:searchString afterDelay:0];
return YES;
}
Your question is basically, how do you filter your array from a search bar string? If so, you can detect when the text changes via UISearchBarDelegate and then go through your array copying those objects that contain the string you are looking for, i.e.
This is the delegate method you want: searchBar:textDidChange:.
[filterArray removeAllObjects];
for(int i = 0; i < [normalArray count]; i++){
NSRange textRange;
textRange =[[[[normalArray objectAtIndex:i] objectForKey:#"name"] lowercaseString] rangeOfString:[searchBarString lowercaseString]];
//I wasn't sure which objectForKey: string you were looking for, just replace the one you want to filter.
if(textRange.location != NSNotFound)
{
[filterArray addObject:[normalArray objectAtIndex:i]];
}
}
filterTableView = YES;
[tableView reloadData];
Note the filterTableView bool value, this is so your tableView knows either to load normally or the filtered version you just made. You implement this in:
tableView:numberOfRowsInSection: //For number of rows.
tableView:cellForRowAtIndexPath: //For the content of the cells.
Hope this is what you were looking for.
NSMutableArray* filtered = [[NSMutableArray alloc] autorelease];
for (int i=0;i<[data count];i++)
{
NSDictionary* item=[data objectAtIndex:i];
if (#"P" == [item objectForKey:#"fcl"] )
{
[filtered addObject:item];
}
}
So every time the search field changes, you will compute a new array, and then reload your tableview. The number of rows will be the numbers of rows in your filtered array.
To compute the new array, you can do this (assuming an array of dictionaries):
NSString *searchString; // from the search field
NSMutableArray *array = [NSMutableArray arrayWithCapacity:[origArray count]];
for(NSDictionary *dict in origArray) {
NSString *val = [dict objectForKey:#"fcl"];
if([val length] >= searchString) {
NSString subString = [val substringToIndex:[searchString length]];
if([subString isEqualToString:val]) [array addObject:dict];
}
}
Each cell then will get its values from the new array.
Just put your json in a NSDictionary and simply do something like :
if ([[yourJson objectForKey:#"fcl"] stringValue] == #"A")
//doSomething

Need help on very strange iPhone currency addition and subtraction

This is 'sorta' urgent since my app just went live today.
My app works fine in the emulator, fine when I copy it to my phone, iPad and other iPhones. We tested the heck out of the app. I submitted it to the appstore and it is now approved and live but an error occurs only with the downloaded appstore app.
Basically i am rebalancing a bill using NSDecimal and number formatters. In the emulator and phone when I step through the code all is well. But the appstore bundle wigs out. It almost appears that the data that I am adding and subtracting is not being initalized.
Has anyone ever seen anything like this?
I know this post is completely vague but trying to figure out where to start debugging if the code in emulator works fine.
--UPDATE-- adding code
-(void)balanceBill:(UITextField*)textField
{
//save the text field data
int row = [textField tag] - 900;
NSString *enteredSplitAmount = [formatter stringWithNoCurrency:[textField text]];
[theGuestTotals replaceObjectAtIndex:row withObject:enteredSplitAmount];
//Get data object
BillDataObject* data = [self theAppDataObject];
int changedSplitBy = 0;
UITableViewCell *cell;
UITextField *cellTextField;
double adjustedBill = 0.0;
NSString *guestAmountFromArray;
//NSString *guestAmountAdjusted;
//Figure out how many guests did NOT have their bill changed & get new bill total
for (NSUInteger i=0; i < [theGuestTotals count]; i++)
{
guestAmountFromArray = [theGuestTotals objectAtIndex:i];
if ([guestAmountFromArray isEqualToString:data.splitByAmountChanged])
{
changedSplitBy++;
}
//Adding ALL guest amounts to get a NEW Bill Total
adjustedBill += [guestAmountFromArray doubleValue];
}
if (changedSplitBy == 0)
changedSplitBy = 1;
//Convert newBill to decimal
NSDecimalNumber *adjustedBillTotal = [[NSDecimalNumber alloc] initWithDouble:adjustedBill];
NSDecimalNumber *originalBillTotal = [[NSDecimalNumber alloc] initWithString:data.totalBill];
NSDecimalNumber *splitBy = [[NSDecimalNumber alloc] initWithInt:changedSplitBy];
NSDecimalNumber *updatedGuestAmount;
//Figure out the the difference is between the new amount and the old amount
NSDecimalNumber *adjustedBillTotalDifference = [originalBillTotal decimalNumberBySubtracting:adjustedBillTotal];
//figure out the difference each guest who did not have their bill changed difference
NSDecimalNumber *guestSplitAmountDifference = [adjustedBillTotalDifference decimalNumberByDividingBy:splitBy];
//loop through array of guest totals to see if a guest total if different from the original split amout
for (NSUInteger i=0; i < [theGuestTotals count]; i++)
{
guestAmountFromArray = [theGuestTotals objectAtIndex:i];
if ([guestAmountFromArray isEqualToString:data.splitByAmountChanged])
{
NSDecimalNumber *guestAmount = [[NSDecimalNumber alloc] initWithString:[theGuestTotals objectAtIndex:i]];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [tableView cellForRowAtIndexPath:indexPath];
cellTextField = (UITextField*)[cell viewWithTag:i+900];
//add the split amount to the guest amount
updatedGuestAmount = [guestAmount decimalNumberByAdding:guestSplitAmountDifference];
//update the textfield with UPDATED amount
cellTextField.text = [formatter stringWithNumberStyle:NSNumberFormatterCurrencyStyle numberToFormat:updatedGuestAmount];
//replace the guest amount in the array with the UPDATED amount
[theGuestTotals replaceObjectAtIndex:i withObject:[formatter stringWithNumberStyle:NSNumberFormatterDecimalStyle numberToFormat:updatedGuestAmount]];
} else {
//Not equal so just update the amount from what it was...
//this might not be needed but I need to format if it is...
cellTextField.text = [formatter stringWithNumberStyle:NSNumberFormatterCurrencyStyle numberToFormat: [NSDecimalNumber decimalNumberWithString:guestAmountFromArray]];
}
}
//Now all guests who were not edited GOT updated now save that update for the next time this function is run
data.splitByAmountChanged = [formatter stringWithNumberStyle:NSNumberFormatterDecimalStyle numberToFormat:updatedGuestAmount];
//Clear out adjustedBill to get NEW totals after we updated each guest in the loop above
adjustedBill = 0;
//Lets see if we are over or under and do the 'REDISTRIBUTE'
for (NSUInteger i=0; i < [theGuestTotals count]; i++)
{
adjustedBill += [[theGuestTotals objectAtIndex:i] doubleValue];
}
adjustedBillTotal = [[NSDecimalNumber alloc] initWithDouble:adjustedBill];
if ([originalBillTotal compare:adjustedBillTotal] == NSOrderedAscending)
{
[warningImage setHidden:NO];
NSDecimalNumber *overage = [adjustedBillTotal decimalNumberBySubtracting:originalBillTotal];
[overUnderLabel setText:[NSString stringWithFormat:#"%# over",[formatter stringWithNumberStyle:NSNumberFormatterCurrencyStyle numberToFormat:overage]]];
// [self disableDone];
[self enableRedistribute];
}
else if ([originalBillTotal compare:adjustedBillTotal] == NSOrderedDescending)
{
[warningImage setHidden:NO];
NSDecimalNumber *underage = [originalBillTotal decimalNumberBySubtracting:adjustedBillTotal];
[overUnderLabel setText:[NSString stringWithFormat:#"%# under",[formatter stringWithNumberStyle:NSNumberFormatterCurrencyStyle numberToFormat:underage]]];
// [self disableDone];
[self enableRedistribute];
}
else
{
[warningImage setHidden:YES];
overUnderLabel.text = #"";
//[self enableDone];
[self disableRedistribute];
}
}
I think the problem is your attempt to use the UITableViewCells for data storage. You don't push data into the cells, you wait for the UITableView to call YOUR cellForRowAtIndexPath method.
The UITableView will only retain enough cells to fill the screen. As soon as one scrolls out of sight, it will be released. When your loop tries to retrieve it again with cellForRowAtIndexPath: it will allocate a new cell to give you. While your loop may initialize it, it won't be stored or used by the UITable object.
There was a dead store issue with 2 variables in my method. Even though I set them later on I removed them and re-submitted the app. The issue went away.
Thanks for your tips.

Change UILabel in loop

I want to change the UILabel after 2 sec in a loop.
But current code change it to last value after loop finished.
- (IBAction) start:(id)sender{
for (int i=0; i<3; i++) {
NSString *tempStr = [[NSString alloc] initWithFormat:#"%s", #" "];
int randomNumber = 1+ arc4random() %(3);
if (randomNumber == 1) {
tempStr = #"Red";
}else if (randomNumber == 2) {
tempStr = #"Blue";
} else {
tempStr = #"Green";
}
NSLog(#"log: %# ", tempStr);
labelsText.text = tempStr;
[tempStr release];
sleep(2);
}
}
Your code updates label to last value only as your function blocks main thread so UI cannot get updated. To solve that problem move your updating code to separate function and call it using performSelector:withObject:afterDelay: method. (or schedule calls using NSTimer)
Possible solution (you will also need to handle the case when user taps your button several times in a row, but that should not be too difficult):
- (IBAction) start:(id)sender{
[self updateLabel];
}
- (void) updateLabel{
static const NSString* allStrings[] = {#"Red", #"Blue", #"Green"};
static int count = 0;
int randomNumber = arc4random()%3;
NSString *tempStr = allStrings[randomNumber];
NSLog(#"log: %# ", tempStr);
labelsText.text = tempStr;
++count;
if (count)
[self performSelector:#selector(updateLabel) withObject:nil afterDelay:2.0];
}
- (IBAction) start:(id)sender{
for (int i=0; i<3; i++) {
int randomNumber = 1+ arc4random() %(3);
NSString *tempStr = #"";
if (randomNumber == 1) {
tempStr = #"Red";
}else if (randomNumber == 2) {
tempStr = #"Blue";
} else {
tempStr = #"Green";
}
[labelsText performSelector:#selector(setText:) withObject:tempStr afterDelay:i * 2]
NSLog(#"log: %# ", tempStr);
}
}
Don't use sleep() to perform actions after a delay, it blocks the whole thread. Use performSelector:withObject:afterDelay: instead. As you are probably running this on the main thread, sleep() will block any UI updates until after the whole loop has run, so the only thing you see is the last update. As a general rule of thumb, assume that the UI doesn't ever get updated until after the app has finished executing your method.
You shouldn't use %s as the format specifier for an NSString, that's the format specifier for a C string. You should use %# instead. In fact, if all you are doing is initialising the string with an NSString literal, there's no need to use initWithFormat at all, you can just use the literal itself.
You've also got big memory problems. At the beginning of the loop, you allocate memory for an instance of NSString that is a single space. You then overwrite the pointer to this memory when you assign to tempStr again, meaning you leak the original allocation of memory. Build and Analyze will find problems like this for you. Then you release tempStr, but as the second assignment to this pointer variable was to an autoreleased NSString, the instance will be released one time too many when the autorelease pool gets drained, which will probably manifest itself as a crash that's impossible to debug a little later in the app.
I'd do something like this:
- (void)showRandomColourAfterDelay {
static NSUInteger count = 0;
switch (arc4random()%3) {
case 0:
labelsText.text = #"Red";
case 1:
labelsText.text = #"Blue";
case 2:
labelsText.text = #"Green";
}
count++;
if (count >= 3) return;
[self performSelector:#selector(showRandomColourAfterDelay) withObject:nil afterDelay:3];
}
In fact, I'd probable use an NSArray to hold the colour strings, but that would involve changes outside of a single method, so I've stuck with your hard-coding approach.