How to make the timer goes faster than 1 second - swift

I have a text that gets updated every second I am using
#State var timeRemaining = 10
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Text("Time Left: \(self.timeRemaining)").onReceive(timer) { _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}
if self.timeRemaining <= 0 {
print("Time is up!")
}
}
But I want it to go faster than just every one second.
Thank you

Do:
let timer = Timer.publish(every: 1 / 2, on: .main, in: .common).autoconnect()
the timer will only take whole numbers not decimals. so you can do some math and figure how much faster you want it

The answer is:
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
I was just calling another timer without noticing the different names.

Related

Changing duration for Slider animation doesn't make any difference

I'm trying to animate Slider smoothly. My original problem was animating it for 1 second every second for a small value (building an audio player scrubber), but the animation was a split second instead of a full second.
To isolate a problem, I built a playground where I'm trying to animate Slider change from 0 to maxValue for maxValue number of seconds. However, whatever maxValue is, the animation happens in a fraction of a second.
Here is the code:
struct SliderTest: View {
#State private var sliderValue = 0.0
let maxValue = 30.0
var body: some View {
VStack(spacing: 10) {
Slider(value: $sliderValue, in: 0...maxValue)
Button("Animate Slider") {
withAnimation(.linear(duration: maxValue-sliderValue)) {
sliderValue = maxValue
}
}
Button("Reset to Random") {
sliderValue = Double.random(in: 0..<maxValue)
}
Button("Reset") {
sliderValue = 0
}
}
}
}
Here you can get the code with a preview for Swift Playgrounds:
https://gist.github.com/OgreSwamp/6e6423d6ef2d26425e3f993042ac208d
First of all, animation is not working here because when it comes into withAnimation it will assign sliderValue to maxValue immediately. That causes the reason above like you said.
For the problem, I will separate the slider just for change UI depends on sliderValue only. Then building sliderTimer for handle the change value of slider and everytime the timer is excuted it will automatically increase sliderValue by step.
The code will be like this
struct ContentView: View {
#State private var sliderValue = 0.0
private var maxValue = 30.0
private var sliderTimeAnimation = 2.0
private var step = 1.0
#State private var sliderTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack(spacing: 10) {
Slider(
value: $sliderValue,
in: 0...maxValue
).onReceive(sliderTimer) {
_ in
sliderValue += step
if (sliderValue > maxValue) {
stopTimer()
}
}
.onAppear() {
self.stopTimer()
}
Button("Animate Slider") {
startTimer()
}
Button("Reset to Random") {
stopTimer()
sliderValue = Double.random(in: 0..<maxValue)
}
Button("Reset") {
stopTimer()
sliderValue = 0
}
}
}
func startTimer() {
self.sliderTimer = Timer.publish(every: sliderTimeAnimation * step / maxValue, on: .main, in: .common).autoconnect()
}
func stopTimer() {
self.sliderTimer.upstream.connect().cancel()
}
}
In this case, I set sliderTimeAnimation is 2.0, step is 1.0 (if you need faster animation you can set step is 0.5 or lower) and the max value is 30.0 like your code.
The result is like this.

Why is my animation quicker than the timer?

