Welcome Back Everyone!  This is the second tutorial of three that will delve into the inner workings of iOS Game Development and (hopefully) show you everything there is to learn about designing and developing an iOS Game from the ground-up.

In this tutorial, I’m going to show you how to design an in-app settings page, including UISegmentedControllers, Switches, Timers, and NSUserDefaults, and incorporate a reset button.  Instead of showing you sound effects in this tutorial, I decided to save them for the last tutorial (Because really, who needs sound effects…).

So without further (witty) parenthetical statements, let’s get started!

In the first tutorial, we designed the application to shuffle the puzzle, move the pieces around until the puzzle completes, and then show an alert letting the user know they finished.  In this tutorial, we will design the in-app settings page, using NSUserDefaults as our way to save the settings, incorporate a reset button to re-shuffle the puzzle, and even allow the user to count the time and number of moves it takes to complete the puzzle.  Easy enough.  Let’s get started.

Step 1: Write the AppDelegate.m

The first thing you’re going to want to do is add this to the AppDelegate.m applicationdidfinishlaunching method:

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

    if ([prefs objectForKey:@"PuzzlePicture"] == nil) {
        [prefs setBool:FALSE forKey:@"Refresh"];
        [prefs setInteger:0 forKey:@"PuzzlePicture"];
        [prefs setBool:TRUE forKey:@"CountMoves"];
        [prefs setBool:TRUE forKey:@"Timer"];
        [prefs setInteger:1 forKey:@"PuzzleLayoutX"];
        [prefs setInteger:1 forKey:@"PuzzleLayoutY"];
    }

The first thing that we’re doing here is setting up the default settings for our preference keys using NSUserDefaults.  If one of the preferences is empty, then it can generally be assumed that the rest of the keys will also, likely, be empty and will need something to default to.  Without getting into too much detail, these settings will be the keys that we will work with throughout the rest of the tutorial and will be included into our in-app settings view.  Don’t fret too much over this part if you’re having trouble, it will become more clear as we carry on.

Step 2: Write the FirstViewController.h

The next thing you’re going to do is add this to your firstviewcontroller.h:

NSTimer *timer;

We’re setting the stage to incorporate a timer into the application in order to time the user to see how long they take to finish the puzzle.

Step 3: Write the FirstViewController.m

Now, add these global variables to your firstviewcontroller.m underneath @Synthesize:

int countmove = 0;
int thetime = 0;

Again, these integers are just setting the stage for counting the time and amount of moves it takes to complete the puzzle.

Now we get into the good stuff.  Replace this code in your firstviewcontroller.m viewdidload method:

self.tiles = [[NSMutableArray alloc] init];

    NSString *Pic = @"picture1.png";
    [self initPuzzle:Pic];

With this:

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

    self.tiles = [[NSMutableArray alloc] init];

	NSString *Pic = [NSString stringWithFormat:@"picture%d.png", [prefs integerForKey:@"PuzzlePicture"]];
    [self initPuzzle:Pic];

This code will utilize your NSUserDefault preferences and allow you to change the puzzle picture whenever the application first opens, provided the name of the picture is “picture” with a number on the end.

After that, add this inside your initPuzzle function:

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
if (tileImageView != nil && [prefs boolForKey:@"Refresh"] == TRUE) {
        for (tileImageView in tiles) {
            [tileImageView removeFromSuperview];
        }
    }

[prefs setBool:FALSE forKey:@"Refresh"];

This code will also utilize your preferences and allow your puzzle to reset whenever the refresh key is activated.  The tiles must be removed from the superview in order for the puzzle to be reset.

Your initpuzzle function should now look like this:

