0

I'm trying to make a Wack-a-Mole game. The way I am doing so is by having one button randomly appear and disappear around the screen after someone has tapped it. If they do not tap the button within one second after it reappears, then it will disappear and find a new position, then reappear and wait one second and repeat the above steps. However, whenever I run this code, it doesn't do that. It moves positions only if I get rid of the 'while' statement and the 'if else' statement. Why won't it loop, disappear, reappear, etc?

Delay is this: https://stackoverflow.com/a/24318861/5799228

   @IBAction func moveButton(button: UIButton) {
    while self.WaffleButton.hidden == true || false {
        if self.WaffleButton.hidden == false {
            self.WaffleButton.hidden = true
            delay(3) {
                // Find the button's width and height
                let buttonWidth = button.frame.width
                let buttonHeight = button.frame.height

                // Find the width and height of the enclosing view
                let viewWidth = button.superview!.bounds.width
                let viewHeight = button.superview!.bounds.height

                // Compute width and height of the area to contain the button's center
                let xwidth = viewWidth - buttonWidth
                let yheight = viewHeight - buttonHeight

                // Generate a random x and y offset
                let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
                let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))

                // Offset the button's center by the random offsets.
                button.center.x = xoffset + buttonWidth / 2
                button.center.y = yoffset + buttonHeight / 2
                self.WaffleButton.hidden = false
                self.delay(1) {
                    self.WaffleButton.hidden = true
                }
            }
        } else { delay(3) {
            // Find the button's width and height
            let buttonWidth = button.frame.width
            let buttonHeight = button.frame.height

            // Find the width and height of the enclosing view
            let viewWidth = button.superview!.bounds.width
            let viewHeight = button.superview!.bounds.height

            // Compute width and height of the area to contain the button's center
            let xwidth = viewWidth - buttonWidth
            let yheight = viewHeight - buttonHeight

            // Generate a random x and y offset
            let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
            let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))

            // Offset the button's center by the random offsets.
            button.center.x = xoffset + buttonWidth / 2
            button.center.y = yoffset + buttonHeight / 2
            self.WaffleButton.hidden = false
            self.delay(1) {
                self.WaffleButton.hidden = true
            }


            }
        }

    }
}
Community
  • 1
  • 1

1 Answers1

1

You are using while in a way it is not meant to be used.

This is how you should use while :

var someCondition = true

while someCondition {

    // this will loop as fast as possible untill someConditionIsTrue is no longer true
    // inside the while statement you will do stuff x number of times
    // then when ready you set someCondition to false
    someCondition = false // stop

}

This is how you are using while :

let someConditionThatIsAlwaysTrue = true

while someConditionThatIsAlwaysTrue {

    // condition is always true, so inifinite loop...

    // this creates a function that is executed 3 seconds after the current looping pass of the while loop.
    // while does not wait for it to be finished.
    // while just keeps going.
    // a fraction of a second later it will create another function that will execute 3 seconds later.
    // so after 3 seconds an infite amount of functions will execute with a fraction of a second between them.
    // except they won't, since the main thread is still busy with your infinite while loop.
    delay(3) {
        // stuff
    }
}

How to do it the right way :

  • don't ever use while or repeat to "plan" delayed code execution.

  • Split up the problem in smaller problems:


Issue 1 : Creating a loop

A loop is created by have two functions that trigger each other. I will call them execute and executeAgain.

So execute triggers executeAgain and executeAgain triggers execute and then it starts all over again -> Loop!

Instead of calling execute and executeAgain directly, you also create a start function. This is not needed but it is a good place to setup conditions for your looping function. start will call execute and start the loop.

To stop the loop you create a stop function that changes some condition. execute and executeAgain will check for this condition and only keep on looping if the check is successful. stop makes this check fail.

var mustLoop : Bool = false

func startLoop() {
    mustLoop = true
    execute()
}

func execute() {
    if mustLoop {
        executeAgain()
    }
}

func executeAgain() {
    if mustLoop {
        execute()
    }
}

func stop() {
    mustLoop = false
}

Issue 2: Delayed Execution

If you need a delay inside a subclass of NSObject the most obvious choice is NSTimer. Most UI classes (like UIButton and UIViewController) are subclasses of NSObject.

NSTimer can also be set to repeat. This would also create a loop that executes every x seconds. But since you actually have 2 alternating actions it makes more sense to adopt the more verbose looping pattern.

An NSTimer executes a function (passed as Selector("nameOfFunction")) after x amount of time.

var timer : NSTimer?

func planSomething() {
    timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("doSomething"), userInfo: nil, repeats: false)
}

func doSomething() {
    // stuff
}

