cs193p – Project #5 Assignment #5 Step #5 – The Configuration

By Toniperis (Own work) [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons

Please note, this blog entry is from a previous course. You might want to check out the current one.

For the configuration – the settings of the game – create a new class which will handle the communication with the user defaults. Most of the variables stored in the settings are optional, as a way when to use default values.

The columns and the rows setting hold the dimensions of the level “one” of the brick wall:

    struct Const {
        static let ColumnsKey = "Settings.Columns"
        static let RowsKey = "Settings.Rows"
    }

    let defaults = NSUserDefaults.standardUserDefaults()

    var columns: Int? {
        get { return defaults.objectForKey(Const.ColumnsKey) as? Int}
        set { defaults.setObject(newValue, forKey: Const.ColumnsKey) }
    }

    var rows: Int? {
        get { return defaults.objectForKey(Const.RowsKey) as? Int}
        set { defaults.setObject(newValue, forKey: Const.RowsKey) }
    }


Currently, the game has only one ball. When tapping the screen and there is no ball, it will be added, otherwise it is pushed. Now the balls settings allows to add multiple balls:

    struct Const {
        ...
        static let BallsKey = "Settings.Balls"
    }

    var balls: Int? {
        get { return defaults.objectForKey(Const.BallsKey) as? Int}
        set { defaults.setObject(newValue, forKey: Const.BallsKey) }
    }

The difficulty settings chooses if the last row of bricks should be “black” and thus need two collisions to be destroyed:

    struct Const {
        ...
        static let DifficultyKey = "Settings.Difficulty"
    }

    var difficulty: Int? {
        get { return defaults.objectForKey(Const.DifficultyKey) as? Int}
        set { defaults.setObject(newValue, forKey: Const.DifficultyKey) }
    }

Autostart will allow balls to appear without user interaction:

    struct Const {
        ...
        static let AutoStartKey = "Settings.AutoStart"
    }

    var autoStart: Bool {
        get { return defaults.objectForKey(Const.AutoStartKey) as? Bool ?? false}
        set { defaults.setObject(newValue, forKey: Const.AutoStartKey) }
    }

… and a way to control the speed of the ball/push:

    struct Const {
        ...
        static let SpeedKey = "Settings.Speed"
    }

    var speed: Float {
        get { return defaults.objectForKey(Const.SpeedKey) as? Float ?? 1.0 }
        set { defaults.setObject(newValue, forKey: Const.SpeedKey) }
    }

Instead of the optional variables I could have added the default values in the new class. But there are already a number of “default” values in the view controller, and I did not wont to spread them to different parts of the code. Instead I added a convenience initializer to provide default values to the settings class:

    convenience init(defaultColumns: Int, defaultRows: Int, defaultBalls: Int, defaultDifficulty: Int) {
        self.init()
        if columns == nil { columns = defaultColumns }
        if rows == nil { rows = defaultRows }
        if balls == nil { balls = defaultBalls }
        if difficulty == nil { difficulty = defaultDifficulty }
    }

An additional variable will hold the information if the settings have been changed:

    struct Const {
        ...
        static let ChangeKey = "Settings.ChangeIndicator"
    }

    var changed: Bool {
        get { return defaults.objectForKey(Const.ChangeKey) as? Bool ?? false }
        set { defaults.setObject(newValue, forKey: Const.ChangeKey) }
    }

In storyboard embed the current controller in a tab view controller, add a new view controller (attach it to a new settings view controller class) and add nice icons. Layout some labels, switches, sliders, … (don’t forget constraints for autolayout) create outlets for each label you want to adjust, and outlets and actions for every switch, slider, …

cs193p - Project #5 Assignment #5 Step #5 - The Configuration - Tab View & Settings Controller
cs193p – Project #5 Assignment #5 Step #5 – The Configuration – Tab View & Settings Controller

For the column and row slider set their limits in storyboard, add outlets for the sliders and the labels and actions for the sliders:

    @IBOutlet weak var columnSlider: UISlider!
    @IBOutlet weak var rowSlider: UISlider!

    @IBOutlet weak var columnLabel: UILabel!
    @IBOutlet weak var rowLabel: UILabel!

    @IBAction func columnsChanged(sender: UISlider) {
    }

    @IBAction func rowsChanged(sender: UISlider) {
    }

Add computed variables which access and set the labels and the sliders:

    var columns: Int {
        get { return columnLabel.text!.toInt()! }
        set {
            columnLabel.text = "\(newValue)"
            columnSlider.value = Float(newValue)
        }
    }

    var rows: Int {
        get { return rowLabel.text!.toInt()! }
        set {
            rowLabel.text = "\(newValue)"
            rowSlider.value = Float(newValue)
        }
    }

When the slider change, set those variable (which will in turn set the labels) and set the user defaults – in addition set the changed variable to let others know that something has happened:

    @IBAction func columnsChanged(sender: UISlider) {
        columns = Int(sender.value)
        Settings().columns = columns
        Settings().changed = true
    }

    @IBAction func rowsChanged(sender: UISlider) {
        rows = Int(sender.value)
        Settings().rows = rows
        Settings().changed = true
    }

And get the initial value when the view will appear from the user defaults:

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        columns = Settings().columns!
        rows = Settings().rows!
    }

