SwiftUI sound on timer ending is playing over and over - swiftui

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.

Related

LazyVStack in ScrollView has jittery auto scroll

I have a chat app, where whenever a chat room is opened, I need the view to scroll to the bottom as soon as the messages are fetched.
The thing is that although it does scroll perfectly when a new message is received or sent (see ViewModel down below), it is very jittery when I tell it to scroll right after the first batch of messages is fetched, which happens once as soon as the view appears.
After a lot of trial and error, I realized that if I add a small delay to the scroll, it'll improve but not completely! It is like it's trying to scroll to the very bottom, but it'll fail just for a few inches. I also realized that if I add a bigger delay, like 2 seconds, it'll scroll just fine.
Here's the messages list view:
struct MessagesView: View {
#StateObject private var viewModel = ViewModel()
// -----------------------
let currentChatRoom: ChatRoom
// -----------------------
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VStack {
ScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(viewModel.messages) { message in
MessageView(message: message)
.id(message.id)
.onTapGesture {
viewModel.shouldDismissKeyboard = true
}
}
}
.onChange(of: viewModel.shouldScrollToMessageId) { messageId in
if let messageId = messageId, !messageId.isEmpty {
proxy.scrollTo(messageId, anchor: .bottom)
}
}
}
}
VStack(alignment: .leading) {
if chatEnvironment.isOtherUserTyping {
TypingIndicationView()
}
BottomView()
.padding(.bottom, 4)
}
}
}
.onAppear {
viewModel.setUp(currentChatRoom: currentChatRoom)
}
}
}
As you can see, it’s viewModel.shouldScrollToMessageId that’s responsible for "auto-scrolling" to the last message.
Here’s MessageView:
fileprivate struct MessageView: View {
let message: Message
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 1) {
Text(message.user.isCurrentUser == true ? "You" : "\(message.user.username)")
.foregroundColor(message.user.isCurrentUser == true ? .customGreen : .customBlue)
.multilineTextAlignment(.leading)
.font(.default(size: 16))
.padding(.bottom, 1)
if let imageURL = message.postSource?.imageURL, !imageURL.isEmpty {
VStack(alignment: .leading) {
WebImage(url: .init(string: imageURL))
.resizable()
.indicator(.activity)
.scaledToFill()
.frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.width / 1.45)
.cornerRadius(25)
}
}
Text(message.text)
.foregroundColor(.white)
.multilineTextAlignment(.leading)
.font(.default(size: 16))
}
Spacer()
}
.padding(.bottom, 8)
.padding(.horizontal)
.background(
Color.black
)
}
}
Here’s the ViewModel:
class ViewModel: ObservableObject {
#Published var messages = [Message]()
#Published var text = ""
#Published var shouldScrollToMessageId: String?
#Published var currentChatRoom: ChatRoom?
// -----------------------------
private var isInitialized = false
// -----------------------------
func setUp(currentChatRoom: ChatRoom) {
guard !isInitialized else { return }
isInitialized.toggle()
// -----------------------------
self.currentChatRoom = currentChatRoom
// -----------------------------
getFirstBatchOfMessages(chatRoom: chatRoom)
subscribeToNewMessages()
}
private func getFirstBatchOfMessages(chatRoom: ChatRoom) {
messagesService.getMessages(chatRoomId: chatRoom.id) { [weak self] messages in
DispatchQueue.main.async {
self?.messages = messages
}
self?.scrollToBottom(delay: 0.15)
}
}
private func subscribeToNewMessages() {
...
if !newMessages.isEmpty {
self?.scrollToBottom(delay: 0)
}
}
func scrollToBottom(delay: TimeInterval) {
DispatchQueue.main.async {
self.shouldScrollToMessageId = self.messages.last?.id
}
}
func sendMessage() {
...
scrollToBottom(delay: 0)
}
}
Here, scrollToBottom is responsible for notifying the MessagesView that shouldScrollToMessageId's value changed and that it should scroll to the last message.
Any help will be much appreciated!!!
I am also writing an application with a chat on SwiftUI and I also have a lot of headaches with a bunch of ScrollView and LazyVStack.
In my case, I load messages from the CoreData and display them in a LazyVStack, so in my case, scrolling to the last message does not work, it seems to me simply because a bottom last view did not render, because rendering starts from the top.
Therefore, I came up with a solution with placing an invisible view at the bottom and assigned it a static ID, in my case -1:
VStack(spacing: 0) {
LazyVStack(spacing: 0) {
ForEach(messages) { message in
MessageRowView(viewWidth: wholeViewProxy.size.width, message: message)
.equatable()
}
}
Color.clear.id(-1)
.padding(.bottom, inputViewHeight)
}
I call scroll to this view:
.onAppear {
scrollTo(messageID: -1, animation: nil, scrollReader: scrollReader)
}
And it works... but sometimes...
Sometimes it works correctly, sometimes it stops without scrolling a couple of screens to the end. Looks like LazyVStack is rendering the next views after the scrollTo has finished its work. Also, if add scrolling with some delay it may works better, but not always perfect.
I hope this can be a little helpful and if you find a stable solution I will be very happy if you share :)

