Swift iOS : Serial Function Execution - swift

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
}

Related

Fill the CALayer with different color when timer reaches to 5 seconds swift ios

In my app i am using a third party library which does circular animation just like in the appstore app download animation. i am using the external file which i have placed in my project. it works but when the timer reaches to 5 seconds the fill color should be red. Currently the whole layer red color applies, i want to only apply the portion it has progressed. i am attaching the video and the third party file. Please can anyone help me with this as what changes should i make to the library file or is there better solution
same video link in case google drive link doesnt work
External Library link which i am using
video link
external third file file
My code snippet that i have tried:
func startGamePlayTimer(color: UIColor)
{
if !isCardLiftedUp {
self.circleTimerView.isHidden = false
self.circleTimerView.timerFillColor = color
Constants.totalGamePlayTime = 30
self.circleTimerView.startTimer(duration: CFTimeInterval(Constants.totalGamePlayTime))
}
if self.gamePlayTimer == nil
{
self.gamePlayTimer?.invalidate()
let timer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(updateGamePlayTime),
userInfo: nil,
repeats: true)
timer.tolerance = 0.05
RunLoop.current.add(timer, forMode: .common)
self.gamePlayTimer = timer
}
}
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7)
}
}
else
{
self.stopGamePlayTimer()
}
}
where circleTimerView is the view which animates as the time progresses
You can achieve that with CAKeyframeAnimation on the StrokePath.
I've Update the code in CircleTimer View as below. You can modified fill color and time as per your need.
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
// self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7) // <- Comment this line
}
}
else
{
self.stopGamePlayTimer()
}
}
open func drawFilled(duration: CFTimeInterval = 5.0) {
clear()
if filledLayer == nil {
let parentLayer = self.layer
let circleLayer = CAShapeLayer()
circleLayer.bounds = parentLayer.bounds
circleLayer.position = CGPoint(x: parentLayer.bounds.midX, y: parentLayer.bounds.midY)
let circleRadius = timerFillDiameter * 0.5
let circleBounds = CGRect(x: parentLayer.bounds.midX - circleRadius, y: parentLayer.bounds.midY - circleRadius, width: timerFillDiameter, height: timerFillDiameter)
circleLayer.fillColor = timerFillColor.cgColor
circleLayer.path = UIBezierPath(ovalIn: circleBounds).cgPath
// CAKeyframeAnimation: Changing Fill Color on when timer reaches 80% of total time
let strokeAnimation = CAKeyframeAnimation(keyPath: "fillColor")
strokeAnimation.keyTimes = [0, 0.25, 0.75, 0.80]
strokeAnimation.values = [timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = duration;
circleLayer.add(strokeAnimation, forKey: "fillColor")
parentLayer.addSublayer(circleLayer)
filledLayer = circleLayer
}
}
open func startTimer(duration: CFTimeInterval) {
drawFilled(duration: duration) // <- Need to pass duration here
if useMask {
runMaskAnimation(duration: duration)
} else {
runDrawAnimation(duration: duration)
}
}
UPDATE:
CAKeyframeAnimation:
You specify the keyframe values using the values and keyTimes properties.
For example:
let colorKeyframeAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorKeyframeAnimation.values = [UIColor.red.cgColor,
UIColor.green.cgColor,
UIColor.blue.cgColor]
colorKeyframeAnimation.keyTimes = [0, 0.5, 1]
colorKeyframeAnimation.duration = 2
This animation will run for the 2.0 duration.
We have 3 values and 3 keyTimes in this example.
0 is the initial point and 1 be the last point.
It shows which associated values will reflect at particular interval [keyTimes] during the animation.
i.e:
KeyTime 0.5 -> (2 * 0.5) -> At 1 sec during animation -> UIColor.green.cgColor will be shown.
Now to answer your question from the comments, Suppose we have timer of 25 seconds and we want to show some value for last 10 seconds, we can do:
strokeAnimation.keyTimes = [0, 0.25, 0.50, 0.60]
strokeAnimation.values = [timerFillColor.cgColor,timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = 25.0 // Let's assume duration is 25.0

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()
}
}

Fade out AVAudioEngine AVAudioPlayerNode

I´m trying to fade out an mp3 file using AVAudioEngine and AVAudioPlayerNode. I cannot figure it out. Can anybody please help in Swift?
I tried some things already. Everything works quite good but not as smooth as I would like it to be.
var fadeOutPlayer: Float = playerVolume
var fadeOutQuinte: Float = playerQuinteVolume
while fadeOutPlayer > 0.0 {
player.volume = fadeOutPlayer
playerQuinte.volume = fadeOutQuinte
fadeOutPlayer -= 0.001
fadeOutQuinte -= 0.001
usleep(100)
}
I also tried a timer (which doesn't do a fade):
if !isTimerFadeOutRunning {
player.volume = 0
playerQuinte.volume = 0
counter = 0
timerFadeOut = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(runTimerFadeOut), userInfo: nil, repeats: true)
isTimerFadeOutRunning = true
}
}
#objc func runTimerFadeOut() {
counter += 0.1
if counter == 0.2 {
player.pause()
playerQuinte.pause()
}
}
I've attached an AVAudioUnitEQ to my flow before mainMixerNode and then controlled the globalGain. The following code shows the idea:
private func manageFadeOut(volume: Double, fadeOutSecs: Double) {
let timeSecs = 0.001
DispatchQueue.main.asyncAfter(deadline: .now() + timeSecs) {
if !self.isFadingOut { return }
for fx in self.fadeOutEffects { fx.globalGain = Float(-96 * (1.0 - volume)) }
if volume <= 0 {
for player in self.players { player.stop() }
self.isFadingOut = false
return
}
let fade = timeSecs / fadeOutSecs
self.manageFadeOut(volume: volume - fade, fadeOutSecs: fadeOutSecs)
}
}

Swift - Animate Image sequence

I have a list of images that I want to animate in Swift. I have tried to find the best possible way to do that - but I still have some problems.
The way I animate my image-list right now is by:
var animatedImage = UIImageView()
var velkomstImgList = [UIImage]()
override func viewDidAppear(_ animated: Bool) {
animatedImage.frame = CGRect(x: 0, y: 0, width: self.scrollView.frame.width, height: 200)
animatedImage.contentMode = .scaleAspectFit
scrollView.addSubview(animatedImage)
animateVelkomst()
}
func animateVelkomst() {
for i in 0...150 {
velkomstImgList.append(UIImage(named:"Velkomst_\(i).png")!)
if i == 150 {
self.animatedImage.animationImages = self.velkomstImgList
self.animatedImage.animationDuration = 5.0
self.animatedImage.startAnimating()
}
}
}
It works and the animation appears as expected. But the for loop in the beginning takes pretty long time and I dont think this is the right way to show the animation. Any suggestions on how I should animate the image sequence?
loading is slow because you are attempt to load 150 images same time. you can use timer and load image when you need it.
let imageCount = 150
var frameIndex = 0
var timer: Timer?
func startAnimation() {
frameIndex = 0
let deltaTime = 5.0 / Double(imageCount)
timer = Timer(timeInterval: deltaTime, target: self, selector: #selector(updateFrame), userInfo: nil, repeats: true)
}
func stopAnimation() {
timer?.invalidate()
timer = nil
}
#objc func updateFrame(_ timer: Timer) {
self.animatedImage.image = UIImage(named:"Velkomst_\(frameIndex).png")
frameIndex += 1
if frameIndex > imageCount {
frameIndex = 0
}
}

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.