Sunday, May 23, 2010

Buttons not responding.

In playing around with my new MathDrill GUI, I'm adding subviews that display data for choices.  I'm placing these in a view that has easy coordinates for my subview, however what I've found is that if the view I'm placing has buttons outside the space on the screen for the superview, that the buttons DO NOT RESPOND.   This is annoying.  

The convertRect and convertPoint routines are essential here.  Do your math in the most convenient space for you, then use some built in utility routines to convert it to coordinates within the space you are interested in.

I modified my placement of the display of a text to select buttons from:


[self.displayArea addSubview:choiceView];

to:
CGRect newFrame=[self.view convertRect:currentProblemDisplay.view.frame fromView:self.displayArea ];
currentProblemDisplay.view.frame=newFrame;
[self.view addSubview:currentProblemDisplay.view];

This way the view is displayed in the same location, but regardless of if it is in the bounds of the variable displayArea, it will still respond to inputs.

Friday, May 21, 2010

Math Drill: Refactoring into a subview

I next made several refactorings for the math drill project.

Protocols instead of classes


 Instead of passing entire classes around, I made some protocols that define behavior and create an id parameter for this.   This gets rid of several 'forward class' declarations, and allows for classes to be included easier.

Protocol 1 is for the math definition itself, it has the commonly exposed functions that are needed outside.

