- Assignment 3: Fancy Triples
- Goals
- Before you begin
- Task 1: Change your storyboard.
- Adapt to Orientation changes
- Task 2: Change the model
- Implement TileButtons programmatically
- Task 3: Custom View
- Task 4: Animation
- Task 5: Gestures
- Task 6: TabBarController
- Task 7: Implement the HighController Scene
- Task 8: Implement the About Scene
- Grading
Assignment 3: Fancy Triples
Due: Apr 5, 2020, 11:59:59 pm, v1
Goals
Learn to use:
- property animation
- custom views
- drawing
- gestures
- tab bar view controllers
- user defaults
- dynamic animations
This is the project where we end up with a real game we can give out to friends and family. We are not significantly changing core functionality, but we are adding many of the extras that make apps usable.
Before you begin
This project is based on the last one, but copying Xcode projects is a dicy, multiple-step project, easy to mess up.
Instead, I suggest you create a new project assign3
in your repository, no need for either unit or UI tests. Create
a new model.swift
, and then copy and paste
whichever portions of your model and ViewController
code you want to keep (probably a lot
of it).
Note: create your new project assign2 directly in your repository, so the resulting directory structure should look like this:
repository
/ \
/ | \
/ | \
/ | \
assign1 assign2 assign3
/ \ / \ / \
assign2/ assign2.x... assign3/ assign3.xcodeproj/
/ \ / \
model.swift ...
Run this version to ensure everything is working correctly, quit out of Xcode, git add assign3
, and then push to your repository. Note that if you create more files, you need
to manually add them as well. Easiest to just do a git add *
in any affected
subdirectories.
We realize that git
is sometimes a challenging, non-intuitive tool, especially as we are
not using Xcode's GUI interface, but understanding git will serve you well in almost any
facet of the tech world going forward.
Task 1: Change your storyboard.
Throw away your statically defined "tile" Buttons. You are going to programmatically create "tiles" on the fly to match your game model.
The storyboard will have the following elements:
- a
view
(view from the interface builder, just as if it were a button) to use as your square board (add a constraint to ensure that it stays square) - a stack of controls, including:
- left, right, up, down buttons
- newgame
- segmented random/determ control
- score label
Put the view and the control stack into another stack (the top-level stack), add constraints as I did in class to:
- pull the top-level stack's leading and trailing edges to 10 points off safe area, center it vertically
- ensure that the square board is as large as possible (pin leading and trailing edges to it's superview
Adapt to Orientation changes
Introduce two variations to allow your storyboard to adapt smoothly to landscape mode and back again:
- Introduce a horizontal variant to the top-level stack for compact height
- Put the storyboard in horizontal mode, click on "Vary for Traits", and get rid of some constraints that no longer apply in horizontal mode (don't need to pin leading and trailing edges of top-level stack to edges, etc.), and add new ones (pin top and bottom of top-level stack to edges, etc.)
- Click done varying, go back to vertical mode, and click "Done Varying".
You cannot run at this point because your IBOutlet
s refer to non-existent views.
Task 2: Change the model
Your model is conceptually unchanged. However, the view controller is now going to animate tile movement, and therefore needs to recognize specific tiles in order to identify tile movements.
We are therefore going to change our board's base type from Int
to a
new struct Tile
.
Tile
s will have a unique id, and therefore be identifiable even
as their value (the number) changes. Define Tile
as follows:
struct Tile {
var val : Int
var id : Int
var row: Int // recommended
var col: Int // recommended
}
Your model's board
will change from [[Int]]
to [[Tile?]]
. Note the
optional base type; each cell of your board will consist of either a Tile
or a nil
.
You might want to include row
and col
properties to make drawing on the
board a bit easier. Be sure to update these properties before returning to the
view controller after a collapse, spawn, or newgame.
Hint: If a row has the following tiles and we are collapsing left:
(val: 1, id: 13), (val:2, id:14), nil, nil
you probably want to have tile 13 disappear, while tile 14 moves to the left. This makes animation more straightforward.
Implement TileButtons programmatically
Your updateViewForGame
(substitute whatever name you use) needs to ensure
that there are buttons on top of the board (viewOutlet
you've attached to
the square view you put on the storyboard) for each current tile. This
involves:
- create buttons for each newly-seen tile and adding as a subview to the
board
at the correct location - moving the
frame
for each tile that we've already seen - removing buttons for any tiles that we had seen before but are no longer in the model
Before you get started, you should create a new subclass ButtonTile
:
class ButtonTile: UIButton {
var tile = Tile(val: 0, id: 0, row: 0, col: 0)
convenience init(t: Tile) {
self.init()
tile = t
}
}
where you have defined Tile
in your new model. You can use a ButtonTile
just like a UIButton
, but it has the Tile
information readily at hand
(crucial for implementing animation via a custom view later).
Your code will be able to recognize tiles from previous steps in the
application because each tile will have a unique ID
.
At the end of this step you should have a fully functional game again. It should adapt well to the orientation changes except that existing TileButtons might have the wrong size and position until a subsequent swipe.
Hint: You should create a method func buttonFrame(row: Int, col: Int, view: UIView) -> CGRect
that will compute the frame for a ButtonTile
, based on view
and the fact
that your game board will always have four rows of four possible positions.
Hint: adding a button to board
can be done w/ the following code:
let button = ButtonTile(t: tileMap[id]!)
board.addSubview(button)
Later on it can be removed via tileButton.removeFromSuperView()
Save early and often by pushing to your repository.
Task 3: Custom View
At this point we want to build a custom view for our board, and have
ButtonTile
operate independently of the view controller.
- Subclass
UIView
with a customBoardView
by create a new Cocoa Touch file. - Move
buttonFrame
your newBoardView
class. -
override func draw(_ rect: CGRect)
to draw the board lines any way you wish, though you must useUIBezierPath
. -
override func layoutSubviews()
to get lay out your buttons in the correct location.
draw
is called by iOS whenever a previously unseen portion of the board gets
seen. Use this just to draw the board's background lines.
layoutSubviews
is the majority of this task. It is called whenever iOS think
there might have been a change in layout (at app startup, orientation changes,
and other seemingly random times).
Your job is to make put each button in it's proper place, set the titles,
colors, etc. Views should not access view controllers' data directly. Instead make this
routine entirely standalone, deriving all information by querying its own
subviews
property.
subviews
is an array of all child view, which in this case is only those
buttons that the view controller has put on the board. But how to figure out
where to situate the buttons, and how to figure out color and text of each?
Luckily, these "buttons" are actually ButtonTile
s. Cast each UIView
to a
ButtonTile
and you now have access to the complete Tile
associated with
each button.
The view controller's updateViewFromGame()
method is now quite short. It has
to:
- add
ButtonTile
s to the board if the model has a previously-unseenTile
. - remove
ButtonTile
s from the board if a previously-unseenTile
no longer exists. - ensure that all remaining
ButtonTile
s have accurate row/col information. - update the score
- ensure that the board's layout / draw methods are called by calling
board.setNeedsDisplay()
Do not add any externally-accessed methods or properties in your custom view.
All communication from view controller to view should be from the
ButtonTiles
in the custom view's subviews
array.
Task 4: Animation
Implement tile animation entirely in your BoardView
class:
- animate new tiles but starting them w/ opacity of 0 and animating to opacity 1.0
- animate movement of existing tiles to new locations
- use opacity again to animate disappearence of previously-seen tiles
Hint: Animating tile disappearence in BoardView
will not work if the
view controller removes the ButtonTile
from the board.
Task 5: Gestures
Add up, down, left, and right swipe gestures to the board. The gesture recognizer use your
game's view controller as a target (first parameter to the
UISwipeGestureRecognizer
initializer), but you can attach this either to
view controller's view, or to the game board. Either works.
Task 6: TabBarController
This task is simple: draw out a TabBarController
in the storyboard, attach
the game controller (your original ViewController
), and ensure that the TabBarController
is the initial
controller by moving the arrow.
Everything else should continue working the same.
Task 7: Implement the HighController Scene
You should:
- subclass the
UIViewController
to a newHighController
class. - use interface builder to add a
UITableView
. - implement routines for the view controller to call providing a new high score
- use
UserDefaults
to store and retrieve high scores - the the tableview to show at least ten of the top scores with rank / score / date-and-date, as shown in the video.
- your game should jump to the high-scores screen at the end of a game when
calling
isDone()
returns true for the first time.
Task 8: Implement the About Scene
It doesn't have to be like mine, and it doesn't have use gestures, but it
should be fancy. Use UIBezierPath
to draw something a diagram, make a little
mini-game using dynamic gestures, be creative!
Grading
Note that the above tasks are for sequencing your work. They do not match up exactly with this grading rubric:
- 30: change storyboard and buttons to be dynamic, adapting to orientation/device changes
- 30: animation, both movement and spawns
- 10: gestures
- 10: tabbarcontroller (just with labels saying "High Score" and "by AwesomeWhoever")
- 10: High scores using a tableview and
UserDefaults
- 10: Fancy About scene using dynamic animation.