NSTimeInterval as a countdown timer, applying it as a seperate reusable class - ios

EDIT: I'm an idiot. I was trying to put the variable into NSTimer() and not NSTimeInterval for whatever silly reason. I guess my question is how can I wrap this all up in a separate class?
Ideally, I'd like all this wrapped up in a separate class (CountdownTimer), so I can create new instance of a timer but still retain all the functionality that NSTimer includes such as the ability to check timer.isValid. Psuedocode would look something like:
var timer = CountdownTimer(countDownFrom: 300)
timer.start()
timer.isValid()
My UIViewController class (not in viewDidLoad):
var totalCountDownTimeInterval = NSTimeInterval(480.0)
var startTime = NSDate()
var timer = NSTimer()
var isRunning = false
func updateTime() {
var elapsedTime : NSTimeInterval = NSDate().timeIntervalSinceDate(startTime)
var remainingTime : NSTimeInterval = totalCountDownTimeInterval - elapsedTime
if remainingTime <= 0.0 {
timer.invalidate()
}
let minutes = UInt8(remainingTime / 60.0)
remainingTime = remainingTime - (NSTimeInterval(minutes) * 60)
let seconds = UInt8(remainingTime)
println("The time is \(minutes) and \(seconds)")
}
#IBOutlet weak var TimerCount: UILabel!
#IBAction func StartButton(sender: AnyObject) {
if !timer.valid {
startTime = NSDate()
let aSelector : Selector = "updateTime"
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: aSelector, userInfo: nil, repeats: true)
}
}
#IBAction func StopButton(sender: AnyObject) {
timer.invalidate()
}
#IBAction func ResetButton(sender: AnyObject) {
timer.invalidate()
TimerCount.text = "00:00"
}

Make sure that you're actually passing in a double. I like to explicitly state the type of my variables when I declare them; it helps to avoid problems just like this.
You're most likely declaring doubleValue like this:
let doubleValue = 480
instead of like this:
let doubleValue = 480.0
or like this:
let doubleValue: Double = 480
If you've declared your variable correctly, this should work:
let timeInterval = NSTimeInterval(doubleValue)
If you are going to let the compiler infer the variable's type, just make sure that whatever's on the right side of your assignment operator evaluates to the type you're looking for. 480 evaluates to Int(480) while 480.0 evaluates to Double(480).
EDIT: Here's the answer to your second question: How can I wrap this [timer functionality] up in a separate class?
It's actually really simple. Assuming that all you want to do with the class is to be able to start it and check if it's still valid, here's how I would go about doing this:
class CountdownTimer
{
var time: NSTimeInterval
private var startTime: NSDate?
init(countDownFrom timeInSeconds: Int)
{
time = NSTimeInterval(timeInSeconds)
}
func start()
{
startTime = NSDate()
}
func isValid() -> Bool
{
if (startTime != nil)
{
let timePassed: NSTimeInterval = -(startTime!.timeIntervalSinceNow)
return timePassed < time
}
else
{
return false
}
}
}
Now, be warned, I barely tested this. Playground isn't complaining and from the looks of it, this should work. Now, just use the class like so:
var myCountdownTimer = CountdownTimer(countDownFrom: 300)
// and then whenever we want to start the countdown:
myCountdownTimer.start()
// and then whenever we want to check if the clock's still "ticking", so to speak:
myCountdownTimer.isValid()
// and if we want to restart the timer:
myCountdownTimer.time = NSTimeInterval(900) // we can change the time if we want
myCountdownTimer.start()
Essentially, all a CountdownTimer object does is save the exact time start() is called to a variable called startTime. Note that NSDate() by default is set to the time-and-date it's created. Then isValid() simply checks to see if the timePassed is less than whatever time the timer was set to count down from; if it is, then it returns true.

I tried this code on playground of Xcode 6.1, And It worked fine.
That's strange...
let someValue: Double = 60.0
var timeInterval = NSTimeInterval(someValue)
println(timeInterval)

