You are on page 1of 7

Project 1 - 15 Puzzle

CS 458

1 Introduction
For this project, you will create an iOS Fifteen Puzzle app: https://en.wikipedia.org/wiki/15_puzzle
Section 2 describes how to use Interface Builder (IB) editor to layout your view
Application components
Delegate Class and program-
matically control the size and position of the tiles. The methods for View
the internal game logic (which you will
Controller Class
need to implement) are specified in Section 3. The details of the Model
View View
SubclassController
holding tile (MVC)
buttons design and
Model and
the flow of events is outlined in Section 5.1. Information about grading Classsome
holdingopportunities
puzzle state for bonus
Storyboard holding
points appear in Section 6. How to submit your solution is specified serialized
in Section 7.
UI components.
image asset catalog

1.1 Creating the Project


launch image
stortboard
This project will use the iOS “Single View App” template in Xcode. Use the product name FifteenPuzzle.
You may have Xcode build a git repository for you, if you like. Xcode should also be able to connect to a
remote repo on the ENCS gitlab instance. Figure 2: Content of project.

2 Laying 2OutLaying
the User
out Interface
the User Interface

status bar Top Layout Guide


UIView
BoardView
UIButton
1 2 3 4
UIView (main view)
5 6 7 8 BoardView (custom view)

9 10 11 12 UIButton (1)
UIButton (2)
13 14 15 …

UIToolBar
UIBarButtonItem
shuffle UIToolBar 44

all spaces = 0 IB Screenshot


UIBarButtonItem

Figure 3: Layout and Hierarchy of Views


Figure 1: Layout and Hierarchy of Views

2.1 Adding Views and Controls


2.1 Adding Views and Controls
Edit the storyboard by adding a UIToolBar at the bottom and another UIView just above it as illustrated
The interface for this project3.will
in Figure Pin consist of, at minimum,
the leading, bottom, an additional
and UIViewofreferred
trailing edges to here
the toolbar toasthe
BoardView,
left, bottom, and trailing
a UIToolBar withedges of the main view; fix its height to the standard of 44 pts. Pin all fourlayout.
UIBarButtonItem, and a total of 15 UIButtons. See Figure 1 for the overall of thePin
edges of the added
the leading, trailing, and (BoardView
subview bottom edges in of
thethe toolbar
figure) to thethere
so that corresponding
is no spaceedges of the
between anysafe view,
of the and fix
corresponding neighboring
edges. The top edge should actually be just below the status bar so that it is aligned with the “Top Layout
Guide.” All the spaces represented by 1 by I-beams on the left of Figure 3 should be zero. Add 15 UIButton’s
to the BoardView and set their titles to the values 1 through 15 and adjust the font to taste. Do not set
any constraints for the fifteen buttons – we are going to override the size and position programmatically as
described in Section 4. Use the Attributes Inspector to set their view tags to 1 through 15 so we can identify
each button in our code. The toolbar on the right of Figure 3 also shows an added Flexible Space and Bar
Button for a bonus feature of the project.
its height to 44 pts. Pin all edges of BoardView so there is no space between it and the corresponding
neighboring edges. (Use the safe view for top and sides, and the toolbar on the bottom.) Spaces represented
by I-beams in the figure should all be zero.
Add the buttons to the BoardView, selecting colors so we can see them on the board view (e.g. make
the buttons white and the board grey). Set the titles of the buttons to the values 1 through 15, and adjust
their fonts to taste. Do not set any constraints on the buttons – we are going to set their size and position
programmatically, as described in Section 4. Use the Attributes Inspect to set their view tags to 1 through
15 (matching their titles) so we can identify the buttons in our code.

2.2 Creating a Custom BoardView


In order to override UIView’s layout mechanism, we create a custom subclass named BoardView. From the
Xcode menu choose File → New → File and choose the iOS / Source / Cocoa Touch Class template in the
presented dialog. Press Next and name the class BoardView and make it a subclass of UIView (do not create
a XIB file; use Swift as the language). You should see the file BoardView.swift in the Navigator now. Using
the Identity Inspector, change the class of the appropriate UIView object (labeled BoardView in Figure 1)
to BoardView. Viola! This object is now an instance of your new class.

2.3 Adding a boardView outlet


Create an outlet so the controller can access the tiles of our board view:
@IBOutlet weak var boardView: BoardView!
You can do this with the Assistant Editor using the control-click-and-drag magic shown in class. Alterna-
tively, you could just add the property above to ViewController.swift and use the Connections Inspector
in IB to connect the boardView outlet with this property.

2.4 Adding action methods


We need to create appropriate callback methods in the ViewController class that will be invoked when the
user selects a tile or wants a new game. You can either use the Assistant Editor to set these up, or create
the methods and set them up with the Connection Inspector. Here are suitable prototypes:

