Search code examples
iosswiftavfoundationavaudioplayer

Unable to get AVAudioPlayer to stop playing


I have a class defined as Music.swift coded as follows:

import Foundation
import AVFoundation

class Music {

   var isPlaying: Bool = false
   public var backgrndSound = AVAudioPlayer()
      
   func isMusicPlaying() -> Bool {
      isPlaying = UserDefaults.standard.bool(forKey: "isPlaying")
      return isPlaying
   }
   
   func StartPlaying() {
      let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
      let url = URL(fileURLWithPath: path)
      do {
         self.backgrndSound = try AVAudioPlayer(contentsOf: url)
         self.backgrndSound.numberOfLoops = -1
         self.backgrndSound.play()
         UserDefaults.standard.setValue(true, forKey: "isPlaying")
      } catch {
         // couldn't load file :(
      }
   }
   
   func StopPlaying() {
      self.backgrndSound.pause()
      self.backgrndSound.stop()
   }  
}

On first load of the app, the music is automatically started with a call to StartPlaying(). That works just fine. Afterwards I have a settings menu that has a switch for the music play :


import UIKit
import AVFoundation

class SettingsViewController: UIViewController {

   @IBOutlet weak var swMusic: UISwitch!
   var myMusic = Music()
   
   override func viewDidLoad() {
        super.viewDidLoad()

      swMusic.isOn = UserDefaults.standard.bool(forKey: "isPlaying")
   }

   @IBAction func musicSwitch(_ sender: Any) {
      if swMusic.isOn == true {
         // turn on music
         myMusic.StartPlaying()
      } else {
         myMusic.StopPlaying()
      }
   }  
}

When I tap the switch it does fire StopPlaying() but the music in the background continues to play despite the tap.

I am not sure why that is, unless the AV object isn't accessible from the original creation and therefore can't stop it properly; but so far I have been unable to figure that out either.

Any help or suggestions would be greatly appreciated.


Solution

  • By instantiating a new instance of the Music class in SettingsViewController you're effectively creating a new AVAudioPlayer instance that knows nothing about the one already instantiated.

    Consider this code, which contains static properties and class methods:

    import Foundation
    import AVFoundation
    
    class Music 
    {
    
       public static var backgrndSound: AVAudioPlayer?
    
       // AVAudioPlayer already has an isPlaying property          
       class func isMusicPlaying() -> Bool 
       {
          return backgrndSound?.isPlaying ?? false
       }
       
       class func StartPlaying() 
       {
          let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
          let url = URL(fileURLWithPath: path)
          do 
          {
             backgrndSound = try AVAudioPlayer(contentsOf: url)
             backgrndSound?.numberOfLoops = -1
             backgrndSound?.play()
          } 
          catch 
          {
             // couldn't load file :(
          }
       }
       
       class func StopPlaying() 
       {
          backgrndSound?.pause()
          backgrndSound?.stop()
       }  
    }
    

    then access this using:

    Music.isMusicPlaying()
    Music.startPlaying()
    Music.stopPlaying()
    

    i.e. you'd not do var myMusic = Music()

    This way there will always be a single instance of the AVAudioPlayer, Music.backgrndSound

    This sample code changes backgrndSound to an optional ... you're effectively creating an unused AVAudioPlayer instance that is discarded as soon as you startPlaying.

    It also removes the unnecessary isPlaying property, as AVAudioPlayer already has a property for this purpose.