-(void) initPuzzle:(NSString *) imagePath{
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
	UIImage *orgImage = [UIImage imageNamed:imagePath];

	if( orgImage == nil ){
		return; 
	}

    if (tileImageView != nil && [prefs boolForKey:@"Refresh"] == TRUE) {
        for (tileImageView in tiles) {
            [tileImageView removeFromSuperview];
        }
    }

	[self.tiles removeAllObjects];

	tileWidth = orgImage.size.width/NUM_HORIZONTAL_PIECES;
	tileHeight = orgImage.size.height/NUM_VERTICAL_PIECES;

	blankPosition = CGPointMake( NUM_HORIZONTAL_PIECES-1, NUM_VERTICAL_PIECES-1 );

	for( int x=0; x<NUM_HORIZONTAL_PIECES; x++ ){
		for( int y=0; y<NUM_VERTICAL_PIECES; y++ ){
			CGPoint orgPosition = CGPointMake(x,y); 

			if( blankPosition.x == orgPosition.x && blankPosition.y == orgPosition.y ){
				continue; 
			}

			CGRect frame = CGRectMake(tileWidth*x, tileHeight*y, 
									  tileWidth, tileHeight );
			CGImageRef tileImageRef = CGImageCreateWithImageInRect( orgImage.CGImage, frame );
			UIImage *tileImage = [UIImage imageWithCGImage:tileImageRef];

			CGRect tileFrame =  CGRectMake((tileWidth+TILE_SPACING)*x, (tileHeight+TILE_SPACING)*y, 
										   tileWidth, tileHeight );

			tileImageView = [[Tile alloc] initWithImage:tileImage];
			tileImageView.frame = tileFrame;
			tileImageView.originalPosition = orgPosition;
			tileImageView.currentPosition = orgPosition;

			CGImageRelease( tileImageRef );

			[tiles addObject:tileImageView];

			// now add to view
			[self.view insertSubview:tileImageView atIndex:0];
			[tileImageView release];
		}
	}

	[self shuffle];
    [prefs setBool:FALSE forKey:@"Refresh"];
}

Now that you have that down, you’re going to completely replace everything in your touchesEnded function with this:

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
	UITouch *touch = [touches anyObject];
    CGPoint currentTouch = [touch locationInView:self.view];	

	Tile *t = [self getPieceAtPoint:currentTouch];
	if( t != nil ){
        //Start the game timer
        if (timer == nil && [prefs boolForKey:@"Timer"] == TRUE) {
            timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(onTimer) userInfo:nil repeats:YES];
        }
        //Move the pieces
		[self movePiece:t withAnimation:YES];
        //Count the moves
        if ([prefs boolForKey:@"CountMoves"] == TRUE) {
            countmove++;
        }
		if( [self puzzleCompleted] ){
            NSString *Winning;
            if ([prefs boolForKey:@"CountMoves"] == TRUE && [prefs boolForKey:@"Timer"] == TRUE) {
                Winning = [NSString stringWithFormat:@"It took you: %i Moves in %i Seconds!", countmove, thetime];
            } else if ([prefs boolForKey:@"CountMoves"] == TRUE && [prefs boolForKey:@"Timer"] == FALSE) {
                Winning = [NSString stringWithFormat:@"It took you: %i Moves", countmove];
            } else if ([prefs boolForKey:@"CountMoves"] == FALSE && [prefs boolForKey:@"Timer"] == FALSE) {
                Winning = [NSString stringWithFormat:@"Great Job!"];
            } else if ([prefs boolForKey:@"CountMoves"] == FALSE && [prefs boolForKey:@"Timer"] == TRUE) {
                Winning = [NSString stringWithFormat:@"It took you: %i Seconds", thetime];
            }

            UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"You Won!"
                                                              message:Winning
                                                             delegate:nil
                                                    cancelButtonTitle:@"OK"
                                                    otherButtonTitles:nil];

            [message show];
            [message release];
            countmove = 0;
            thetime = 0;
            if (timer != nil) {
                [timer invalidate];
                timer = nil;
            }
	}
}

