0
votes

I am currently trying to play audio from my firebase database. I have two view controllers, the first is a tableviewcontroller and the second is a view controller with an AVAudioPlayer. I am trying to figure out how to sucessfully play the audio url that is grabbed from firebase that is then passed to the second view controller to play when the audio is selected in the tableview. Here is my current code which returns "Fatal error: Unexpectedly found nil while unwrapping an Optional value" on my second viewcontroller when the audio is selected from the tableview controller.

TableViewController:

import UIKit
import Firebase

class HeaderViewTableViewController: UITableViewController {


    @IBOutlet weak var trackImage: UIImageView!
    @IBOutlet weak var trackName: UILabel!
    @IBOutlet weak var artistName: UILabel!
    @IBOutlet weak var trackHeaderImage: UIImageView!

    var image: UIImageView?
    var image2: UIImageView?

    var songs = [String]()
    var urls = [URL]()

    var song:SongPost?

    private let tableHeaderViewHeight: CGFloat = 350.0
    private let tableHeaderViewCutAway: CGFloat = 0.1

    var headerView: HeaderView!
    var headerMaskLayer: CAShapeLayer!

    override func viewDidLoad() {
        super.viewDidLoad()



        print(song)

        trackName.text = song?.title

        ImageService.getImage(withURL: song!.coverImage) { image in
         self.trackImage.image = image
            self.trackHeaderImage.image = image
        }
        artistName.text = song?.author.fullname




        tableView.tableFooterView = UIView()

        headerView = tableView.tableHeaderView as! HeaderView
        headerView.imageView = trackImage
        tableView.tableHeaderView = nil
        tableView.addSubview(headerView)

        tableView.contentInset = UIEdgeInsets(top: tableHeaderViewHeight, left: 0, bottom: 0, right: 0)
        tableView.contentOffset = CGPoint(x: 0, y: -tableHeaderViewHeight + 64)

        //cut away header view
        headerMaskLayer = CAShapeLayer()
        headerMaskLayer.fillColor = UIColor.black.cgColor
        headerView.layer.mask = headerMaskLayer

        let effectiveHeight = tableHeaderViewHeight - tableHeaderViewCutAway/2
        tableView.contentInset = UIEdgeInsets(top: effectiveHeight, left: 0, bottom: 0, right: 0)
        tableView.contentOffset = CGPoint(x: 0, y: -effectiveHeight)

        updateHeaderView()
    }

    func updateHeaderView() {
        let effectiveHeight = tableHeaderViewHeight - tableHeaderViewCutAway/2
        var headerRect = CGRect(x: 0, y: -effectiveHeight, width: tableView.bounds.width, height: tableHeaderViewHeight)

        if tableView.contentOffset.y < -effectiveHeight {
            headerRect.origin.y = tableView.contentOffset.y
            headerRect.size.height = -tableView.contentOffset.y + tableHeaderViewCutAway/2
        }

        headerView.frame = headerRect

        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0, y:0))
        path.addLine(to: CGPoint(x: headerRect.width, y: 0))
        path.addLine(to: CGPoint(x: headerRect.width, y: headerRect.height))
        path.addLine(to: CGPoint(x: 0, y: headerRect.height - tableHeaderViewCutAway))

        headerMaskLayer?.path = path.cgPath
    }


      override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
          self.tableView.decelerationRate = UIScrollView.DecelerationRate.fast
      }

      override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
          return UITableView.automaticDimension
      }



    @IBAction func backButton(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }

}

extension HeaderViewTableViewController {


    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return (song?.audioName.count)!
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SongCell", for: indexPath) as! SongNameTableViewCell

        cell.set(song: self.song!)

        cell.songName.text = song?.audioName[indexPath.row]
        return cell
    }

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

        let vc = storyboard?.instantiateViewController(identifier: "ViewController2") as? ViewController2
        vc?.song = song
        vc?.audioName = (song?.audioName[indexPath.row])!

        self.present(vc!, animated: true, completion: nil)
        vc?.url = song?.audioUrl[indexPath.row]

    }

}


extension HeaderViewTableViewController {
    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        updateHeaderView()
    }
}

Second ViewController:

import UIKit
import AVFoundation
import MediaPlayer
import Firebase

class ViewController2: UIViewController, UITabBarControllerDelegate {



    var song2 = [SongPost]()

    var song: SongPost?
    var audioName : String?
    var url:URL?

    @IBOutlet weak var progressBar: UISlider!
    @IBOutlet weak var downChevron: UIButton!
    var timer:Timer?
    var player = AVAudioPlayer()

    @IBOutlet weak var currentTime: UILabel!
    @IBOutlet weak var durationTime: UILabel!

    @IBOutlet weak var coverImage: UIImageView!
    @IBOutlet weak var backgroundImage: UIImageView!
    @IBOutlet weak var songName: UILabel!
    @IBOutlet weak var artistName: UILabel!
    var trackID: Int = 0
    var toggleState = 1

    var ref: DatabaseReference?


    @IBOutlet weak var playButton: UIButton!


    override func viewDidLoad() {
        super.viewDidLoad()

        setUI()


        tabBarController?.tabBar.barTintColor = UIColor.black
        tabBarController?.tabBar.isTranslucent = false

        tabBarController?.tabBar.tintColor = .white
        tabBarController?.tabBar.unselectedItemTintColor = .gray


        do {

            let songURL = url

            print(songURL)
            try player = AVAudioPlayer(contentsOf: songURL!)
            progressBar.maximumValue = Float(player.duration)
            timer = Timer.scheduledTimer(timeInterval: 0.00001, target: self, selector: #selector(changeSliderValueFollowPlayerCurtime), userInfo: nil, repeats: true)
            player.delegate = self

            player.play()


            MPNowPlayingInfoCenter.default().nowPlayingInfo = [
                MPMediaItemPropertyTitle : "The Box",
                MPMediaItemPropertyArtist: "Roddy Ricch",
                MPMediaItemPropertyPlaybackDuration: player.duration,

            ]
            UIApplication.shared.beginReceivingRemoteControlEvents()
            becomeFirstResponder()


        } catch {


        }

        do {
             try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: .defaultToSpeaker)
        } catch {

        }

    }


