Monday, September 12, 2011

The UIPicker - setting the default (tricker than you'd think!)



Ok, we're going to spiff up the settings page today, putting the correct values in the level picker, then adding numeric only and logic edits to the start and end quiz numbers. We'll start with that and take things from there.

Here's what we currently have in the array used for the data picker.


[arrayNo addObject:@" 100 "];
[arrayNo addObject:@" 200 "];
[arrayNo addObject:@" 400 "];
[arrayNo addObject:@" 600 "];
[arrayNo addObject:@" 1000 "];


So, do we change this to a number? Why is it an object as opposed to an NSString, for example? I think we'll just stick with it as it is, and then stip the blanks and covert it to a number. Then, we'll store the number in app state, and persist app state. Ugh. Can we avoid that big mess again and just go straight to prefs? Let's try that, first.

Here's some info on NSNumber vs. NSInteger:

http://iphonedevelopertips.com/cocoa/nsnumber-and-nsinteger.html

As we saw previously, NSInteger is just a cpu-architecture sensitive typedef:

if __LP64__ || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif


NSNumber, on the other hand, is a full fledged Objective C class. To wit:

You can create an NSNumber object from a signed or unsigned char, short int, int, long int, long long int, float, double or BOOL.

One of the primary distinctions is that you can use NSNumber in collections, such as NSArray, where an object is required. For example, if you need to add a float into an NSArray, you would first need to create an NSNumber object from the float:

float percentage = 40.5;
...
// Create NSNumber object, which can now be inserted into an NSArray
NSNumber *percentageObject = [NSNumber numberWithFloat:percentage];


So, maybe we should try to change this to an NSNumber, instead of string. Well, if I look at the docs, to create an NSNumber, then can be an init or create. What's the difference?

I'm not sure what create does still, but I'm pretty sure that init is kind of a constructor. There's an example SO:

NSNumber * n = [[NSNumber alloc] initWithInt:number];
self.minutesLeft = n;
[n release], n = 0;


But, actually, the technique of adding the string objects maybe means that you don't really have to worry about releasing the numbers - at least not yet; just because of the way it's allocating. Well, anyway, in the spirit of minimalistic programming, let's just move this thing ahead by changing the values and dealing with the rest later.


arrayNo = [[NSMutableArray alloc] init];
[arrayNo addObject:@" 1 "];
[arrayNo addObject:@" 2 "];
[arrayNo addObject:@" 3 "];
[arrayNo addObject:@" 4 "];
[arrayNo addObject:@" 5 "];


Ok, now, what's the next step? We'll want to save the number to preferences. Do we have any examples in the code? I think so.

Ah, here it is:

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

// save counters
[prefs setInteger:correctAnswers forKey:@"correctAnswers"];
[prefs setInteger:currentShuffledArrayIndex forKey:@"currentQuestionNumber"];

// saving it all
[prefs synchronize];




Let's create a utils class to save the state.

So, what we want to do is pass a name and a value to this to be saved. But the problem is, it will need to have a separate method for each type - setInteger, etc. Well, we'll probably only be saving integers and strings. You know what? It's three lines of code vs. one. Let's just use it.

This is the method that gets the chosen value:


- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
mlabel.text= [arrayNo objectAtIndex:row];
}


So, how do we convert this object to an Integer? And what kind of integer is it? According to the docs, it's an NSInteger - not the value. So, how do convert from an NSString to an NSInteger? We *think* it's an NSString because that's what mlabel.text is declared as as part of UILabel - unless it does some type of automatic casting or type conversion.

Here's a nice, simple example from SO:

NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
[f setNumberStyle:NSNumberFormatterDecimalStyle];
NSNumber * myNumber = [f numberFromString:@"42"];
[f release];



Oh, right, that's NSNumber, so we need to grab NSInteger from that.

NSInteger myInteger = [myNumber integerValue];

Ok, how does this look?

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
mlabel.text= [arrayNo objectAtIndex:row];
NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
[f setNumberStyle:NSNumberFormatterDecimalStyle];
NSNumber * myNumber = [f numberFromString:[arrayNo objectAtIndex:row]];
[f release];

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

NSInteger myInteger = [myNumber integerValue];