I have an animation that I want to coincide with the timer but right now it ends with 6seconds left of the timer. How do I get the animation to match? Also, how would i go about repeating the animation for the countdown of the iteration, i?
The code has the animation, in a circle, and then preset timer of 30s (which will eventually be a slider input). I will also eventually want to include a pause, and stop button for the timer which will need to coincide with the animation
import UIKit
var timer = Timer()
var time = 30
var i = 5
class ViewController: UIViewController {
#IBOutlet weak var displayTime: UILabel!
let shape = CAShapeLayer()
private let label: UILabel = {
let label = UILabel()
label.text = String(i)
// change label to update i
label.font = .systemFont(ofSize: 36, weight: .light)
return label
}()
func countdown() {
displayTime.text = String(time)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector:
#selector(doCountdown), userInfo: nil, repeats: true)
}
override func viewDidLoad() {
super.viewDidLoad()
label.sizeToFit()
view.addSubview(label)
label.center = view.center
let circlePath = UIBezierPath(arcCenter: view.center, radius: 150, startAngle: -
(.pi / 2), endAngle: .pi * 2, clockwise: true)
let trackShape = CAShapeLayer()
trackShape.path = circlePath.cgPath
trackShape.fillColor = UIColor.clear.cgColor
trackShape.lineWidth = 15
trackShape.strokeColor = UIColor.lightGray.cgColor
view.layer.addSublayer(trackShape)
shape.path = circlePath.cgPath
shape.lineWidth = 15
shape.strokeColor = UIColor.blue.cgColor
shape.fillColor = UIColor.clear.cgColor
shape.strokeEnd = 0
// cg = core graphics
view.layer.addSublayer(shape)
let button = UIButton(frame: CGRect(x: 20, y: view.frame.size.height-70, width:
view.frame.size.width-40, height: 50))
view.addSubview(button)
button.setTitle("Animate", for: .normal)
button.backgroundColor = .systemGreen
button.addTarget(self, action:#selector(didTapButton), for: .touchUpInside)
}
#objc func didTapButton() {
countdown()
// Animate
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.toValue = 1
animation.duration = Double(time)
// duration of animation
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
shape.add(animation, forKey: "animation")
}
#objc func doCountdown() {
time = time - 1
displayTime.text = String(time)
if time == 0 {
i = i - 1
time = 30
}
if i == 0 {
label.text = "0"
timer.invalidate()
}
}
}
Your implementation does not work because you are using a naive implementation of countdown.
A timer is not guaranteed to fire exactly after the given amount of time. It won't fire exactly after one second. The accuracy of Timer is 50-100 milliseconds. Therefore the total possible error can add up to 30*100 milliseconds, that is 3 entire seconds.
Instead, you have to use a Timer that will update your UI more often:
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(doCountdown), userInfo: nil, repeats: true)
And that also means you have to calculate your time differently. First of all, store the expected time of animation end:
// declare instance properties
private var animationEnd = Date()
private var timer: Timer? {
didSet {
// invalidate when nil is assigned
oldValue?.invalidate()
}
}
func startCountdown() {
// store the start time - 30 seconds in the future
animationEnd = Date().addingTimeInterval(TimerInterval(time))
// start the timer
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
guard let self = self else { return }
let remainingTime = max(0, self.animationEnd.timeIntervalSinceNow)
if remainingTime == 0 {
// invalidate the timer
self.timer = nil
}
// convert time to seconds
let remaininingSeconds = Int(remainingTime) ?? 0
self.displayTime.text = "\(remaininingSeconds)"
}
}
//
That's all.
If you want to pause & resume the timer, the process is the same. Invalidate the timer, store the current time (e.g. timePaused = Date) and when resumed, just add the difference between current time and timePaused to animationEnd and restart the timer.
Also, please, don't put variables on file level. Always put them to the scope of classes. Otherwise you will soon have problems.
I think these two variables are the source of your problem -
var time = 30
var i = 5
Can you try deleting the i variable and use this updated implementation -
#objc func doCountdown() {
time -= 1
displayTime.text = String(time)
if time == 0 {
timer.invalidate()
}
}

SwiftUI: Implement a simple timing slider

