Search code examples
iosswiftavfoundationswift4avaudioplayer

I cannot play music in the background (Xcode 9, Swift 4)


As the question suggests, I need my app to play music in the background like most of the music players. I was trying to find something online but they do not work. Could somebody look at my problem? I think it might be the case that apple automatically kills my app in the background since it can play in the background for a couple seconds (Probably like 10s).

Moreover, I want to keep the music playing in another viewController. For now, whenever I go back to my root viewController, the music always stops.

Here is my code.

//
//  MusicLibraryTableViewController.swift
//  WF
//
//  Created by Bo Ni on 7/1/18.
//  Copyright © 2018 Bo Ni. All rights reserved.
//

import UIKit
import AVFoundation

class MusicLibraryTableViewController: UITableViewController, AVAudioPlayerDelegate{

    let songs: [String] = ["After Master"]

    let producer: [String] = ["August Wu/Zoro"]

    let identifier = "musicIdentifier"

    var audioPlayer: AVAudioPlayer?

    var isAudioPlayerPlaying = false

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }

    override func tableView(_ tableView: UITableView, cellForRowAt
        indexPath: IndexPath) -> UITableViewCell {

        let cell: MusicTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: identifier) as! MusicTableViewCell
        cell.producerLabel.text = producer[indexPath.row]
        cell.musicNameLabel.text = songs[indexPath.row]
        cell.playButton.setImage(UIImage(named:"Play Button")?.withRenderingMode(.alwaysOriginal), for: UIControlState.normal)

        return cell
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 64.0
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return songs.count
    }

    override func tableView(_ tableView: UITableView,
        didSelectRowAt indexPath: IndexPath) {

        let cell: MusicTableViewCell = self.tableView.cellForRow(at: indexPath) as! MusicTableViewCell

        let music = NSURL.fileURL(withPath: Bundle.main.path(forResource: songs[indexPath.row], ofType: "mp3")!)
        do {
            try audioPlayer = AVAudioPlayer(contentsOf: music)
        } catch{
            print(error.localizedDescription)
        }

        UIApplication.shared.beginReceivingRemoteControlEvents()

        audioPlayer?.delegate = self
        audioPlayer?.prepareToPlay()

        if isAudioPlayerPlaying == true{
            stopAudio()
            isAudioPlayerPlaying = false
            cell.playButton.setImage(UIImage(named: "Play Button"), for: UIControlState.normal)
        }else{
            prepareAudio()
            playAudio()
            isAudioPlayerPlaying = true
            cell.playButton.setImage(UIImage(named: "Stop Button"), for: UIControlState.normal)
        }
    }

    func prepareAudio(){
        do {
            //keep alive audio at background
            try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
        } catch _ {
        }
        do {
            try AVAudioSession.sharedInstance().setActive(true)
        } catch _ {
        }
    }

    func playAudio(){
        if let player = audioPlayer{
            player.play()
        }
    }

    func stopAudio(){
        if let player = audioPlayer{
            player.stop()
        }
    }
}

Solution

  • After spending a couple of days on this issue, I finally figured it out. In swift 4, I think we need to add a couple of lines of code to the appDelegate class under the project directory. Like this:

    import UIKit
    import AVFoundation
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        var window: UIWindow?
    
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
            // Override point for customization after application launch.
            let session = AVAudioSession.sharedInstance()
            do{
                try session.setActive(true)
                try session.setCategory(AVAudioSessionCategoryPlayback)
            } catch{
                print(error.localizedDescription)
            }
            return true
        }
    
        func applicationWillResignActive(_ application: UIApplication) {
            // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
            // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
        }
    
        func applicationDidEnterBackground(_ application: UIApplication) {
            // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
            // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
        }
    
        func applicationWillEnterForeground(_ application: UIApplication) {
            // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
        }
    
        func applicationDidBecomeActive(_ application: UIApplication) {
            // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
        }
    
        func applicationWillTerminate(_ application: UIApplication) {
            // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
        }
    }
    

    Update: Swift 4.2

    This helped also for me:

    1. First step:
    enter image description here

    2. Second:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
    
        let session = AVAudioSession.sharedInstance()
        do{
            try session.setActive(true)
            try session.setCategory(.playback, mode: .default,  options: .defaultToSpeaker)
        } catch{
            print(error.localizedDescription)
        }
        return true
    }