If you need a delay in another class/struct (or you don't like NSTimer) you can use the delay function that matt posted.

It will execute whatever you enter in the closure after x amount of time.

func planSomething() {
    delay(3) {
        doSomething()
    }
}

func doSomething() {
    // stuff
}

Combining the two solutions:

By using the loop pattern above you now have distinct functions. Instead of calling them directly to keep the loop going. You insert the delay method of your choice and pass the next function to it.

So NSTimer will have a Selector pointing to execute or executeAgain and with delay you place them in the closure


How to implement it elegantly:

I would subclass UIButton to implement all this. Then you can keep your UIViewController a lot cleaner. Just choose the subclass in IB and connect the IBOutlet as usual.

enter image description here

This subclass has a timer attribute that will replace your delay. The button action wacked() is also set in the init method.

From your UIViewController you call the start() func of the button. This will start the timer.

The timer will trigger appear() or disappear.

wacked() will stop the timer and make the button hide.

class WackingButton : UIButton {

    var timer : NSTimer?

    var hiddenTime : NSTimeInterval = 3
    var popUpTime : NSTimeInterval = 1

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addTarget(self, action: "wacked", forControlEvents: UIControlEvents.TouchUpInside)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.addTarget(self, action: "wacked", forControlEvents: UIControlEvents.TouchUpInside)
    }

    func start() {
        timer = NSTimer.scheduledTimerWithTimeInterval(hiddenTime, target: self, selector: Selector("appear"), userInfo: nil, repeats: false)
    }

    func appear() {

        self.center = randomPosition()

        self.hidden = false

        timer?.invalidate()
        timer = NSTimer.scheduledTimerWithTimeInterval(popUpTime, target: self, selector: Selector("dissappear"), userInfo: nil, repeats: false)
    }

    func dissappear() {

        self.hidden = true

        timer?.invalidate()
        timer = NSTimer.scheduledTimerWithTimeInterval(hiddenTime, target: self, selector: Selector("appear"), userInfo: nil, repeats: false)
    }

    func wacked() {
        self.hidden = true
        timer?.invalidate()
    }

    func randomPosition() -> CGPoint {

        // Find the width and height of the enclosing view
        let viewWidth = self.superview?.bounds.width ?? 0 // not really correct, but only fails when there is no superview and then it doesn't matter anyway. Won't crash...
        let viewHeight = self.superview?.bounds.height ?? 0

        // Compute width and height of the area to contain the button's center
        let xwidth = viewWidth - frame.width
        let yheight = viewHeight - frame.height

        // Generate a random x and y offset
        let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
        let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))

        // Offset the button's center by the random offsets.
        let x = xoffset + frame.width / 2
        let y = yoffset + frame.height / 2

        return CGPoint(x: x, y: y)
    }
}

Your UIViewController :

class ViewController: UIViewController {

    @IBOutlet weak var button1: WackingButton!

    override func viewDidAppear(animated: Bool) {
        button1.start()
    }
}
Community
  • 1
  • 1
R Menke
  • 7,577
  • 4
  • 32
  • 59
  • I'm testing it out now. Thanks. – Will Boland Jan 16 '16 at 22:37
  • I am just confused on where to put the functions inside of the UIViewController. Or do I even need to call those functions? Is all I need to do is create a new blank swift file, insert the above code that you typed up (thank you so much) and then subclass it to the Button? Thanks so much for your answer, I'm just confused slightly because I'm still learning Swift and iOS Programming. – Will Boland Jan 17 '16 at 02:19
  • When I connect an outlet for the button to the UIViewController, should it be of type "WackingButton" or "UIButton?" And I tried also making the Button connected to an action of type WackingButton and in it I put the Start Method (so when they clicked it, it started.) Nothing happened. – Will Boland Jan 17 '16 at 02:32
  • Nevermind, figured it out. Only one problem: The button never reappears after I click it. – Will Boland Jan 17 '16 at 02:39
  • The button never appears again. – Will Boland Jan 17 '16 at 02:58
  • 1
    In R. Menke's example code, nothing is programmed to make a new button appear in the wacked() function. If you didn't add anything there yourself (or in the IBAction) , the program is behaving as designed. – Alain T. Jan 17 '16 at 04:24
  • Oh, I understand. So what you're saying I need to do is create a loop such as: 'while WackingButton.hidden == true || false' then always have it start the function? Thanks! – Will Boland Jan 17 '16 at 14:18
  • How do I call the 'start()' function inside of my ViewController? – Will Boland Jan 17 '16 at 14:31
  • @WillBoland I updated the answer with more info. I also removed my comments here, since that info is now also in the answer. Tell me if you are still stuck. – R Menke Jan 17 '16 at 15:23
  • Thank you so much. The part I was confused on was what the View controller should look like. I didn't know about the viewDidAppear part. Thank you so much, I'll test it out now, and I fully understand everything now. Thank you so much. Sorry if I frustrated you by asking so many questions and not knowing what to do, but I am trying to learn. Thanks. – Will Boland Jan 17 '16 at 17:59
  • @WillBoland No problem. StackOverflow is just not a great place to learn when you have almost no knowledge of the subject. This is because often people fail to write ok questions and do no understand the answers. It is better to spend a bit more time just reading and doing tutorials. Not that you are not welcome here, you are! – R Menke Jan 17 '16 at 18:36