The next one is for the controller.  It defines the action of selecting a choice (we don't care what it is), and switching to an arbitrary problem.

//The first protocol defines a class that can define a set of problems and
// also select choices.
@protocol MathProblemDefinition
// Called when this problem is seleced.
- (IBAction) setFocus:(id) choice;
// called to indicae that this problem has a choice selected.
- (bool) setChoice:(int) choice;
- (NSMutableArray*) calculateChoices:(int) maxNumChoices;
@end

// A protocol for a controller that this view will notify when it comes into
// focus.
@protocol MathProblemController

- (void) choiceDone;
- (void) setProblemNumber:(int) problemNumber;

@end

A dedicated view controller for the problem display


I defined a separate class for the problem display.  This allows less coupling, and make the actual controller simpler.   I later want to make the UI better.
Header:

@interface MathChoiceDisplay : UIViewController {
NSSet *choices;
id<MathProblemController> controller;
id<MathProblemDefinition> problem;
}
@property (nonatomic,retain) id problem;
@property (nonatomic,retain) NSSet *choices;
@property (nonatomic,retain) id controller;
- (id) initWithChoices:(UIView *) targetView choices:(NSArray*)possibleChoices;
- (IBAction) buttonChosen:(id)sender;


Implementation:
}
- (id) initWithChoices:(UIView *) targetView choices:(NSArray*)possibleChoices
{
int width=targetView.frame.size.width;
int height=targetView.frame.size.height;
self.view =[[UIView alloc] initWithFrame:CGRectMake(targetView.frame.origin.x-width, targetView.frame.origin.y-height,
targetView.frame.size.width*3, targetView.frame.size.height*3)];
self.view.center=targetView.center;
self.view.backgroundColor=[UIColor colorWithRed:0 green:0 blue:0 alpha:0];
int row=0;
int column=0;
[self.view becomeFirstResponder];
self.view.userInteractionEnabled=true;
NSLog(@"Num Choices %d",[possibleChoices count]);
for (NSNumber *value in possibleChoices)
{
if ((row==1) &&(column==1))
column++;
UIButton *button=[[UIButton alloc]init] ;
button.tag=[value intValue]; 
button.frame=CGRectMake(column*width,height*row,width,height);
button.backgroundColor=[UIColor redColor];
button.userInteractionEnabled=true;
[button setTitle:[value stringValue] forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonChosen:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
column++;
if (column>=3)
{
column=0;
row++;
}
}
[self becomeFirstResponder];

return self;
}

- (IBAction) buttonChosen:(id)sender
{
UIButton *button=(UIButton*) sender;
int choice=button.tag;
[problem setChoice:choice];
[controller choiceDone ];
 
 
}
 
@end

Usage: This becomes much cleaner and allows it to be seen what is doing.

- (void) setProblemNumber:(int) problemNumber
{
NSLog(@"Choosing problem %d",problemNumber);
MathProblem *oldProblem=[self getProblemAtIndex:currentProblem];
if (oldProblem!=nil)
oldProblem.view.alpha=0.5;
if (currentProblemDisplay!=nil)
{
[currentProblemDisplay.view removeFromSuperview];
self.currentProblemDisplay=nil;
}
currentProblem=problemNumber;
MathProblem *problem=[self getProblemAtIndex:problemNumber];

if (problem==nil)
return;
self.currentProblemDisplay= [[MathChoiceDisplay alloc] initWithChoices:problem.view choices:[problem calculateChoices:8]]; 
self.currentProblemDisplay.problem=problem;
self.currentProblemDisplay.controller=self;
[self.displayArea addSubview:currentProblemDisplay.view];

problem.view.alpha=1;
}


I also refactored the controller to make the 'selecting math problem based on index' easier:
- (MathProblem*) getProblemAtIndex:(int) index
{
MathProblem *returnValue=nil;
if ((index>=0)&&(index<[currentProblems count]))
{
returnValue=(MathProblem*)[currentProblems objectAtIndex:index];
}
return returnValue;
}

Thursday, May 20, 2010

IPAD Math: dynamic button display

I found that having the math buttons at the bottom of the screen for the proper choice was a poor UI choice.  It caused your eyes to travel from the problem to the buttons and back, slowing down the process.  I made a preliminary UI overlay that allows buttons to be dynamically displayed. This is not a final solution, it takes up too much screen real-estate however it was a good experiment with dynamic view overlays.

The only reason I'm showing this stage, is so that I can discuss the refactoring that will occur to create cleaner, more modular code.

I display a set of buttons around the problem being currently displayed. I made the buttons the same size as the problem, however that didn't allow enough choices, and takes up too much real estate.
In MathProblem I added a method that returns a list of possible choices for the math problem.  In this case it only returns 8 choices, which is based on the display method I'm using.   
- (NSMutableArray *)calculateChoices;

- (NSMutableArray *)calculateChoices
{
NSMutableArray *set=[[NSMutableArray alloc]init];
for (int i=0;i<9;i++)
{
if (i==correctResult)
continue;
[set addObject:[NSNumber numberWithInt:i]];
}
NSNumber *correctResultO=[NSNumber numberWithInt:correctResult];
    NSMutableSet *returnValues=[[NSMutableSet alloc]init];
[returnValues addObject:correctResultO];
for (int i=0;i<7;i++)
{
int randomalue=arc4random()%[set count];
NSNumber * value=(NSNumber *)[set objectAtIndex:randomalue];
[set removeObject:value];
[returnValues addObject:value];
}
[set release];
return [returnValues autorelease];
}

Next I refactored the MathDrillViewController to have some additional properties, and methods to handle displaying a set of selections:
@property (nonatomic,retain) UIView *choiceView;

and I modified the set problem choice to display the dialog.


- (void) setProblemNumber:(int) problemNumber
{
NSLog(@"Choosing problem %d",problemNumber);
if ((currentProblem>=0) && (currentProblem<=[currentProblems count]))
{
MathProblem *oldProblem=(MathProblem*)[currentProblems objectAtIndex:currentProblem];
if (choiceView!=nil)
{
[choiceView removeFromSuperview];
self.choiceView=nil;
}

oldProblem.view.alpha=0.5;
}
currentProblem=problemNumber;
MathProblem *problem=(MathProblem*)[currentProblems objectAtIndex:currentProblem];
problem.view.alpha=1;
int width=problem.view.frame.size.width;
int height=problem.view.frame.size.height;
self.choiceView =[[UIView alloc] initWithFrame:CGRectMake(problem.view.frame.origin.x-width, problem.view.frame.origin.y-height,
  problem.view.frame.size.width*3, problem.view.frame.size.height*3)];
self.choiceView.center=problem.view.center;
[self.displayArea addSubview:choiceView];

choiceView.backgroundColor=[UIColor colorWithRed:0 green:0 blue:0 alpha:0];
NSMutableArray *choices=problem.resultChoices;
int row=0;
int column=0;
for (NSNumber *value in choices)
{
if ((row==1) &&(column==1))
column++;
UIButton *button=[[UIButton alloc]init] ;
button.tag=[value intValue]; 
button.frame=CGRectMake(column*width,height*row,width,height);
button.backgroundColor=[UIColor redColor];
button.userInteractionEnabled=true;
[button setTitle:[value stringValue] forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonChosen:) forControlEvents:UIControlEventTouchUpInside];
[self.choiceView addSubview:button];
column++;
if (column>=3)
{
column=0;
row++;
}
}

}