Related

Wait for Swift timers to finish

So I'm doing a simple game on swift 5 and I basically have a 3..2..1.. go timer to start the game and then a 3..2..1..stop timer to stop the game. And finally a function that displays the score. I need a way for each function call to wait for the timer to be done before the next one begins, any suggestions? Here's my code so far. (Also if you have any other suggestions on the app let me know as well, the end goal is to register how many taps of a button you can do in 3 seconds)
var seconds = 3 //Starting seconds
var countDownTimer = Timer()
var gameTimer = Timer()
var numberOfTaps = 0
override func viewDidLoad() {
super.viewDidLoad()
self.startCountdown(seconds: seconds)
self.gameCountdown(seconds: seconds)
self.displayFinalScore()
}
func startCountdown(seconds: Int) {
countDownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.seconds -= 1
if self?.seconds == 0 {
self?.countdownLabel.text = "Go!"
timer.invalidate()
} else if let seconds = self?.seconds {
self?.countdownLabel.text = "\(seconds)"
}
}
}
func gameCountdown(seconds: Int) {
gameTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.seconds -= 1
if self?.seconds == 0 {
self?.countdownLabel.text = "Stop!"
timer.invalidate()
} else if let seconds = self?.seconds {
self?.countdownLabel.text = "\(seconds)"
}
}
}
deinit {
// ViewController going away. Kill the timer.
countDownTimer.invalidate()
}
#IBOutlet weak var countdownLabel: UILabel!
#IBAction func tapMeButtonPressed(_ sender: UIButton) {
if gameTimer.isValid {
numberOfTaps += 1
}
}
func displayFinalScore() {
if !gameTimer.isValid && !countDownTimer.isValid {
countdownLabel.text = "\(numberOfTaps)"
}
}
You should think about the states your game could be in. It could be -
setup - Establish the game
starting - The first three seconds
running - After the first three seconds but before the end
ending - In the final three seconds
ended - Time is up.
Each time your timer ticks you need to consider what action do you need to take and what state do you need to move to. You haven't said how long you want the game to last, but let's say it is 30 seconds.
When a new game is started, you are in the setup state; The button is disabled (ie. it doesn't react to taps) and you set the score to 0. You move to the starting state.
In the starting you show the countdown. After three seconds you enable the button and move into the running state.
Once you reach 27 seconds, you move into the ending state and show the end count down
Finally time is up and you move into the ended state, disable the button and show the score.
You could code it something like this
enum GameState {
case setup
case starting
case running
case ending
case ended
}
class ViewController: UIViewController {
#IBOutlet weak var startButton: UIButton!
#IBOutlet weak var tapButton: UIButton!
#IBOutlet weak var countdownLabel: UILabel!
var gameState = GameState.ended
var gameTimer:Timer?
var numberOfTaps = 0
var gameStartTime = Date.distantPast
let GAMEDURATION: TimeInterval = 30
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func startButtonTapped(_ sender: UIButton) {
self.startGame()
}
#IBAction func tapMeButtonPressed(_ sender: UIButton) {
self.numberOfTaps += 1
}
func startGame() {
self.gameState = .setup
self.gameTimer = Timer.scheduledTimer(withTimeInterval:0.1, repeats: true) { timer in
let elapsedTime = -self.gameStartTime.timeIntervalSinceNow
let timeRemaining = self.GAMEDURATION-elapsedTime
switch self.gameState {
case .setup:
self.gameStartTime = Date()
self.tapButton.isEnabled = false
self.startButton.isEnabled = false
self.numberOfTaps = 0
self.gameState = .starting
case .starting:
if elapsedTime > 2.5 {
self.gameState = .running
self.tapButton.isEnabled = true
self.countdownLabel.text = "Go!"
} else {
let countdown = Int(3-round(elapsedTime))
self.countdownLabel.text = "\(countdown)"
}
case .running:
if timeRemaining < 4 {
self.gameState = .ending
}
case .ending:
let countdown = Int(timeRemaining)
self.countdownLabel.text = "\(countdown)"
if timeRemaining < 1 {
self.countdownLabel.text = "Stop"
self.gameState = .ended
self.tapButton.isEnabled = false
}
case .ended:
if timeRemaining <= 0 {
self.countdownLabel.text = "You tapped the button \(self.numberOfTaps) times"
self.startButton.isEnabled = true
self.gameTimer?.invalidate()
self.gameTimer = nil
}
}
}
}
}
The approach shouldn't be that the function calls wait for the timer to get finished, rather the timers should call the functions when they finish.
So, you need to move below function calls out of viewDidLoad and put them inside the Timer blocks.
self.gameCountdown(seconds: seconds)
self.displayFinalScore()
I.e. the function call self.gameCountdown(seconds: seconds) will go inside the timer block started in startCountdown. In that, when you are invalidating the timer when the seconds become 0, you call gameCountdown.
Similarly, in the timer started in gameCountdown, you call the self.displayFinalScore when the seconds become 0.
Few other suggestions. You should avoid checking properties in tapMeButtonPressed.
You should rather disable and enable the tap me button instead. I.e. enable it when you start the gameCountdown and disable it when it ends.
Similarly, you shouldn't need to check the state of the timers in displayFinalScore. It should just do one thing i.e. display the final score.
Will save you a lot of headaches later :). My 2 cents.