That’s a lot of new code…  Let’s break it down.  The first thing you’re going to do is instantiate a new instance of NSUserDefaults (necessary for accessing our preferences).  Next, we check to see if the timer preference is enabled – allowing us to time the game.  If it is, we schedule the timer which we set up in our .h to run by the function name “onTimer” every one second.  After that, we move the piece the user touched and then add the move to our counter if the counting preference is on.  Next, we check to see if the puzzle is completed, and, if so, we base the output of the alert to show the timer and the counter, either one, or neither, based on the preferences.  Last, we set the timer to nil and zero out the integers holding the number of seconds passed and the counter.  That was a lot, but I don’t think it was that bad.

Now that we’ve done all that, add this function underneath the ontouches function:

- (void)onTimer {
    thetime++;
}

This code will increment the timer by 1 every time a second has passed – very simple.

Now let’s do one last thing in the firstviewcontroller.m.  We’re going to change the flipsideviewcontrollerDidFinish function to update the settings whenever the flipsideview goes away and the firstviewcontroller (puzzle) view comes up.  Change the flipsideviewcontrollerDidFinish function to this:

[self dismissModalViewControllerAnimated:YES];

    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

    //Perform Settings Changes to the Main View
    switch ([prefs integerForKey:@"PuzzleLayoutX"]) {
        case 0:
            NUM_HORIZONTAL_PIECES = 2;
            break;
        case 1:
            NUM_HORIZONTAL_PIECES = 3;
            break;
        case 2:
            NUM_HORIZONTAL_PIECES = 4;
            break;
        case 3:
            NUM_HORIZONTAL_PIECES = 5;
            break;
        default:
            break;
    }

    switch ([prefs integerForKey:@"PuzzleLayoutY"]) {
        case 0:
            NUM_VERTICAL_PIECES = 2;
            break;
        case 1:
            NUM_VERTICAL_PIECES = 3;
            break;
        case 2:
            NUM_VERTICAL_PIECES = 4;
            break;
        case 3:
            NUM_VERTICAL_PIECES = 5;
            break;
        default:
            break;
    }

    if ([prefs boolForKey:@"Refresh"] == TRUE) {
        countmove = 0;
        thetime = 0;
        if (timer != nil) {
            [timer invalidate];
            timer = nil;
        }
        NSString *Pic = [NSString stringWithFormat:@"picture%d.png", [prefs integerForKey:@"PuzzlePicture"]];
        [self initPuzzle:Pic];
    }

It looks like a lot, but it’s really not.  We want to make sure that whenever the settings view goes away and the puzzle view comes back to the front, that the NSUserDefault settings are all updated.  In order to do that, we need to reestablish what they’re set to and act accordingly.  We first setup a switch to get the integer value of the number of horizontal pieces in the puzzle, then the number of vertical pieces.  Next, we check to see if the user wants the puzzle reshuffled – if so, zero out the timer and number of moves, then reinitialize the puzzle with the picture that the user wants.  Not too bad at all.  And that’s all there is to it…for the firstviewcontroller.  Now on to the flipsideview (settings view).

Step 4: Write the FlipSideViewController.h

Now let’s start working on the FlipSideViewController.  Add all of this to your flipsideviewcontroller.h file:

IBOutlet UIBarButtonItem *Refresh;
    IBOutlet UISegmentedControl *PuzzlePicture;
    IBOutlet UISwitch *CountMoves;
    IBOutlet UISwitch *Timer;
    IBOutlet UISegmentedControl *PuzzleLayoutX;
    IBOutlet UISegmentedControl *PuzzleLayoutY;
}
@property (retain, nonatomic) IBOutlet UIBarButtonItem *Refresh;
@property (retain, nonatomic) IBOutlet UISegmentedControl *PuzzlePicture;
@property (retain, nonatomic) IBOutlet UISwitch *CountMoves;
@property (retain, nonatomic) IBOutlet UISwitch *Timer;
@property (retain, nonatomic) IBOutlet UISegmentedControl *PuzzleLayoutX;
@property (retain, nonatomic) IBOutlet UISegmentedControl *PuzzleLayoutY;