@IBAction func tileSelected(_ sender: UIButton) { ... }


@IBAction func shuffleTiles(_ sender: AnyObject) { ... }

3 The Model
Define a class FifteenBoard that will encapsulate the puzzle’s logic and support the methods below. (This
will be a normal Swift file.) The puzzle tiles are encoded as integers from 1 to 15 and the space is represented
with a zero. The puzzle tiles are assumed to lie in a 4 × 4 grid with one empty space, which I encode as a
2D array:
class FifteenBoard {
var state : [[Int]] = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 0] // 0 => empty slot
]
...
}

2
func scramble(numTimes n: Int) Choose one of the “slidable” tiles at random and slide it into the
empty space; repeat n times. We use this method to start a new game using a large value (e.g., 150)
for n.
func getTile(atRow r: Int, atColumn c: Int) -> Int Fetch the tile at the given position (0
is used for the space).
func getRowAndColumn(forTile tile: Int) -> (row: Int, column: Int)? Find the position
of the given tile (0 is used for the space) – returns tuple holding row and column.
func isSolved() -> Bool Determine if puzzle is in solved configuration.
func canSlideTileUp(atRow r : Int, Column c : Int) -> Bool Determine if the specified tile
can be slid up into the empty space.
func canSlideTileDown(atRow r : Int, Column c : Int) -> Bool
func canSlideTileLeft(atRow r : Int, Column c : Int) -> Bool
func canSlideTileRight(atRow r : Int, Column c : Int) -> Bool
func canSlideTile(atRow r : Int, Column c : Int) -> Bool
func slideTile(atRow r : Int, Column c: Int) Slide the tile into the empty space, if possible.
It is important that the positions of the tiles are not simply chosen at random since you may create an
unsolvable puzzle. Instead, “scramble” the tiles of a puzzle that we know is solvable when the user wants to
start a new game.

3.1 Creating the Model Instance


We create a singleton instance of FifteenBoard class to hold the state of the puzzle. Since both the Board
View and the View Controller will want access to this object, let the AppDelegate object own it:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var board : FifteenBoard?
let numShuffles = 100 // used to scramble board
...
}
Create and initialize the the board when the application finishes launching:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.board = FifteenBoard()
self.board!.scramble(numTimes: numShuffles)
return true
}
The app delegate exists for the entire lifetime of the app, so it is the only entity that needs to “own”the
board object (i.e., uses strong reference). Placing the board object in the application delegate also makes it
easy to save and load the state of the puzzle if you choose to add data persistence (as a bonus feature). We
can obtain a reference to the “global” board object anywhere in our program as follows:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let board = appDelegate.board

3
4 Sizing and Positioning the Tile Buttons

override func layoutSubviews() {


super.layoutSubviews() // let autolayout engine finish first

let appDelegate = UIApplication.shared.delegate! as! AppDelegate


let board = appDelegate.board // get model from app delegate

let boardSquare = boardRect() // determine region to hold tiles (see below)


let tileSize = boardSquare.width / 4.0
let tileBounds = CGRect(x: 0, y: 0, width: tileSize, height: tileSize)

for r in 0 ..< 4 { // manually set the bounds, and of each tile


for c in 0 ..< 4 {
let tile = board!.getTile(atRow: r, atColumn: c)
if tile > 0 {
let button = self.viewWithTag(tile)
button!.bounds = tileBounds
button!.center = CGPoint(x: boardSquare.origin.x + (CGFloat(c) + 0.5)*tileSize,
y: boardSquare.origin.y + (CGFloat(r) + 0.5)*tileSize)

}
}
}
}

Figure 2: BoardView method that overrides UIViews layoutSubviews method to position the tile buttons
to reflect the state of the board model.

We want to programmatically control the size and positions of the tile buttons so we override the values
computed by the AutoLayout engine. This is accomplished by overriding UIView’s layoutSubviews method
as listed in Figure 2. Once the AutoLayout is finished, we immediately reset by assigning to desired values
of the bounds and center properties of each button. 1

func boardRect() -> CGRect { // get square for holding 4x4 tiles buttons
let W = self.bounds.size.width
let H = self.bounds.size.height
let margin : CGFloat = 10
let size = ((W <= H) ? W : H) - 2*margin
let boardSize : CGFloat = CGFloat((Int(size) + 7)/8)*8.0 // next multiple of 8
let leftMargin = (W - boardSize)/2
let topMargin = (H - boardSize)/2
return CGRect(x: leftMargin, y: topMargin, width: boardSize, height: boardSize)
}

Figure 3: Auxiliary method that determines the largest square that fits in the center of the board view. We
make sure the board size of is a multiple of 8 so the tile centers lie on the integer grid.