The other variables/settings work analogue:

    @IBOutlet weak var ballStepper: UIStepper!
    @IBOutlet weak var difficultySelector: UISegmentedControl!
    @IBOutlet weak var autoStartSwitch: UISwitch!
    @IBOutlet weak var speedSlider: UISlider!
    
    @IBOutlet weak var ballLabel: UILabel!
    @IBOutlet weak var speedLabel: UILabel!
    
    var balls: Int {
        get { return ballLabel.text!.toInt()! }
        set {
            ballLabel.text = "\(newValue)"
            ballStepper.value = Double(newValue)
        }
    }
    
    var difficulty: Int {
        get { return difficultySelector.selectedSegmentIndex }
        set { difficultySelector.selectedSegmentIndex = newValue }
    }

    var autoStart: Bool {
        get { return autoStartSwitch.on }
        set { autoStartSwitch.on = newValue }
    }
    
    var speed: Float {
        get { return speedSlider.value / 100.0 }
        set {
            speedSlider.value = newValue * 100.0
            speedLabel.text = "\(Int(speedSlider.value)) %"
        }
    }

    override func viewWillAppear(animated: Bool) {
        ...
        balls = Settings().balls!
        difficulty = Settings().difficulty!
        autoStart = Settings().autoStart
        speed = Settings().speed
    }

    @IBAction func ballsChanged(sender: UIStepper) {
        balls = Int(sender.value)
        Settings().balls = balls
    }
    
    @IBAction func difficultyChanged(sender: UISegmentedControl) {
        Settings().difficulty = difficulty
        Settings().changed = true
    }
    
    @IBAction func autoStartChanged(sender: UISwitch) {
        Settings().autoStart = autoStart
    }
    
    @IBAction func speedChanged(sender: UISlider) {
        speed = sender.value / 100.0
        Settings().speed = speed
        Settings().changed = true
    }

In the view controller set the default values – they only will be set, if there is value in the user defaults:

    override func viewDidLoad() {
        ...
        Settings(defaultColumns: Constants.BrickColumns, defaultRows: Constants.BrickRows, defaultBalls: 1, defaultDifficulty: 0)
        ...
    }

When the view will appear (e.g. when the switching back from the settings tab), check if something has changed. Reset the changed property. Remove all existing bricks from the view and destroy them. Remove any balls left. Reset the animator and the breakout behavior. Finally, recreate the bricks with the new settings. To be able to reset the breakout behavior, change it from let to var:

    var breakout = BreakoutBehavior()

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        if Settings().changed {
            Settings().changed = false
            for (index, brick) in bricks {
                brick.view.removeFromSuperview()
            }
            bricks.removeAll(keepCapacity: true)
            
            for ball in breakout.balls {
                ball.removeFromSuperview()
            }
            animator.removeAllBehaviors()
            breakout = BreakoutBehavior()
            animator.addBehavior(breakout)
            breakout.collisionDelegate = self
            levelOne()
        }
    }

To use the number of rows and columns from the settings use those values in the setup:

    func levelOne() {
        ...        
        let deltaX = Constants.BrickTotalWidth / CGFloat(Settings().columns!)
        let deltaY = Constants.BrickTotalHeight / CGFloat(Settings().rows!)
        ...        
        for row in 0..<Settings().rows! {
            for column in 0..<Settings().columns! {
                ...                
                bricks[row * Settings().columns! + column] = Brick(relativeFrame: frame, view: brick, action: action)
            }
        }
    }

To allow additional balls, check if the maximum number of balls has been reached yet:

    func pushBall(gesture: UITapGestureRecognizer) {
        ...
            if breakout.balls.count < Settings().balls {
                ...
            }
            ...
    }

Add the “black” row only when the difficulty “hard” has been chosen:

    func levelOne() {
                ...
                if Settings().difficulty == 1 {
                    if row + 1 == Settings().rows! {
                        ...
                    }
                }                
                ...
    }

To start balls automatically, add a timer which periodically checks, if there is a ball (or the maximum number of balls) and push them if necessary.

A property will hold the current timer if there is any:

    var autoStartTimer: NSTimer?

Create a timer when the view will appear:

    override func viewWillAppear(animated: Bool) {
        setAutoStartTimer()
    }

Remove it when the view will disappear again:

    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        autoStartTimer?.invalidate()
        autoStartTimer = nil
    }

The timer will be started only when the configuration is set to do so:

    func setAutoStartTimer() {
        if Settings().autoStart {
            autoStartTimer =  NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: "autoStart:", userInfo: nil, repeats: true)
        }
    }

When the timer fires – and there are not “enough” balls, create a new one, place it, add it to the behavior and push it:

    func autoStart(timer: NSTimer) {
        if breakout.balls.count < Settings().balls {
            let ball = createBall()
            placeBall(ball)
            breakout.addBall(ball)
            breakout.pushBall(breakout.balls.last!)
        }
    }

Don’t forget to remove the timer when a game has finished, and start it again afterwards:

    func levelFinished() {
        autoStartTimer?.invalidate()
        autoStartTimer = nil
        ...
        alertController.addAction(UIAlertAction(title: "Play Again", style: .Default, handler: { (action) in
            ...
            self.setAutoStartTimer()
        }))
        ...
    }

To change the speed of the ball, add a public property to the behavior:

    var speed:CGFloat = 1.0

… and use it as magnitude when pushing the ball:

    func pushBall(ball: UIView) {
        ...
        push.magnitude = speed        
        ...
    }

Set the speed in the view controller:

    override func viewWillAppear(animated: Bool) {
        ...
        if Settings().changed {
            breakout.speed = CGFloat(Settings().speed)
        }        
    }

The complete code for step #5 is available on GitHub.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

2 thoughts on “cs193p – Project #5 Assignment #5 Step #5 – The Configuration”

  1. Why did it stopped assignment V solutions?
    What a pity!
    They were so interesting!
    Continue publications! I pray a lot!

    1. Because, I had to work on some Apps for paying customers 😉 … and Swift 1.2 introduced some changes, for which I should redo the code … and now there is Swift 2.0 on the horizon, …

      Please, stop praying, because more spare time means less paying customers. You don’t want my family to starve, do you?

Leave a Reply

Your email address will not be published.