@property (assign, nonatomic) IBOutlet id  delegate;

- (IBAction)done:(id)sender;
- (IBAction)Refreshed:(id)sender;
- (IBAction)PuzzlePictured:(id)sender;
- (IBAction)CountMoved:(id)sender;
- (IBAction)Timered:(id)sender;
- (IBAction)PuzzleLayoutedX:(id)sender;
- (IBAction)PuzzleLayoutedY:(id)sender;

I don’t want to bore you, so I’ll simply explain that we’re going to create several buttons, switches, and segmented controllers to allow the user to customize almost every aspect of the puzzle.  Plus, their respective IBActions to update the preferences when they change.

*Make sure if you’re following along on your own project, that you change the “SliderPuzzleFlipsideViewControllerDelegate” to whatever your FlipsideViewControllerDelegate is called or else you’ll get some errors.*

Step 5: Write the FlipSideViewController.m

After that, add all of this into your .m file (don’t worry, we’ll go over everything):

@synthesize Refresh;
@synthesize PuzzlePicture;
@synthesize CountMoves;
@synthesize Timer;
@synthesize PuzzleLayoutX;
@synthesize PuzzleLayoutY;
@synthesize delegate = _delegate;

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

    switch ([prefs integerForKey:@"PuzzlePicture"]) {
        case 0:
            PuzzlePicture.selectedSegmentIndex = 0;
            break;
        case 1:
            PuzzlePicture.selectedSegmentIndex = 1;
            break;
        case 2:
            PuzzlePicture.selectedSegmentIndex = 2;
            break;
        case 3:
            PuzzlePicture.selectedSegmentIndex = 3;
            break;
        default:
            break;
    }

    if ([prefs boolForKey:@"CountMoves"] == TRUE) {
        CountMoves.on = TRUE;
    } else {
        CountMoves.on = FALSE;
    }

    if ([prefs boolForKey:@"Timer"] == TRUE) {
        Timer.on = TRUE;
    } else {
        Timer.on = FALSE;
    }

    switch ([prefs integerForKey:@"PuzzleLayoutX"]) {
        case 0:
            PuzzleLayoutX.selectedSegmentIndex = 0;
            break;
        case 1:
            PuzzleLayoutX.selectedSegmentIndex = 1;
            break;
        case 2:
            PuzzleLayoutX.selectedSegmentIndex = 2;
            break;
        case 3:
            PuzzleLayoutX.selectedSegmentIndex = 3;
            break;
        default:
            break;
    }

    switch ([prefs integerForKey:@"PuzzleLayoutY"]) {
        case 0:
            PuzzleLayoutY.selectedSegmentIndex = 0;
            break;
        case 1:
            PuzzleLayoutY.selectedSegmentIndex = 1;
            break;
        case 2:
            PuzzleLayoutY.selectedSegmentIndex = 2;
            break;
        case 3:
            PuzzleLayoutY.selectedSegmentIndex = 3;
            break;
        default:
            break;
    }
}

#pragma mark - Actions

- (IBAction)done:(id)sender
{
    [self.delegate flipsideViewControllerDidFinish:self];
}

- (IBAction)Refreshed:(id)sender {
     NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    [prefs setBool:TRUE forKey:@"Refresh"];
}

- (IBAction)PuzzlePictured:(id)sender {
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    switch (PuzzlePicture.selectedSegmentIndex) {
        case 0:
            [prefs setInteger:0 forKey:@"PuzzlePicture"];
            break;
        case 1:
            [prefs setInteger:1 forKey:@"PuzzlePicture"];
            break;
        case 2:
            [prefs setInteger:2 forKey:@"PuzzlePicture"];
            break;
        case 3:
            [prefs setInteger:3 forKey:@"PuzzlePicture"];
            break;
        default:
            break;
    }
}