We first determine a suitably sized square in the center of the BoardView region where the tiles will be
placed using the helper method boardRect listed in Figure 3. We then consult the board object to determine
1 Note
that we set the bounds and center properties rather than the frame property for each button since setting the frame
property is ignored if the transformation property is not the identity (see Apple’s UIView documentation).

4
We first determine a suitably sized square in the center of the BoardView region where the tiles will be
placed using the helper method boardRect listed in Figure 5. We then consult the board object to determine
the row and column of each tile in the puzzle. From this we determine the appropriate position and location
of each button.

5 User Interaction
5.1 Model View Controller
Model-View-Controller

CONTROLLER
ViewController
boardView
outlet
board (from app delegate)
tileSelected:
shuffleTiles:

tar
FifteenBoard

g
var state : [[Int]]

et/a
init
getTile(atRow:,atColumn:)
getRowAndColumn(forTile:)

ctio
isSolved
canSlideTileUp(atRow:Column:)

n
canSlideTileDown(atRow:Column:)
canSlideTileLeft(atRow:Column:)
canSlideTileRight(atRow:Column:)
slideTile(atRow:Column:)
scramble(numTimes:)

MODEL

VIEW
Figure 6: MVC elements of 15-puzzle app. The View objects are stored in the NIB file within a storyboard.
Figure 4: MVC
When the NIB is loaded, the boardView outletelements of the 15 isPuzzle
of the Controller boundApp to the associated BoardView and
the various UIButton target/actions are created. The Controller accesses an instance of FiftenBoard stored
in the app 4delegate
Figure (thethe
illustrates sole Model
main object) which
components of theencapsulates
15 Puzzle App theclassified
game’s logic.
according to the MVC pattern.
The UIButtons and UIViews that are archived and reanimated at run time from the storyboard file are
shown on the
Figure right. The
6 illustrates thecontroller referencesofthe
main components 15-Puzzle via
theboardView Appanclassified
IB outletaccording
so that ittocan
themanipulate
MVC pattern. the
tiles on the board. The target/action mechanism is used to inform the controller when
The UIButton’s and UIView’s that are archived and reanimated at run time from the NIB file are shown on the user initiates
an event.
the The controller
right. The controller references
accesses the
thesole model object
boardView via anstored in the
IB outlet app it
so that delegate which encodes
can manipulate theonstate
the tiles the
of the puzzle and provides methods for querying and modifying the board. The model
board. The target/action mechanism is used to inform the controller when the user initiates an event. is not tied to The
any
specific user
controller interface
accesses the and
sole could
modelbe usedstored
object in another
in theapplication with
app delegate a different
which encodesuser
the interface.
state of the puzzle and
provides methods for querying and modifying the board. The model is not tied to any specific user interface
and
5.2 could be usedTiles
Sliding in another application with a di↵erent user interface.
Figure 5 shows the sequence of events that are triggered on a “touch-up” event when the user touches tile
5.2
14. WeSliding
wired thisTiles
event to send a tileSelected: message to the Controller (1). Whenever the user touches
a tile we
Figure consultthe
7 shows thesequence
board object to determine
of events if there isona aneighboring
that are triggered “touch-up”empty space the
event when for user
the tile to slide
touches tileinto
12.
(2). wired
We If so, this
we update
event tothe board
send state (3) and move
a tileSelected: messagethe to
button accordingly
the controller. (4). Thisthe
Whenever is accomplished
user touches awithtile
theconsult
we action method
the boardlisted in Figure
object 6. We ifsimply
to determine there move the buttonempty
is a neighboring by changing changing
space for the tileits
to center property.
slide into. If so,
Rather the
we update thanboard
move state
the tile
anddirectly
move theto its new accordingly.
button location, we This
can animate the change
is accomplished withbythe
replacing the line
action method
that moves
listed the button’s
in Figure centermove
8. We simply with the button by changing changing its center property. It is amazingly
simple to show the button sliding by animating the change of the button property as follows:
UIView.animateWithDuration(0.5, animations: {sender.center = buttonCenter})
UIView.animateWithDuration(0.5, animations: {sender.center = buttonCenter})
This uses a Swift closure for the animations: argument which sets the center property of the view. (All of
the UIKit views are backed by a Core Animation layer and it is simple to animate many of their properties.)

5
CONTROLLER
ViewController
boardView
2 board (from app delegate)
tileSelected:
shuffleTiles:
1

3 4

FifteenBoard
var state : [[Int]]
init
getTile(atRow:,atColumn:)
getRowAndColumn(forTile:)
isSolved
canSlideTileUp(atRow:Column:)
canSlideTileDown(atRow:Column:)
canSlideTileLeft(atRow:Column:)
canSlideTileRight(atRow:Column:)
slideTile(atRow:Column:)
scramble(numTimes:)