Is it possible to call up a GameScene function within a class/struct instance method?

So I'm making a game for my computer science class and I made a class named Bomb for the sprites in the game. In the Bomb class, I have a function named blowUp() that is called when a countdown timer for the bomb goes off. When time runs out, the bomb's texture changes among other things, but it should also trigger a game over.
I have a gameOver() function in my GameScene that I want to call up in the blowUp() method but when I do that I get the error message "Instance member 'gameOver' cannot be used on type 'GameScene'; did you mean to use a value of this type instead?"
Is there any way around this? Thanks in advance
class Bomb{
var sprite = SKSpriteNode()
var timer = Timer()
var secondsLeft = 20
func countDown(){
secondsLeft -= 1
if secondsLeft == 0{
blowUp()
}
}
init {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in self.countDown()})
}
func blowUp(){
self.timer.invalidate()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sprite.removeFromParent()
}
// gameOver() gives me an error
}
}
Here are the changes I would make to your code.
Extend SKSpriteNode and remove the sprite variable, because you want your bomb to be an actual sprite.
class Bomb : SKSpriteNode{
var sprite = SKSpriteNode()
Eliminate your Timer. Timer goes off of real world, not in game world, so if any external event happens (like a phone call) your time will be off. Use SKActions instead.
var timer = Timer()
var secondsLeft = SKAction.wait(forDuration:20)
Remove your countdown function, blowup function and your init, and replace it with an ignite function
func countDown(){
secondsLeft -= 1
if secondsLeft == 0{
blowUp()
}
}
init {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in self.countDown()})
}
func blowUp(){
self.timer.invalidate()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sprite.removeFromParent()
}
// gameOver() gives me an error
}
func ignite() {
var explosionAnimation = SKAction.animate(with:arrayOfExplosionTextures,timePerFrame:0.1666)
var blowup = SKAction.run{
[unowned self] in
(self.scene as GameScene).gameOver()
}
//var seq = SKAction.sequence([secondsLeft,explosionAnimation,blowup,SKAction.removeFromParent])
var seq = SKAction.sequence([secondsLeft,blowup,SKAction.removeFromParent])
}
What ignite does is start your bomb when you are ready to start it, waits 30 seconds, then explodes. This allows you to place bombs on the screen that are delayed or duds.
Here is what your class should look like:
class Bomb : SKSpriteNode{
var secondsLeft = SKAction.wait(forDuration:20)
func ignite() {
//var explosionAnimation = SKAction.animate(with:arrayOfExplosionTextures,timePerFrame:0.1666)
var blowup = SKAction.run{
[unowned self] in
(self.scene as GameScene).gameOver()
}
//var seq = SKAction.sequence([secondsLeft,explosionAnimation,blowup,SKAction.removeFromParent])
var seq = SKAction.sequence([secondsLeft,blowup,SKAction.removeFromParent])
}
}
Here is how you use it in game scene:
func placeBomb(x:CGFloat,y:CGFloat){
let bomb = Bomb(imageNamed:"bomb")
addChild(bomb)
bomb.position = CGPoint(x:x,y:y)
bomb.ignite()
}