- (IBAction)CountMoved:(id)sender {
     NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    if (CountMoves.on == TRUE) {
        [prefs setBool:TRUE forKey:@"CountMoves"];
    } else {
        [prefs setBool:FALSE forKey:@"CountMoves"];
    }
}

- (IBAction)Timered:(id)sender {
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    if (Timer.on == TRUE) {
        [prefs setBool:TRUE forKey:@"Timer"];
    } else {
        [prefs setBool:FALSE forKey:@"Timer"];
    }
}

- (IBAction)PuzzleLayoutedX:(id)sender {
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    switch (PuzzleLayoutX.selectedSegmentIndex) {
        case 0:
            [prefs setInteger:0 forKey:@"PuzzleLayoutX"];
            break;
        case 1:
            [prefs setInteger:1 forKey:@"PuzzleLayoutX"];
            break;
        case 2:
            [prefs setInteger:2 forKey:@"PuzzleLayoutX"];
            break;
        case 3:
            [prefs setInteger:3 forKey:@"PuzzleLayoutX"];
            break;
        default:
            break;
    }
}

- (IBAction)PuzzleLayoutedY:(id)sender {
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    switch (PuzzleLayoutY.selectedSegmentIndex) {
        case 0:
            [prefs setInteger:0 forKey:@"PuzzleLayoutY"];
            break;
        case 1:
            [prefs setInteger:1 forKey:@"PuzzleLayoutY"];
            break;
        case 2:
            [prefs setInteger:2 forKey:@"PuzzleLayoutY"];
            break;
        case 3:
            [prefs setInteger:3 forKey:@"PuzzleLayoutY"];
            break;
        default:
            break;
    }

}

- (void)viewDidUnload
{
    [Refresh release];
    Refresh = nil;
    [self setRefresh:nil];
    [PuzzlePicture release];
    PuzzlePicture = nil;
    [self setPuzzlePicture:nil];
    [CountMoves release];
    CountMoves = nil;
    [self setCountMoves:nil];
    [Timer release];
    Timer = nil;
    [self setTimer:nil];
    [PuzzleLayoutX release];
    PuzzleLayoutX = nil;
    [self setPuzzleLayoutX:nil];
    [PuzzleLayoutY release];
    PuzzleLayoutY = nil;
    [self setPuzzleLayoutY:nil];
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
	[super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
	[super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

- (void)dealloc {
    [Refresh release];
    [PuzzlePicture release];
    [CountMoves release];
    [Timer release];
    [PuzzleLayoutX release];
    [PuzzleLayoutY release];
    [super dealloc];
}

Good graciousness, that is a ton of code!  Fortunately for us, it is very easy to comprehend.  The first thing you’re doing here is synthesizing your IBOutlets.  Next, in the viewdidload function, you’re setting the state of all the buttons to what their respective values are in NSUserDefaults.  In the next couple IBAction functions, you’re changing the value of all the preferences whenever the user clicks on one of the buttons and changes it (awesome!).  And lastly, you have to release everything you synthesized (common sense).  And that’s that.

Step 6: Connect the IBOutlets

Congratulations!  You’ve finished all the coding for this tutorial, now for the IBActions…  Link up all of the IBButtons and IBActions in Xcode by dragging them from the .h file or by linking them one-by-one in interface builder.  Feel free to mess with the sizes, change the title of the navigation bar, or add your own personal touch to everything (make it unique!).  It doesn’t have to look perfect, but spend some time and make sure everything is aligned and aesthetically pleasing.

Step 7: Add some pictures

The final step is to add your own pictures.  Choose anything you’d like, just make sure to resize it to 320×460 and make sure it’s in .png format.  Here are some samples:

Step 8: Build & Go!

That’s it!!!  You’ve just completed the second Slider Puzzle Game tutorial!!  Thanks for coming, and, as always, feel free to email or comment if you have any comments, questions, or concerns.  Thanks for reading and stay tuned for our next tutorial!!!

You can download the sample project here:

http://shmoopi.net/Slider%20Puzzle%202.zip

-Shmoopi