MODEL

VIEW
Figure 7: Figure
Communication sequence sequence
5: Communication triggeredtriggered
when thewhen
user the
“touches up” on up”
user “touches the 14-tile: (1) the button
on the 14-tile
sends a tileSelected: message to the Controller; (2) The Controller (using the sender’s tag = 14) queries
the Model for the column and row of the corresponding button; The Controller then queries the Model to
determine which direction the tile can slide (if any); Since it was determined that the tile can slide down,
5.3 Shuffling Tiles
(3) the Controller tells the Model to slide the tile and (4) moves the View’s button by altering it’s center
property.
When the user chooses to shuffle the tiles to start over, we simply tell the model to scramble the tiles and
inform the boardView that it needs to layout the buttons again using the action method listed in Figure 7.
Note that we do not invoke boardView’s layoutSubviews method directly – we simply mark the boardView
A
as Swift closure
needing is provided
a layout for eventually
which will the animations: argument
trigger the which sets message
layoutSubviews the center property
to be of the
sent to the view (all
boardView.
the UIKit views are backed by a Core Animation layer and it is simple to animate many of their properties).

6 Grading
5.3 and Bonus Points
Shu✏ing Tiles
When the user chooses
This assignment is worth toashu✏e the50tiles
total of to start
points, over,
broken up we simply tell the model to scramble the tiles and
as follows:
inform the boardView that it needs to layout the buttons again using the action method listed in Figure 9.
Note 10 ptswe do
that General Style boardView’s
not invoke / Compilation layoutSubviews method directly – we simply mark the boardView
05 pts Interface is properly laid outtrigger the layoutSubviews message to be sent to the boardView.
as needing a layout which will eventually
10 pts Model is present and correctly structured / implemented
10 pts Model, View, and Controller are properly separated
6 05 Optional
pts Shuffle Bells
is handled and Whistles
appropriately
05 pts Tiles which can be slid slide correctly
You should
05 pts provide all the cannot
Tiles which appropriate images
be slid remainforstationary
the various app icons (at least for iOS 8) which are stored in
the Image Assets Catalog Images.xcassets. Customize the LaunchScreen.storyboard file as appropriate.
Youwill
We cannot
earn some
focus toobonus
much onpoints forforgoing
polish abovebut
this app, and beyond
good artworkthecan
requirements
really makeofanthe
appassignment, to a
more appealing.
maximum of 5 bonus points. Here are a few examples of things you might do:

6.1• Provide
Data (reasonably
Persistence
nice) app icons, and customize the launch screen to be more appealing (1pt)
• Save/restorethe
Saving/restoring thestate
stateofofthe
thepuzzle
puzzleto/from
to local the app’s
storage Documents folder is generally a good idea and
(2pts)
can give the appearance that the app is never really terminated. I simply copy the state of my puzzle
• Use images on the buttons (in addition to their title) so the puzzle shuffles / fixes the image. (2 pts)
into 16 NSNumber’s stored in an NSArray which can be trivially saved/restored as a Property List (plist) as
demonstrated in class. If you loaded a background image as described below, saving this can take a bit more
work
7 (IWhatdon’t bother with that).
to Submit
Zip up your project’s directory, as usual. If you would like your project to be considered for extra credit,
include a README file in the zip which indicates what else program does, and how to use the functionality.

76
@IBAction func tileSelected(_ sender: UIButton) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let board = appDelegate.board

let pos = board!.getRowAndColumn(forTile: sender.tag)


let buttonBounds = sender.bounds
var buttonCenter = sender.center
var slide = true
if board!.canSlideTileUp(atRow: pos!.row, Column: pos!.column) {
buttonCenter.y -= buttonBounds.size.height
} else if board!.canSlideTileDown(atRow: pos!.row, Column: pos!.column) {
buttonCenter.y += buttonBounds.size.height
} else if board!.canSlideTileLeft(atRow: pos!.row, Column: pos!.column) {
buttonCenter.x -= buttonBounds.size.width
} else if board!.canSlideTileRight(atRow: pos!.row, Column: pos!.column) {
buttonCenter.x += buttonBounds.size.width
} else {
slide = false
}

if slide {
board!.slideTile(atRow: pos!.row, Column: pos!.column)
sender.center = buttonCenter // or animate the change
if (board!.isSolved()) {
// celebrate victory
}
}
}

Figure 6: Action method that can trigger a tile to slide into a neighboring hole.

@IBAction func shuffleTiles(_ sender: AnyObject) {


let appDelegate = UIApplication.shared.delegate as! AppDelegate
let board = appDelegate.board!
board.scramble(numTimes: appDelegate.numShuffles)
self.boardView.setNeedsLayout()
}

Figure 7: Shuffling tiles to generate a new puzzle.

You might also like