// save counters
[prefs setInteger:myInteger forKey:@"jlptLevel"];

// saving it all
[prefs synchronize];

}


Ok, it runs without crashing. Next, we'll retrieve the level from settings and NSLog.

There's actually a kind of a problem. The UIPicker is relatively HUGE. Apparently, it's not supposed to be shrunk. But, it takes up way to much real estate on the view. But one thing I know - I'm not going to muck with it. Apparently there are possible glitches. We'll worry about it later.

Ok, let's retrieve and print the value from prefs to make sure it's working.

// Get last state if existing
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

NScorrectAnswers = [prefs integerForKey:@"correctAnswers"];


Hmm...I'm doing something wrong. This code:

myInteger = 0;

NSUserDefaults *prefs2 = [NSUserDefaults standardUserDefaults];

// Get last state if existing
prefs2 = [NSUserDefaults standardUserDefaults];

myInteger = [prefs2 integerForKey:@"jlptLevel"];

NSLog(@"myInteger: %@", myInteger );


is giving me a bad access on the NSLog statement. Let's debug it.

Ok, it must be the way I'm printing it out. Oh, darn. It's because of the @ - that's an object. What's an integer? i? d?

This give some good info:

looks like it should be %d.

Yup - that was it. Man, it's getting cold in here.

Ok. now that that's done, let's maybe just set the default to 3 for the first time in. Well, it's easily changeable in the default settings. I could set up an XML file with control settings to read. Similar to the strings resource in Android. The troubling thing is this is called "NSUserDefaults", as if they expect it to get it's defaults from some kind of a file.

Well, we'll tackle that later. Right now, I'm just trying to get this thing up and running as fast as possible. So I'm going HARCODE this initial setting (screams of horror, shrieks of fear), if it's not found in prefs. That's kind of what the Android does, it gives you default value when retrieving something from settings. So, where does that happen? Well, we'll do it in the start controller.

First, we'll retrieve it with something like this:

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

NSInteger myInteger = 0;

NSInteger myInteger = [prefs integerForKey:@"jlptLevel"];

if (myInteger == 0) {
// save level
[prefs setInteger:3 forKey:@"jlptLevel"];

// saving it all
[prefs synchronize];

}

Actually, let's check this one, too:


NSUserDefaults *prefs2 = [NSUserDefaults standardUserDefaults];

// Get last state if existing
prefs2 = [NSUserDefaults standardUserDefaults];

myInteger = [prefs2 integerForKey:@"jlptLevel"];

NSLog(@"StartController, JLPTLevel: %d ", myInteger );



2011-09-12 16:26:03.941 JlptQuizApp[1377:207] StartController, JLPTLevel: 2

It's showing up as 2 - is that right?

Let's change it in settings to 4 and restart the app.

Yes.

2011-09-12 16:29:25.220 JlptQuizApp[1422:207] StartController, JLPTLevel: 4

Ok, so, now what remains is to correctly set the UIPicker control with whatever value's in prefs.

Ah, I see - the selectRow does it. It wasn't clear from skimming the docs. From SO:

self.myPicker selectRow:rowInt inComponent:componentInt animated:NO]

I'm not sure what the component is supposed to be, so I'll just set it to zero for now, and set the row to 5.

Ah, actually, it's already there from yesterday's tutorial:

[pickerView selectRow:1 inComponent:0 animated:NO];
mlabel.text= [arrayNo objectAtIndex:[pickerView selectedRowInComponent:0]];



So, let's change the row to 5 and make sure it's working.

Actually, 4 since it's zero-based.

[pickerView selectRow:4 inComponent:0 animated:NO];
mlabel.text= [arrayNo objectAtIndex:[pickerView selectedRowInComponent:0]];


Hmmm...no impact.


SO to rescue - found this thread:

http://stackoverflow.com/questions/3513636/uipicker-selectrow

Resulting in this successful code:


(NSString *)pickerView:(UIPickerView *)pickerViewParm titleForRow:(NSInteger)row forComponent:(NSInteger)component;
{
[pickerViewParm selectRow:4 inComponent:0 animated:YES];

return [arrayNo objectAtIndex:row];
}



Here's the happy outcome:


No comments:

Post a Comment