I have a line slider that looks like this:
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 200 * (timeRemaining), y: 0))
}
.stroke(Color.orange, lineWidth: 4).opacity(0.5)
The timeRemaining will go from 2s, 1s, 0s making the line slider to decrease from right to left. How can I change this implementation so that the slider will decrease more like this:
400,399,398...2,1,0
which will make the line slider looks smoother.
Here is the timer declaration:
#State var timeRemaining = 2
#State var timer: Timer.TimerPublisher = Timer.publish (every: 1,
on: .main, in: .common)
.onReceive(timer) { time in
if timeRemaining > 0 {
timeRemaining -= 1
}
I think I need to make some changes in the onReceive part.
To have a finer granularity and a smoother animation you simply need to scale down the update interval. Since 2 / 400 is 0.005 you should use this for your timer and your count down
#State var timeRemaining = 2.0 // Use Double
#State var timer: Timer.TimerPublisher = Timer.publish (every: 0.005, on: .main, in: .common)
...
.onReceive(timer) { time in
if timeRemaining > 0 {
timeRemaining -= 0.005
}

Swift Countdown Function

I am attempting to create a countdown timer for a game using SpriteKit, but whenever I try to run countDown(), my game freezes. I am pretty sure my logic is correct here. I do not know what is going on.
func countDown(){
let countDownWait = SKAction.wait(forDuration: 1.0)
repeat {
self.run(countDownWait){
self.countDownTime -= 1
}
} while (self.countDownTime > 0)
if self.countDownTime == 0{
self.runGameOver()
}
}
you can do some checking in the update func for time passed or use a SKAction to track time similar to what you were doing in your code
let someLabel = SKLabelNode()
func countdown() {
var offset: Double = 0
for x in (0...10).reversed() {
run(SKAction.wait(forDuration: offset)) {
someLabel.text = "\(x)"
if x == 0 {
//do something when counter hits 0
//self.runGameOver()
}
else {
//maybe play some sound tick file here
}
}
offset += 1.0
}
}
Here's how I solved this problem for my Swift/SpriteKit 'Breakout' application. I wanted a countdown from 5 to 1 onthe main game screen but before the ball started to move. I Added these functions and then a call to countdown(5) at the end of didMoveToView: Notice the ball.physicsBody!.applyImpulse(CGVectorMake(20, 20)) as the last step of endCountdownwhich starts the ball and effectively starts the game.
func countdown(count: Int) {
countdownLabel.horizontalAlignmentMode = .Center
countdownLabel.verticalAlignmentMode = .Baseline
countdownLabel.position = CGPoint(x: size.width/2, y: size.height*(1/3))
countdownLabel.fontColor = SKColor.whiteColor()
countdownLabel.fontSize = size.height / 30
countdownLabel.zPosition = 100
countdownLabel.text = "Launching ball in \(count)..."
addChild(countdownLabel)
let counterDecrement = SKAction.sequence([SKAction.waitForDuration(1.0),
SKAction.runBlock(countdownAction)])
runAction(SKAction.sequence([SKAction.repeatAction(counterDecrement, count: 5),
SKAction.runBlock(endCountdown)]))
}
func countdownAction() {
count--
countdownLabel.text = "Launching ball in \(count)..."
}
func endCountdown() {
countdownLabel.removeFromParent()
ball.physicsBody!.applyImpulse(CGVectorMake(20, 20))
}
Try this solution to run countdown...
var seconds = 7200
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
runTimer()
}
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(self.updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
if seconds < 1 {
timer.invalidate()
//Send alert to indicate time's up.
} else {
seconds -= 1
timerLabel.text = timeString(time: TimeInterval(seconds))
}
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
Have you considered using a timer? Here is a good tutorial. It might take you 20 minutes to go through it or so, but it goes into detail about starting, stopping, pausing, and more for timers.

Swift iOS : Serial Function Execution

I wish to create a function that runs 3 internal functions 1 after the 1 and repeat that process 3 times. The problem I am finding is that all 3 functions execute simultaneously.
Currently when I test the app it turns green and that is it. Where I would like it to turn red for 30 seconds, then green for 10, then repeat that process 3 times.
I would like to keep the process open so I may add conditions such as "Play a sound at 4 seconds in) and allow the overall times to changeable in the future.
CODE:
import UIKit
var timer = Timer()
var intCount = 0
var seconds = 0
let greenColor = UIColor(red: 0/255.0, green: 255/255.0, blue: 0/255.0, alpha: 1.0)
let redColor = UIColor(red: 255/255.0, green: 0/255.0, blue: 0/255.0, alpha: 1.0)
// Start Button
#IBAction func Start(_ sender: Any)
{
repeat
{
performTimer()
intCount += 1
}while intCount < 2
StartHide.isHidden = true
}
func performTimer()
{
timer1() // Execute -> Finish
timer2() // Execute -> Finish
}
func timer1()
{
// Set Seconds
seconds = 30
// Colour for 30 Seconds
view.backgroundColor = redColor
// Run Timer
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerPage.counter), userInfo: nil, repeats: true)
}
func timer2()
{
// Set Seconds
seconds = 10
// Colour for 10 Seconds
view.backgroundColor = greenColor
// Run Timer
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerPage.counter), userInfo: nil, repeats: true)
}
func counter()
{
seconds -= 1
if (seconds == 0)
{
timer.invalidate()
}
}
Surely there is a simple way to stop functions running simultaneously?
The timers are running simultaneously (a better word is asynchronously) because of this:
repeat
{
performTimer()
intCount += 1
}while intCount < 2
and this:
func performTimer()
{
timer1() // Execute -> Finish
timer2() // Execute -> Finish
}
The call to timer1 will return as soon as the timer has been created, not when the timer counts down to 30 seconds. Same goes for timer2 and performTimer methods.
So you are creating all these timers in a very short time and they all share a seconds variable. That's bad.
To fix this, you should only create new times after the one running has finished.
let count = 0
let seconds = 0
var currTimer: Timer!
func start() {
timer30s()
}
func timer30s() {
seconds = 30
currTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: selector(handleTimer30s), userInfo: nil, repeats: true)
}
func timer10s() {
seconds = 10
currTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: selector(handleTimer10s), userInfo: nil, repeats: true)
}
func handleTimer30s() {
if seconds > 0 {
seconds -= 1
return
}
currTimer.invalidate()
timer10s()
view.backgroundColor = redColor
}
func handleTimer10s() {
if seconds > 0 {
seconds -= 1
return
}
count += 1
currTimer.invalidate()
if count < 3 {
timer30s()
}
view.backgroundColor = greenColor
}