Timer and dependancy injection

I've just started with Swift and using MVVM with dependency injection.
In my ViewModel I have Timer that handles refreshing the data. I've simplified the code a little for clarity.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = ViewModel()
}
}
class ViewModel: NSObject {
private var timer: Timer?
override init() {
super.init()
setUpTimer()
}
func setUpTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
self.refreshData()
}
}
func refreshData() {
//refresh data
print("refresh data")
}
}
I want to use dependency injection to pass the Timer into the ViewModel so that I can control the timer when doing unit tests and make it call immediately.
So passing the Timer is pretty simple. How can I pass a Timer in to ViewModel that has the ability to call the refreshData() belonging to ViewModel. Is there a trick in Swift that allows this?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
// call refreshData() from the class ViewModel
}
var viewModel = ViewModel(myTimer:timer)
}
}
class ViewModel: NSObject {
private var timer: Timer?
init(myTimer:Timer) {
super.init()
//setUpTimer()
timer = myTimer
}
func refreshData() {
//refresh data
print("refresh data")
}
}
I thought it might be possible using the scheduelTimer that takes a selector instead of a block but that would require using a #objc before the func refreshData() which seems clunky since I am using an Objective C feature in Swift.
Is there a nice way to achieve this?
Many Thanks,
Code
Conceptually, you want to decouple the implementation. So instead of having to pass Timer to the view model, you pass some other "control" object, which guarantees to perform the operation (of calling back after a delay)
If that doesn't shout protocol, I don't know what does...
typealias Ticker = () -> Void
protocol Refresher {
var isRunning: Bool { get }
func register(_ ticker: #escaping Ticker)
func start();
func stop();
}
So, pretty basic concept. It can start, stop and an observer can register itself to it and be notified when a "tick" occurs. The observer doesn't care "how" it works, so long as it guarantees to perform the specified operation.
A Timer based implementation then might look something like...
class TimerRefresher: Refresher {
private var timer: Timer? = nil
private var ticker: Ticker? = nil
var isRunning: Bool = false
func register(_ ticker: #escaping Ticker) {
self.ticker = ticker
guard timer == nil else {
return
}
}
func start() {
guard ticker != nil else {
return
}
stop()
isRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true, block: { (timer) in
self.tick()
})
}
func stop() {
guard let timer = timer else {
return
}
isRunning = false
timer.invalidate()
self.timer = nil
}
private func tick() {
guard let ticker = ticker else {
stop()
return
}
ticker()
}
}
This provides you the entry point for mocking the dependency injection, by replacing the implementation of the Refresher with one you can control manually (or use a different "delaying" action, depending on your needs)
This is just a conceptual example, your implementation/needs may differ and lead you to a slightly different design, but the idea remains the same, decouple the physical implementation in some way.
An alternative would require you to rethink your design, and instead of the view model performing it's own refresh, the view/controller would take over that responsibility instead. Since that's a significant design decision, you're really only the person who can make that decision, but it's another idea
If I understand you correctly, you want the model to refresh every 30 seconds when running in the app, but faster for test. If so, don't inject the Timer. Inject the refresh frequency.
class ViewModel: NSObject {
// We need something to observe and confirm that the data is fresh
#objc dynamic var lastRefreshed: Date?
private var timer: Timer!
// The default frequency is 30 seconds but users can adjust that
// The unit test uses it to inject dependency
init(refreshFrequency: TimeInterval = 30) {
super.init()
timer = Timer.scheduledTimer(timeInterval: refreshFrequency, target: self, selector: #selector(refreshData), userInfo: nil, repeats: true)
}
#objc func refreshData() {
lastRefreshed = Date()
print("refreshed on: \(lastRefreshed!)")
}
}
And your unit test:
func testModel() {
let startTime = Date()
let model = ViewModel(refreshFrequency: 5)
// Test first refresh: must be within 5 - 6 seconds from startTime
keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 5...6 ~= duration {
return true
} else {
return false
}
}
// Test second refresh: must be within 10 - 12 seconds from startTime
keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 10...12 ~= duration {
return true
} else {
return false
}
}
// Wait 12 seconds for both expectations to be fulfilled
waitForExpectations(timeout: 12, handler: nil)
}
Timer is not exact: it does not fire exactly every 5 seconds like you asked. Apple say Timer is accurate to about 50 - 100ms. Hence we cannot expect that the first refresh will happen 5 seconds from now. We must allow for some tolerances. The further out you go, the bigger this tolerance have to become.