Is there a way to create a new session of WKExtendedRuntimeSession in SwiftUI?

I've created a simple countdown timer in watchOS. When you press start the timer starts and session.start is declared as well. Everything works fine in the background. Then when it stops either by the button or by the timer the session will invalidate.
By the way that I'm reading the documentation here
It states, "you can only schedule one session at a time; if you need to reschedule a session, invalidate the current session, and schedule a new one."
So my question is, how do I create a new session in SwiftUI? Here is my code:
import SwiftUI
struct TestView: View {
#State var start = false
#State var number = 10
#State var time = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var session = WKExtendedRuntimeSession()
var body: some View {
VStack {
Text("\(number)")
Spacer()
Button(action: {
self.start.toggle()
if self.start {
session.start()
} else {
session.invalidate()
}
}) {
Text(self.start ? "Stop" : "Start")
}
}.onReceive(self.time) { (_) in
if self.start {
if number > 0 {
number -= 1
} else {
self.start.toggle()
number = 10
session.invalidate()
}
}
}
}
}
It is better to do in class, like Coordinator
class SessionCoordinator {
private var session: WKExtendedRuntimeSession?
func start() {
guard session?.state != .running else { return }
// create or recreate session if needed
if nil == session || session?.state == .invalid {
session = WKExtendedRuntimeSession()
}
session?.start()
}
func invalidate() {
session?.invalidate()
}
}
struct TestView: View {
let coordinator = SessionCoordinator()
#State var start = false
#State var number = 10
#State var time = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("\(number)")
Spacer()
Button(action: {
self.start.toggle()
if self.start {
coordinator.start()
} else {
coordinator.invalidate()
}
}) {
Text(self.start ? "Stop" : "Start")
}
}.onReceive(self.time) { (_) in
if self.start {
if number > 0 {
number -= 1
} else {
self.start.toggle()
number = 10
coordinator.invalidate()
}
}
}
}
}

How to create an instruction prompt after pressing a button?

I'm creating an instruction dialog that will start after pressing a button. I want the instruction to be displayed in the same view and just get replaced after a few seconds to display the next instruction.
The instructions are stored in a text array.
Any help will be greatly appreciated as I'm having trouble finding a solution
#State var showPrompt = false
#State var textToUpdate = ""
let instructionTo = ["hi", "bye"]
VStack {
Text(textToUpdate)
.frame(width: UIScreen.main.bounds.width - 80, height: 350)
Button(action: {
if(self.showPrompt == false)
{
self.showPrompt = true
}
else{
self.showPrompt = false
}
}) {
if(self.showPrompt == false)
{
Text("Start")
}
else
{
Text("Stop")
// some for loop that would update the text
// and have a delay of 5 seconds between each
// index in the array
}
}
}
you can add another State variable for the Text value.
here is a sample code, assuming that you want to start changing the text after tapping on the start button after 5 seconds. I just added an empty string to show an empty text at first.
struct TestView: View {
#State var showPrompt = false
#State var textIndex = 0
let instructionTo = ["", "hi", "bye"]
var body: some View {
VStack {
Text(instructionTo[textIndex])
.frame(width: UIScreen.main.bounds.width - 80, height: 350)
Button(action: {
if(self.showPrompt == false)
{
self.showPrompt = true
// update text value
self.updateText()
}
else{
self.showPrompt = false
}
}) {
if(self.showPrompt == false)
{
Text("Start")
}
else
{
Text("Stop")
}
}
}
}
func updateText() {
if self.textIndex < self.instructionTo.count - 1 {
// update text after 5 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.textIndex += 1
self.updateText()
}
}
}
}