Originally I had the choiceView belong to the math problem view, but when I did that I didn't get button presses, and the alpha was picked up from the problem view (causing strange display  results).

Note the line:

choiceView.backgroundColor=[UIColor colorWithRed:0 green:0 blue:0 alpha:0];
This set the background to be transparent, so that only the buttons are displayed.


Thursday, May 13, 2010

IPad Math: adding a timer

I added a simple timer to my code.  Just a label at the top of my screen which has a custom button behind it.  I then added the following code to start my timer  and to count down.  Note that right now I display 60 for a while (for seconds) instead of 1:00.  I probably need a more advanced time formatting function.


New properties


I added two new properties, one for my timer, and one to track when the timer was started.

NSDate *timerStart;
NSTimer *timer;



New method declarations



I added an action for choosing the button, as well as a  callback for the timer.

- (void) updateTime:(NSTimer*)timer
- (IBAction) startTimer:(id) sender;





Interface builder changes

I added a label and placed a custom button to intercept actions to the label.  I could have actually implemented touches, but this was easier than a gesture controller.  I then hooked my button the callback to start the timer.
Note that the invisible button is currently highlighted.  Setting it to custom causes it to be hidden, but events still occur.
The implementation of the timer start

Toggle the timer.  Check to see if it is running, if it is then reset the label, and release the timer (by setting the property to null which causes it to be released.


- (void) startTimer:(id) sender
{
if (timerStart!=nil)
{
timerStart=nil;
self.timer=nil;
self.timerLabel.text=@"1:00";
}
else {

self.timerStart=[[NSDate alloc]init];
self.timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateTime:) userInfo:nil repeats:YES];
}
}



The callback for counting down


Note the use of floor, without using floor it instantly goes down to 59 when the timer starts, and will reach the end early.


NSDate *current=[[NSDate alloc]init];
int timeLeft=60-floor([current timeIntervalSinceDate:timerStart]);
if (timeLeft>0)
{
[self.timerLabel setText:[NSString stringWithFormat:@":%0d",timeLeft]];
}
else {
self.timer=nil;
}

Thursday, May 6, 2010

IPad Math: Fixing the code, and advancing through paths.

The next step is to make a progressive ability to represent the states of the items.  I've also added some highlighting ability.


  1. Modify the MathProblem class to have problemNumber property for which particular index of problems it is.
  2. Modify MathDrillViewController so that it has a currentProblem integer value, as well as an array of problems.
  3. Add a set problem method that sets the current problem

In this code segment the setProblemNumber will find the previous problem and reset it's alpha to 0.5.  It then set the current problem and changes the alpha to 1.  Note that I need to refractor this code a bit.
- (void) setProblemNumber:(int) problemNumber
{
NSLog(@"Choosing problem %d",problemNumber);
if ((currentProblem>=0) && (currentProblem<=[currentProblems count]))
{
MathProblem *oldProblem=(MathProblem*)[currentProblems objectAtIndex:currentProblem];

oldProblem.view.alpha=0.5;
}
currentProblem=problemNumber;
MathProblem *problem=(MathProblem*)[currentProblems objectAtIndex:currentProblem];
problem.view.alpha=1;
}