Creating a simple timer in WatchOS with Swift 4

Seemingly simple but I'm struggling...the code below crashes on the line setting the date of the workoutTimer. Also my WKInterfaceTimer isn't hooked up to an IBOutlet, does it need to be? I wanted to use it just for purposes of the time.
class InterfaceController {
var workoutTimer: WKInterfaceTimer!
var workoutStartTime: NSDate? = nil
func startWorkOutTimer() {
self.startWorkout()
if let test = self.workoutSecondsElapsed() {
print("timer seconds = \(test)")
}
}
func startWorkout() {
// To count up use 0.0 or less, otherwise the timer counts down.
workoutTimer.setDate(NSDate(timeIntervalSinceNow: 0.0) as Date)
workoutTimer.start()
self.workoutStartTime = NSDate()
}
func stopWorkout() {
workoutTimer.stop()
}
func workoutSecondsElapsed() -> TimeInterval? {
// If the timer hasn't been started then return nil
guard let startTime = self.workoutStartTime else {
return nil
}
// Time intervals from past dates are negative, so
// multiply by -1 to get the elapsed time.
return -1.0 * (self.workoutStartTime?.timeIntervalSinceNow)!
}
}
From Apple doc:
Do not subclass or create instances of this class yourself. Instead, define outlets in your interface controller class and connect them to the corresponding objects in your storyboard file.
Your app probably is crashing because your timer is nil, but for what you need you can use Timer class instead of WKInterfaceTimer.

How to synchronize different actions in Swift Language for IOS

I created a timer in one class, and tried to do something else in another class while timer works, and do other thing while timer stops. For example, show every second when timer works. I simplified the code as below. How to realize that?
import Foundation
import UIView
class TimerCount {
var timer: NSTimer!
var time: Int!
init(){
time = 5
timer = NSTimer.scheduledTimerWithTimeInterval( 1.0 , target: self, selector: Selector("update"), userInfo: nil, repeats: true)
}
func update(){
if(time > 0) {
time = time - 1
// do something while timer works
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}
class Main: UIView {
var Clock: TimerCount!
override func viewDidLoad() {
Clock = TimerCount()
//? do something else while clock works
// ? do other things while clock stops
// FOR EXAMPLE: show every second when timer works
if(Clock.time > 0){
println(Clock.time)
}else{
println("clocker stops")
}
}
}
viewDidLoad is most likely only going to be called once. You could simply make the update method be in your Main object and then pass that instance of main and that update method into the scheduledTimerWithTimerInterval call. Otherwise you need a new method in the Main class to call from the timerCount class and pass in the int for the current time.
This is in your Main class:
func updateMethodInMain(timeAsInt: Int){
//do stuff in Main instance based on timeAsInt
}
This is what you have in your timer class:
func update(){
if(time > 0) {
time = time - 1
instanceNameForMain.updateMethodInMain(time)
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}

Resources