How to display an image for one second? SWIFTUI

I want to show an image for one second when the player achieve the goal. I thought about putting an alert but it will slow down the game. I just want the image to stay for one second at the top of the screen and disappear until the next achievement.
Example code is below:
var TapNumber = 0
func ScoreUp() {
TapNumber += 1
if TapNumber == 100 {
showImage()
}
}
func showImage() {
// this is the function I want to create but. I do not know how
show image("YouEarnedAPointImage") for one second
}
Here is a demo of possible approach
struct DemoShowImage1Sec: View {
#State private var showingImage = false
var body: some View {
ZStack {
VStack {
Text("Main Content")
Button("Simulate") { self.showImage() }
}
if showingImage {
Image(systemName: "gift.fill")
.resizable()
.frame(width: 100, height: 100)
.background(Color.yellow)
}
}
}
private func showImage() {
self.showingImage = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showingImage = false
}
}
}

SwiftUi Transition / Animation

I am new to SwiftUI and have managed to build simple game.
All works except my plan to implement some sort of image animation / transition to introduce the image into the view.
The images are formatted correctly, but too abrupt and hoped to soften the image glow
I have tried by using the animation method, but has not mad a difference.
I hope someone can point me in the right direction.
var body: some View {
NavigationView {
ZStack {
Image("background")
.resizable()
.aspectRatio(contentMode: .fill)
.scaledToFill()
.edgesIgnoringSafeArea(.all)
VStack {
Text("Tap Correct Image")
.font(.headline)
.foregroundColor(Color.blue)
ForEach((0...2), id: \.self) { number in
Image(self.naijaObject[number])
.resizable()
.frame(width: 100, height: 100)
.border(Color.black,width: 1)
.transition(.slide)
.animation(.easeInOut(duration: 1))
.onTapGesture {
self.pictureTapped(number)
}
}
.navigationBarTitle(Text(processCorrectAnswer))
.alert(isPresented: $showAlert) {
Alert(title: Text("\(alertTitle), Your score is \(score)"), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}//End of VStack
}//End of
} //End of Naviagtion View
} //End of View
//Function to action which object is tapped
func pictureTapped(_ tag: Int) {
if tag == correctAnswer {
score += 1
alertTitle = "Correct"
} else {
score -= 1
alertTitle = "Wrong"
}
showAlert = true
}
//Function to continue the game
func askQuestion() {
naijaObject.shuffle()
correctAnswer = Int.random(in: 0...2)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I think I understand, what you want to achieve, but in future better to present more code. Here I used DispatchQueue.main.asyncAfter(deadline: .now() + 1) where I change the variable showAlert to true. And it works smoothly, hope it'll help you:
struct QuestionGame: View {
#State private var showAlert = false
#State private var score: Int = 0
#State private var correctAnswer = 1
#State private var tapped = false
var body: some View {
NavigationView {
VStack {
Text("Tap Correct Image")
.font(.headline)
ForEach((0...2), id: \.self) { number in
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(self.tapped && number == self.correctAnswer ? .green : .black) // you can do anything for animation
.animation(.easeInOut(duration: 1.0))
.onTapGesture {
self.pictureTapped(number)
}
}
.navigationBarTitle(Text("answer the question"))
.alert(isPresented: self.$showAlert) {
Alert(title: Text("Your score is \(score)"), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}
}
}
func pictureTapped(_ tag: Int) {
tapped = true
if tag == correctAnswer {
score += 1
} else {
score -= 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showAlert = true
}
}
//Function to continue the game
func askQuestion() {
tapped = false
showAlert = false
correctAnswer = Int.random(in: 0...2)
}
}
struct QuestionGame_Previews: PreviewProvider {
static var previews: some View {
QuestionGame()
}
}
P.S. and no, I didn't find the way to show alert smoothly. For this case may be better to use ZStack and change the alert (or any other view) opacity from 0 to 1 (with animation), when you tap on image.