SwitfUI - Timer does not reset - swiftui

I apologise if the issue has been resolved, but I searched and could not find one.
I have a Timer which counts down from 3 minutes which works.
The issue is if the timer completes (Application completes) and I go back in to the app, the time no longer counts down.
The application has to be restarted for the timer to work.
#State private var timeRemaining = 3
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
Text("\(timeRemaining)")
.frame(width:40,height: 40)
.background(Color.red)
.foregroundColor(Color.white)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white,lineWidth: 2))
.onReceive(timer) {_ in
if self.timeRemaining > 0 && self.gamePlayStatus == true {
self.timeRemaining -= 1
if self.timeRemaining == 2 && self.gamePlayStatus == true {
self.readTimeRemaining = "\(self.timeRemaining) minutes remaining"
ReadSynthWord(word: self.readTimeRemaining)
} else if self.timeRemaining == 1 {
self.readTimeRemaining = "\(self.timeRemaining) minute remaining"
ReadSynthWord(word: self.readTimeRemaining)
} else if self.timeRemaining == 0 && self.gamePlayStatus == true {
ReadSynthWord(word: "Time is up")
}

I was doing more testing and found out that all I needed to do was to declare the time as a #State variable.
#State private var timeRemaining = 10
#State private var timer = Timer.publish(every: 1,tolerance: 0.5, on: .main, in: .common).autoconnect()
Thanks for all the help

Related

SwiftUI sound on timer ending is playing over and over

I created a timer.
When the timer ends it should play a complete sounds once.
It is a soundfile that is about 5 seconds long.
But what happens is that when the timer ends, the sounds is playing over and over again and it only plays the first 2 seconds of the sound.
It happens on the simulator and a real device.
I have no idea why this is happening. I've been googling voor quite some time but I can't find a solution.
I tried to connect the sound to buttons, and the play and stop functions work fine.
But even if I try to put the 'SoundManager.instance.stopSound()' into the stop button, it still doesn't stop playing the sound when I tap it when the timer ends.
Can someone tell me what I am doing wrong?
This is my code:
import SwiftUI
import AVKit
class SoundManager {
static let instance = SoundManager()
var player: AVAudioPlayer?
func playSound() {
guard let url = Bundle.main.url(forResource: "Tada", withExtension: ".mp3") else {return}
do {
player = try AVAudioPlayer(contentsOf: url)
player?.play()
} catch let error {
print("Error playing sound. \(error.localizedDescription)")
}
}
func stopSound() {
// Stop AVAudioPlayer
player?.stop()
}
}
struct TimerCountDown: View {
#State var countDownTimer = 30
#State var timerRunning = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("\(countDownTimer)")
.onReceive(timer) { _ in
if countDownTimer > 0 && timerRunning {
countDownTimer -= 1
}
else if countDownTimer < 1 {
SoundManager.instance.playSound()
}
else {
timerRunning = false
}
}
.font(.system(size: 80, weight: .bold))
.opacity(0.80)
HStack (spacing: 30) {
Button("Start") {
timerRunning = true
}
.padding()
.background(.blue)
.foregroundColor(.white)
.cornerRadius(8)
Button("Stop") {
timerRunning = false
}
.padding()
.background(.blue)
.foregroundColor(.white)
.cornerRadius(8)
Button("Reset") {
countDownTimer = 30
}
.padding()
.background(.red)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
}
struct TimerCountDown_Previews: PreviewProvider {
static var previews: some View {
TimerCountDown()
}
}
Your problem is in your .onReceive(timer) code. When the countDownTimer has reached 0, the timer is still publishing every second, so your code enters the second clause which plays the end timer sound. It does this every second. You should only do that if timerRunning and you should set that to false when your count reaches 0
.onReceive(timer) { _ in
if timerRunning {
if countDownTimer > 0 {
countDownTimer -= 1
if countDownTimer == 0 {
timerRunning = false
SoundManager.instance.playSound()
}
}
}
}
Note: Your code leaves the timer publishing at all times, and it is really only needed when the timer is running.
Check this answer for ideas on how to stop and restart the publishing of the timer.

Instance method 'onReceive(_:perform:)' requires that 'CountdownTimer' conform to 'Publisher'

I'm currently building out a project where a user could create multiple timers however I'm running into the following error. "Instance method 'onReceive(_:perform:)' requires that 'CountdownTimer' conform to 'Publisher'". I just can't figure out how to get Countdown Timer to conform to Publisher.
Here is the code for my timer object:
struct CountdownTimer: Identifiable {
let id = UUID()
let name: String
var minutes: Int
var seconds: Int
var countdown: Int {
let totalTime = seconds + (minutes * 60)
return totalTime
}
}
func timeString(time: Int) -> String {
return String(format: "%01i:%02i", minutes, seconds)
}
class CountdownTimers: ObservableObject {
#Published var timers = [CountdownTimer]()
}
My Content View:
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var countdownTimers = CountdownTimers()
#State private var showingAddSheet = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
Color.pastelGreen
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView {
ForEach(countdownTimers.timers) { timer in
ZStack {
CardView()
.overlay(
HStack {
VStack(alignment: .leading) {
Text("\(timer.countdown)")
.font(.custom("Quicksand-Bold", size: 30))
Text(" \(timer.timeString(time: timer.countdown))")
.font(.custom("Quicksand-Bold", size: 40))
.onReceive(timer){ _ in
if timers.countdown > 0 {
timers.countdown -= 1
}
}
The error is happening on the .OnReceive(timer) line. the minutes and seconds are created on a different "AddView" but I don't believe that the issue is related to that section.
Any help would be greatly appreciated.
You're shadowing the name timer. It appears first here:
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Then, later, you use it again here:
ForEach(countdownTimers.timers) { timer in
Swift is going to assume you always mean the timer in the most-recently-defined scope, so when you use .onReceive, it assumes you mean timer from the ForEach (which is of type CountdownTimer) and not the Timer defined earlier.
To fix this, use a different name in your ForEach and adjust your code (such as the Text elements) to use the new name.

SwiftUI "Game over.Restart?" message pop-up

I am new to SwiftUI and I am stuck here.
I have a "choose right flag game". Currently it can show you 3 flags and you have to choose the one that represents country named above. It keeps score of your game and number of rounds you've played( max is 20 ).
When you hit the 20 game sets all numbers to 0 and you start again.
I want to make it pop-up alert message with "Game over. Your result is (bad, average, good ( based on the amount of scores))!" and button "Restart".
I've made alert massage for every round, but simply "copy-paste" with some changes doesn't work.
How can I do it?
import SwiftUI
struct ContentView: View {
#State private var countries = [
"afghanistan",
"albania",.......//here goes list of countries
"zimbabwe"
].shuffled()
#State private var correctAnaswer = Int.random(in: 0...2)
#State private var score = 0
#State private var showingAlert = false
#State private var endGameAlert = false
#State private var alertTitle = ""
#State private var currentRound = 0
#State private var maxRound = 20
var body: some View {
NavigationView {
VStack{
ForEach((0...2), id:\.self) { number in
Image(self.countries[number])
.border(Color.black, width: 1)
.onTapGesture {
self.flagTapped(number)
}
}
Text("Your Score \(score), current round is \(currentRound) of \(maxRound) ")
Spacer()
}
.background(Image("background").resizable().scaledToFill().edgesIgnoringSafeArea(.all).blur(radius: 20))
.navigationBarTitle(Text(countries[correctAnaswer].uppercased()))
.alert(isPresented: $showingAlert) {
Alert(title: Text(alertTitle),
message:Text("Your score is \(score)"),
dismissButton: .default(Text("Continue")){
self.askQuestion()
})
}
}
}
func flagTapped(_ tag: Int){
if currentRound <= 20 {
if tag == correctAnaswer {
score += 1
currentRound += 1
alertTitle = "Correct"
} else {
score -= 1
currentRound += 1
alertTitle = "Wrong"
}
showingAlert = true
}
else {
endGameAlert = true
score = 0
currentRound = 0
}
}
func askQuestion() {
countries.shuffle()
correctAnaswer = Int.random(in: 0...2)
}
}
First change your flagTapped(_) into something like this:
func flagTapped(_ tag: Int){
if tag == correctAnaswer {
score += 1
alertTitle = "Correct"
} else {
score -= 1
alertTitle = "Wrong"
}
endGameAlert = currentRound == maxRound
// score = 0 // reset this at the "Restart" call
// currentRound = 0 // reset this at the "Restart" call
if currentRound < maxRound {
currentRound += 1
}
showingAlert = true
}
Then you can check for endGameAlert in the .alert:
.alert(isPresented: $showingAlert) {
if endGameAlert {
return Alert(title: Text("Game over"),
message:Text("Your result is <>!"),
dismissButton: .default(Text("Restart")){
self.resetGame() // reset game here
})
} else {
return Alert(title: Text(alertTitle),
message:Text("Your score is \(score)"),
dismissButton: .default(Text("Continue")){
self.askQuestion()
})
}
}

Triggering an animation when a boolean is true

I would like to make an animation that is like this: a ZStack with a view and over it, a purple rectangle, that initially has scale 0 and opacity 0 and is at the center of screen.
When the animation is triggered, two animations happen:
the rectangle's scale increases from 0 to 1 in 0.4 seconds.
the rectangle's opacity increases from 0 to 1 in 0.3 seconds and from 1 to 0 in 0.1 seconds.
This is me trying to do this:
struct ContentView : View {
#State private var scale: CGFloat = 0
#State private var scaleSeed: CGFloat = 0.1
#State private var counter: Int = 1
#State private var triggerFlashAnimation = false {
didSet {
scale = 0
counter = 1
}
}
var body: some View {
ZStack {
Text("Hello")
if triggerFlashAnimation {
Rectangle()
.background(Color.purple)
.scaleEffect(scale)
.onAppear {
scale += scaleSeed
counter += 1
}
}
}
I think this will animate the scale but this is very crude as I don't have any control over time.
I remember back in the day of CoreGraphics that you could define keyframes for time and values.
How do I do that with SwiftUI. I mean a precise animation defining keyframes and values for every parameter?
I googled around and found nothing.
SwiftUI doesn't have support for keyframes like we're used to in CoreAnimation (sources: https://stackoverflow.com/a/56908148/560942, https://swiftui-lab.com/swiftui-animations-part1/).
However, you can do a variation on chaining by using delay:
struct ContentView : View {
#State private var scale: CGFloat = 0
#State private var opacity: Double = 0
#State private var triggerFlashAnimation = false
func triggerAnimationActions() {
withAnimation(.linear(duration: 0.4)) {
scale = 1
}
withAnimation(.linear(duration: 0.3)) {
opacity = 1
}
withAnimation(Animation.linear(duration: 0.1).delay(0.3)) {
opacity = 0
}
// reset
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
triggerFlashAnimation = false
scale = 0
opacity = 0
}
}
var body: some View {
ZStack {
Text("Hello")
Button(action: { triggerFlashAnimation = true }) {
Text("Button")
}.onChange(of: triggerFlashAnimation) { (val) in
if val {
triggerAnimationActions()
}
}
if triggerFlashAnimation {
Rectangle()
.fill(Color.purple.opacity(opacity))
.frame(width: 100, height: 100)
.scaleEffect(scale)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
It's also probably worth looking into AnimatableModifier which lets you define an animatableData property where you can interpolate values, base exact frames on a certain state of the animation, etc, but, as far as I know, doesn't account for timing at all -- rather it just lets you set a state based on the precise value of the current animation. Good resource for reading about AnimatableModifier: https://www.hackingwithswift.com/quick-start/swiftui/how-to-animate-the-size-of-text

SwiftUI page control implementation

I need to implement something like an animated page control. And I don't want to use integration with UIKit if possible. I have pages array containing 4 views I need to switch between. I create the animation itself by changing the value of progress variable using timer. And I have the following code right now
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}
}
It is animating great - current page is moved to the left until it disappears, and the next page is revealed from the right taking its place. At the end of animation I add 1 to both indices and reset progress to 0. But once the animation (well not exactly an animation - I just change the value of progress using timer, and generate every state manually) is over, the page with index 1 is swapped back to page with index 0. If I check with debugger, currentIndex and nextIndex values are correct - 1 and 2, but the page displayed after animation is always the one I started with (with index 0). Does anybody know why this is happening?
The whole code follows
struct ContentView : View {
let limit: Double = 15
let step: Double = 0.3
let timer = Timer.publish(every: 0.01, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}.edgesIgnoringSafeArea(.vertical)
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct PageView: View {
#State var title: String
#State var imageName: String
#State var content: String
let imageWidth: Length = 150
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(imageName)
.resizable()
.frame(width: imageWidth, height: imageWidth)
.cornerRadius(imageWidth/2)
.clipped()
Text(content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}
in SceneDelegate:
if let windowScene = scene as? UIWindowScene {
let pages = (0...3).map { i in
PageView(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView(pages:
pages))
self.window = window
window.makeKeyAndVisible()
}
The following solution works. I think the problem was switching out views while SwiftUI tries to diff and update them is not something SwiftUI is good at.
So just use the same two PageView views and swap out their content based on the current index.
import Foundation
import SwiftUI
import Combine
struct PagesView : View {
let limit: Double = 15
let step: Double = 0.3
#State var pages: [Page] = (0...3).map { i in
Page(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
static let timerSpeed: Double = 0.01
#State var timer = Timer.publish(every: timerSpeed, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
var body: some View {
ZStack {
Button(action: {
self.isAnimating.toggle()
self.timer = Timer.publish(every: Self.timerSpeed, on: .current, in: .common).autoconnect()
}) { self.shape
}.offset(y: 300)
PageView(page: pages[currentIndex])
.offset(x: -CGFloat(pow(2, self.progress)))
PageView(page: pages[nextIndex])
.offset(x: CGFloat(pow(2, (self.limit - self.progress))))
}.edgesIgnoringSafeArea(.vertical)
.onReceive(self.timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct Page {
var title: String
var imageName: String
var content: String
let imageWidth: CGFloat = 150
}
struct PageView: View {
var page: Page
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(page.title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(page.imageName)
.resizable()
.frame(width: page.imageWidth, height: page.imageWidth)
.cornerRadius(page.imageWidth/2)
.clipped()
Text(page.content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}