Search code examples
swiftuiavaudioplayer

swiftUI: strange behavior with AVAudioPlayer


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


Solution

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