I want to build an Onboarding Screen for an app especially for blind people. so the pages should play a small mp3 file.
But if I implement the code as attached, the behavior seems weird for me. If I change from page 1 to page 2 the first mp3 file stops and the second starts. as I want it. if I go further to page 3 the mp3 from page 2 continues and the mp3 from page 3 starts also. the both sound are overlaying each other.
I don't understand why .onDisappear sometimes seems not to be executed...
sometime Xcode throws a Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value --> solved by the hint from jnpdx - Thanks
in the .onDisappear section.
Here is a litte video of my problem. Maybe it's easier to understand what I mean if you see the video...
https://www.eckeonline.de/RPReplay_Final1643832563.MP4
import SwiftUI
import AVKit
struct ManualView: View {
let titleText = ["Willkommen", "Kategorien", "Neuigkeiten","Beispiel", "star.fill","cart", "star.fill","cart", "star.fill"]
let titleImage = ["character.book.closed.fill", "list.dash", "megaphone.fill","moon.zzz.fill", "star.fill","cart", "star.fill","cart", "star.fill"]
let soundFile = ["WelcomePage","CategoryPage", "NewsPage", "ExamplePage","WelcomePage", "WelcomePage", "WelcomePage","WelcomePage", "WelcomePage", "WelcomePage"]
var body: some View {
TabView {
ForEach(0..<4) { value in
OnboardingPage(soundFile: soundFile[value], title: titleText[value] , image: titleImage[value])
.tag(value)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
}
}
struct OnboardingPage: View {
var soundFile: String
let title: String
let image: String
@State var audioPlayer: AVAudioPlayer!
var body: some View {
VStack(spacing: 25) {
// PlayMP3View(soundFile: soundFile)
Text(title)
.padding(.horizontal, 20)
.font(.system(size: 200))
.lineLimit(1)
.minimumScaleFactor(0.25)
// .font(.largeTitle)
.foregroundColor(.primary)
.accessibility(hidden: true)
Spacer()
Image(systemName: image)
.font(.system(size: 200))
.foregroundColor(.primary)
.accessibility(hidden: true)
Spacer()
}
.onAppear() {
if let sound = Bundle.main.path(forResource: soundFile, ofType: "mp3") {
self.audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: sound))
self.audioPlayer.play()
}
}
.onDisappear() {
self.audioPlayer.pause()
}
}
}
Any time you use !, you risk a crash if that value ends up nil. The easiest solution here is to make audioPlayer an optional.
Second, in regards to your issue with overlapping audio, the tab probably isn't getting truly unloaded (and not called onDisappear
). Instead of using onAppear
and onDisappear
, you could use onChange
and watch the selection of the TabView
(some of the AudioPlayer
code is removed for brevity):
struct ManualView: View {
let titleText = ["Willkommen", "Kategorien", "Neuigkeiten","Beispiel", "star.fill","cart", "star.fill","cart", "star.fill"]
let titleImage = ["character.book.closed.fill", "list.dash", "megaphone.fill","moon.zzz.fill", "star.fill","cart", "star.fill","cart", "star.fill"]
let soundFile = ["WelcomePage","CategoryPage", "NewsPage", "ExamplePage","WelcomePage", "WelcomePage", "WelcomePage","WelcomePage", "WelcomePage", "WelcomePage"]
@State private var selection = -1
var body: some View {
TabView(selection: $selection) {
ForEach(0..<4) { value in
OnboardingPage(soundFile: soundFile[value],
title: titleText[value],
image: titleImage[value],
isActive: selection == value)
.tag(value)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.onAppear {
selection = 0 //trick to get onChange triggered on first screen
}
}
}
struct OnboardingPage: View {
var soundFile: String
let title: String
let image: String
var isActive : Bool
@State var audioPlayer: AVAudioPlayer?
var body: some View {
VStack(spacing: 25) {
Text(title)
}
.onChange(of: isActive) { newValue in
if newValue {
print("Play \(title)")
audioPlayer?.play()
} else {
print("Stop \(title)")
audioPlayer?.stop()
}
}
}
}