    func setUI() {

        ImageService.getImage(withURL: song!.coverImage) { image in
                       self.coverImage.image = image
                          self.backgroundImage.image = image
                      }
               songName.text = song?.title
               artistName.text = song?.author.fullname
    }


    override func remoteControlReceived(with event: UIEvent?) {
        if let event = event {
            if event.type == .remoteControl {
                switch event.subtype {
                case .remoteControlPlay:
                    player.play()
                case .remoteControlPause:
                    player.pause()
                case .remoteControlNextTrack:
                    print("next")
                case .remoteControlPreviousTrack:
                    print("previous")
                default:
                    print("error")
                }
            }
        }
    }

    @objc func changeSliderValueFollowPlayerCurtime() {

        let curValue = Float(player.currentTime)
        progressBar.value = curValue

    }


    @IBAction func sliderUsed(_ sender: Any) {
        player.pause()
        let curTime = progressBar.value
        player.currentTime = TimeInterval(curTime)
        player.play()
    }

    @IBAction func playPauseButtonTapped(_ sender: Any) {
        let playBtn = sender as! UIButton
               if toggleState == 1 {
                  player.pause()
                toggleState = 2
                playBtn.setImage(UIImage(named:"play 5.png"),for:UIControl.State.normal)

               } else {
                player.play()
                   toggleState = 1
                playBtn.setImage(UIImage(named:"pause 4.png"),for:UIControl.State.normal)

               }
    }


    @IBAction func nextButton(_ sender: Any) {


    }

    @IBAction func backButton(_ sender: Any) {
        if trackID != 0 || trackID > 0 {

            trackID -= 1
        }

        do {
            let audioPath = Bundle.main.path(forResource: "the box", ofType: "mp3")
            try player = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)
        } catch {


        }


    }

    @IBAction func downChevronTapped(_ sender: Any) {

        dismiss(animated: true, completion: nil)
    }

}

extension ViewController2: AVAudioPlayerDelegate {
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        if flag {
            do {

                    let audioPath = Bundle.main.path(forResource: "the box", ofType: "mp3")
                try self.player = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)

                    progressBar.maximumValue = Float(player.duration)
                    timer = Timer.scheduledTimer(timeInterval: 0.00001, target: self, selector: #selector(changeSliderValueFollowPlayerCurtime), userInfo: nil, repeats: true)
                    player.delegate = self

                    player.play()
                } catch {

                }
        }
    }
}

SongPost Class:

import Foundation

class SongPost {
    var id: String
    var author: UserProfile
    var title: String
    var coverImage: URL
    var audioUrl: [URL]
    var audioName : [String]
    var createdAt:Date



    init(id:String, author:UserProfile, title:String, coverImage:URL, audioUrl:[URL], audioName: [String] ,timestamp:Double) {
        self.id = id
        self.author = author
        self.coverImage = coverImage
        self.audioUrl = audioUrl
        self.audioName = audioName

        self.title = title
        self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
    }

}

UPDATED

Func Configure Code:

 func configure(song: SongPost, audioName: String, url:URL) {

          do {

                    let songURL = url

                    print(songURL)
        //            let audioPath = Bundle.main.path(forResource: "the box", ofType: "mp3")
            try player = AVAudioPlayer(contentsOf: songURL)
                    progressBar.maximumValue = Float(player.duration)
                    timer = Timer.scheduledTimer(timeInterval: 0.00001, target: self, selector: #selector(changeSliderValueFollowPlayerCurtime), userInfo: nil, repeats: true)
                    player.delegate = self

                    player.play()


                    MPNowPlayingInfoCenter.default().nowPlayingInfo = [
                        MPMediaItemPropertyTitle : "The Box",
                        MPMediaItemPropertyArtist: "Roddy Ricch",
                        MPMediaItemPropertyPlaybackDuration: player.duration,

                    ]
                    UIApplication.shared.beginReceivingRemoteControlEvents()
                    becomeFirstResponder()


                } catch {


                }

                do {
                     try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: .defaultToSpeaker)
                } catch {

                }

    }
1

1 Answers

1
votes

Avoid using unwrapping so much in your code if you're not exactly sure optional is not nil.

At the time you call instantiateViewController, viewDidLoad method of ViewController2 may be called.

let vc = storyboard?.instantiateViewController(identifier: "ViewController2") 
                                                                       as? ViewController2

Which means you will get error on the line where you unwrap the songURL which is nil.

try player = AVAudioPlayer(contentsOf: songURL!)

So viewDidLoad is not a good place to setup your view in your case.

Create a configure function in ViewController2, which accept parameters from previous viewController, then you can setup your player when configure is called which means your parameters are not nil.

class ViewController2: UIViewController, UITabBarControllerDelegate {

   func configure(song: SongPost, audioName : String, url:URL) {
       //setup your view
   }

   //...... other methods, viewDidload etc.
}

//

extension HeaderViewTableViewController {

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

        let vc = storyboard?.instantiateViewController(identifier: "ViewController2") 
                                                                  as? ViewController2
        self.present(vc!, animated: true, completion: nil)
        vc?.configure(
             song: song, 
             audioName: (song?.audioName[indexPath.row])!, 
             url: song?.audioUrl[indexPath.row])

    }

}