In this code segment I modified the math problem creation to load an array, and set the alpha to 0.5
- (void)viewDidLoad {
    [super viewDidLoad];
int width=self.view.frame.size.width/6;
int height=(self.view.frame.size.height-100)/5;

currentProblems=[[NSMutableArray alloc]init];
for (int row=0;row<5;row++)
{
for (int column=0;column<6;column++)
{
MathProblem *problem=[[MathProblem alloc] initWithMax:10];
problem.problemNumber=row*6+column;
problem.view.alpha=0.5;
[self.currentProblems addObject:problem];

problem.view.frame=CGRectMake(column*width, height*row, width, height);
problem.controller=self;
// problem.view.frame=CGRectMake(40, 40, 100, 100);
[self.view addSubview:problem.view];
}
}

int buttonWidth=resultChoices.frame.size.width/10;
int buttonHeight=resultChoices.frame.size.height;
resultChoices.userInteractionEnabled=true;
self.view.userInteractionEnabled=true;
 
for (int i=0;i<10;i++)
{
UIButton *button=[[[UIButton alloc]init] retain];
button.tag=i;
button.frame=CGRectMake(i*buttonWidth,buttonHeight+resultChoices.frame.origin.y,buttonWidth,buttonHeight);
button.backgroundColor=[UIColor redColor];
button.userInteractionEnabled=true;
[button setTitle:[NSString stringWithFormat:@"%d",i] forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonChosen:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
}


In this code segment I change the behavior of the select a number to increment to the next problem.



- (IBAction) buttonChosen:(id)sender
{
UIButton *button=(UIButton*) sender;
int choice=button.tag;
if ((currentProblem>=0) && (currentProblem<= [currentProblems count]))
{
MathProblem *problem=(MathProblem*)[currentProblems objectAtIndex:currentProblem];

[problem setChoice:choice];
NSLog(@"setting choice to %d",choice);

[self setProblemNumber:(currentProblem+1)%([currentProblems count])];
}
}

Wednesday, May 5, 2010

The analyzer and adding subviews: unrecognized selector sent to instance

I ran the analyzer on some code and found a 'leak'. I added a release and then stuff started to crash.  The problem is that the analyzer didn't realize that by adding my view as a subview resulted in a retain.   Adding a release caused the item to get released, and a crash.

I had made some other changes, so I didn't realize what was causing the problem.

The controller was being released, but I still needed to keep it around, because releasing it caused the view to be released.  I need to maintain a collection for the controllers anyway, so that will solve the problem.

Tuesday, May 4, 2010

IPad Math: The Code -- The controller.

The controller will go through and calculate where the views for the math problem should exist.  It then uses the extent of the view to calculate where the various items should go.  Note that it currently does not properly support rotation.  In addition I originally had a placeholder view that would contain the buttons, but for some reason I wasn't getting events, even though I turned on UI interaction for that view.


- (void)viewDidLoad {
    [super viewDidLoad];
int width=self.view.frame.size.width/6;
int height=(self.view.frame.size.height-100)/5;

for (int row=0;row<5;row++)
{
for (int column=0;column<6;column++)
{
MathProblem *problem=[[MathProblem alloc] initWithMax:10];
problem.view.frame=CGRectMake(column*width, height*row, width, height);
problem.controller=self;
// problem.view.frame=CGRectMake(40, 40, 100, 100);
[self.view addSubview:problem.view];
}
}

int buttonWidth=resultChoices.frame.size.width/10;
int buttonHeight=resultChoices.frame.size.height;
resultChoices.userInteractionEnabled=true;
self.view.userInteractionEnabled=true;
 
for (int i=0;i<10;i++)
{
UIButton *button=[[[UIButton alloc]init] retain];
button.tag=i;
button.frame=CGRectMake(i*buttonWidth,buttonHeight+resultChoices.frame.origin.y,buttonWidth,buttonHeight);
button.backgroundColor=[UIColor redColor];
button.userInteractionEnabled=true;
[button setTitle:[NSString stringWithFormat:@"%d",i] forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonChosen:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
}

- (IBAction) buttonChosen:(id)sender
{
UIButton *button=(UIButton*) sender;
int choice=button.tag;
if (currentProblem!=nil)
{
[currentProblem setChoice:choice];
}
}