I am trying to make my app so that when a user touches a UIImageView A certain sound will play. However, while that sound is playing, I want the UIImageView.isUserInteractionEnabled to be assigned to false. Once the sound is done playing I want the UIImageView.isUserInteractionEnabled to be assigned to true. When ever I run the following code I get an error in the card class, even if I force unwrap. Below is the class that contains the image view I want to disable.
class SecondViewController: UIViewController , UIGestureRecognizerDelegate {
#IBOutlet weak var imgPhoto: UIImageView!
func imageTapped(tapGestureRecognizer: UITapGestureRecognizer)
{
imgPhoto.isUserInteractionEnabled = false
itemList[imageIndex].playSound()
}
}
This is the class where the playSound func is located.
import Foundation; import UIKit; import AVFoundation
class Card: NSObject
{
var player: AVAudioPlayer?
var svc = SecondViewController()
var image: UIImage
var soundUrl: String
init(image: UIImage, soundUrl: String, isActive:Bool = true) {
self.image = image
self.soundUrl = soundUrl
}
func playSound()
{
guard let url = Bundle.main.url(forResource: self.soundUrl, withExtension: "m4a") else { return }
do
{
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url)
player?.delegate = self
guard let player = player else { return }
player.prepareToPlay()
player.play()
audioPlayerDidFinishPlaying(player)
print("play")
} catch let error {
print(error.localizedDescription)
}
}
}
extension Card: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer){
svc.imgPhoto.isUserInteractionEnabled = true
}
}
this is the error I get
The problem is this line:
var svc = SecondViewController()
That svc is not your SecondViewController, the existing one in the interface with an imgPhoto. Instead, you are creating a new, separate, and above all blank view controller with an empty view, so its imgPhoto is nil and you crash when you refer to it.
What you want to do is use the "delegate" pattern to hold a reference to the real SecondViewController so you can talk to it.
Related
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.
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.
This question already has answers here:
What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean?
(16 answers)
Closed 4 years ago.
I am currently working on code that disables a UIImageView while an AVAudioPlayer is playing. The AVAudioPlayer and the ImageView I want to disable are located in two different classes. I tried writing a delegate to have my two classes communicate but I keep on getting the error "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value" when trying to assign to self in my viewController.
The code for the class containing my AVAudioPlayer is below
import Foundation; import UIKit; import AVFoundation
protocol isOnProtocol{
func isOn()
}
class Card: NSObject
{
var delegate: isOnProtocol?
var player: AVAudioPlayer?
var image: UIImage
var soundUrl: String
init(image: UIImage, soundUrl: String, isActive:Bool = true) {
self.image = image
self.soundUrl = soundUrl
}
func playSound()
{
guard let url = Bundle.main.url(forResource: self.soundUrl, withExtension: "m4a") else { return }
do
{
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url)
//player?.delegate = self
guard let player = player else { return }
player.prepareToPlay()
player.play()
audioPlayerDidFinishPlaying(player)
print("play")
} catch let error {
print(error.localizedDescription)
}
}
}
extension Card: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer){
if let del = delegate{
del.isOn()
}else{
print("the delegate is not set")
}
}
}
The code for my ViewController that I am trying to communicate with is below. When the image tapped function is called, the sound plays and UserInteractions should be false until the AVAudioPlayer is finished (isOn function is called).
class SecondViewController: UIViewController , UIGestureRecognizerDelegate, isOnProtocol {
#IBOutlet weak var imgPhoto: UIImageView!
var card: Card!
override func viewDidLoad() {
super.viewDidLoad()
card.delegate = self //error is thrown here
}
func imageTapped(tapGestureRecognizer: UITapGestureRecognizer)
{
imgPhoto.isUserInteractionEnabled = false
itemList[imageIndex].playSound()
}
func isOn() {
imgPhoto.isUserInteractionEnabled = true
}
}
Since card is defined as force-unwrapped optional:
var card: Card!
Line:
card.delegate = self
Is the same as if you force unwrapped it:
card!.delegate = self
Problem is that card is nil at that point. You have to initialize it to some value before you can use it. It has nothing to do with the Card implementation itself.
E.g.:
override func viewDidLoad() {
super.viewDidLoad()
card = Card(image: UIImage(), soundUrl: "url", isActive: true)
card.delegate = self
}
Anyway, I would recommend to either use optional (Card?), or initialize the card from the very beginning and not use optional at all. If you have to use optional, using Card? will force you to always remember that the card can point to nil, and will force you to handle it somehow.
I want to track when playing song is finished. I tried different solutions from the web but they could not solve my problem.
I implemented audioPlayerDidFinishPlaying method but it is not working.
How can I understand if playing song is finished?
I am playing songs with playSound function
playSound func:
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
audioPlayerDidFinishPlaying func:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is not working, not printing "finished"
}
How can I solve my problem? How to track when playing song is finished
EDIT: I am adding whole code.
//
// ViewController.swift
import UIKit
import SwiftVideoBackground
import AudioToolbox
import AVFoundation
class ViewController: UIViewController,AVAudioPlayerDelegate {
var player: AVAudioPlayer?
#IBOutlet weak var backgroundVideo: BackgroundVideo!
#IBOutlet weak var initialLabel: UILabel!
#IBOutlet weak var statementLabel: UILabel!
var mp3: [String] = ["turk_milleti_demokrattir","xyz"]
var fav: [String] = ["0","0"]
var name: [String] = ["Türk milleti demokrattır","xy"]
var toggleState = 1
#IBOutlet weak var playB: UIButton!
var counter = 0
var duration = 0.1
override func viewDidLoad() {
super.viewDidLoad()
player?.delegate = self
playB.setImage(UIImage(named: "playbtn.png"), for: .normal)
statementLabel.text = name[counter]
backgroundVideo.createBackgroundVideo(name: "abc", type: "mp4")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func likeButton(_ sender: Any) {
fav[counter] = "1"
print(fav[0...1])
}
#IBAction func playButton(_ sender: Any) {
let name = mp3[counter]
playSound(name: name)
let playBtn = sender as! UIButton
if toggleState == 1 {
player?.play()
toggleState = 2
playBtn.setImage(UIImage(named: "pausebtn.png"), for: .normal)
} else {
player?.pause()
toggleState = 1
playBtn.setImage(UIImage(named:"playbtn.png"),for: .normal)
}
}
#IBAction func nextButton(_ sender: Any) {
counter = counter + 1
if counter == mp3.count {
counter = 0
}
toggleState = 2
playB.setImage(UIImage(named: "pausebtn.png"), for: .normal)
playSound(name: mp3[counter])
statementLabel.text = name[counter]
}
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is not working, not printing "finished"
}
}
I solved my problem with help of Leo Dabus.
I changed my edited code. I moved player?.delegate = self
to playSound func. Finally, it is working.
playSound & audioPlayerDidFinishPlaying function:
func playSound(name: String ) {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
print("url not found")
return
}
do {
/// this codes for making this app ready to takeover the device audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
/// change fileTypeHint according to the type of your audio file (you can omit this)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
player?.delegate = self
// no need for prepareToPlay because prepareToPlay is happen automatically when calling play()
player!.play()
} catch let error as NSError {
print("error: \(error.localizedDescription)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("finished")//It is working now! printed "finished"!
}
Do not forget to add AVAudioPlayerDelegate to ViewController!
class ViewController: UIViewController,AVAudioPlayerDelegate {
You are not setting the player's delegate correctly.
In viewDidLoad, your player is going to be nil, so this line:
player?.delegate = self
Will do nothing (The question mark is optional chaining, so if player == nil, it does nothing.)
You need to set the delegate after loading the player.
I want to control the video player of the video view from the screen transition destination.
I want to play a video by tapping the cell
Try to
// Select(Tap) to cell
override func tableView(_ table: UITableView,didSelectRowAt indexPath: IndexPath) {
let video = ViewController()
video.playerView.playVideo()
}
But fatal error: unexpectedly found nil while unwrapping an Optional value
Append:
I tried to use YTPlayer from https://github.com/youtube/youtube-ios-player-helper
But I did not understand how to write in your code "YTPlayer".
YTPlayer doesn't have YTPlayer(exist YTPlayerView) and YTPlayerLayer.
My viewcontoller's code here
import UIKit
import youtube_ios_player_helper
import AVFoundation
class ViewController: UIViewController, YTPlayerViewDelegate{
#IBOutlet var playerView: YTPlayerView!
var appDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate
override func viewDidLoad() {
super.viewDidLoad()
self.playerView.delegate = self
self.playerView.sizeToFit()
let vars = ["playsinline": 1, "controls": 2, "showinfo": 0, "origin":"https://www.google.com"] as [String : Any]
if self.appDelegate.selectMV == ""
{
print("Not Load",self.appDelegate.selectMV)
}
else
{
print("Load",self.appDelegate.selectMV)
playerView.load(withVideoId: self.appDelegate.selectMV, playerVars: vars)
}
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback)
}
catch {
print("")
}
}
func playerViewDidBecomeReady(_ playerView: YTPlayerView) {
playerView.playVideo()
}
func play() {
print("Push! play")
//Error playerView nil
playerView.load(withVideoId: self.appDelegate.selectMV)
playerView.playVideo()
}
}
It's quit simple.. You need to code in didselect as you've done. But This kinds of code will never works here. You have to code like this in ViewDidLoad() or ViewWillAppear() First.
self.player = AVPlayer(url: NSURL(fileURLWithPath: filePath) as URL)
self.playerLayer = AVPlayerLayer(player: self.player)
self.playerLayer.frame = self.vview.bounds
Then In didSelect Only code for simply
Player.play()
If you want to change playerItem There simply code in didSelect Like this..
player.replaceCurrentItemWithPlayerItem(AVPlayerItem(URL: NSURL(fileURLWithPath: filePath)))
It's for the single View. Try like this and if you need any help feel free to ask me.
First of all, I just want to let you know I'm a newbie in iOS programming ;)
I would like to know if the following scenario is possible or which way I need to update my code to make it work.
I would like to play an audio file located in the player class file within the ViewController file.
I added the file "test.wav" to the root of project.
My problem
When I play the sound by tapping a button, the program can't find the sound file. I have the following error message:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
Main swift file: ViewController.swift
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player1: Player!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func playAudio(sender: AnyObject) {
player1.playAudioFile()
}
}
Class file: Player.swift
import Foundation
import AVFoundation
class Player {
// Variables
var vc: ViewController!
var audioFile: AVAudioPlayer!
// Initializer
init (){
}
// Methods
func playAudioFile() {
if audioFile.playing {
audioFile.stop()
} else {
audioFile.play()
}
}
// Intialize ViewController
init (vc:ViewController!) {
// Set path for the attack sound
let audioSnd = NSBundle.mainBundle().pathForResource("test", ofType: "wav")
let audioFileURL = NSURL(fileURLWithPath: audioSnd!)
do {
try audioFile = AVAudioPlayer(contentsOfURL: audioFileURL, fileTypeHint: nil)
audioFile.prepareToPlay()
} catch let err as NSError {
print(err.debugDescription)
}
}
}
More information
My program has a class "Player", with subclasses such as "Human" and "Monster".
By default, my class player has some audio files for attacks, dying, etc.
Under some conditions, the "player" can become a human or a monster and get custom attack and dying sounds.
Thanks a lot for your help! ;)
Why are you initializing you player with a viewController ? You dont need that.
Change the Player class to:
import Foundation
import AVFoundation
class Player {
// Variables
var audioFile: AVAudioPlayer!
// Initializer
init() {
// Set path for the attack sound
let audioSnd = NSBundle.mainBundle().pathForResource("test", ofType: "wav")
let audioFileURL = NSURL(fileURLWithPath: audioSnd!)
do {
try audioFile = AVAudioPlayer(contentsOfURL: audioFileURL, fileTypeHint: nil)
audioFile.prepareToPlay()
} catch let err as NSError {
print(err.debugDescription)
}
}
// Methods
func playAudioFile() {
if audioFile.playing {
audioFile.stop()
} else {
audioFile.play()
}
}
}
And your ViewController.swift to:
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player1: Player!
override func viewDidLoad() {
super.viewDidLoad()
// Note, that you need to initialize the player1 variable
player1 = Player()
}
#IBAction func playAudio(sender: AnyObject) {
player1.playAudioFile()
}
}
Swift is functional programming, so it is way better to separate the audio operation and viewController into different file/class. Then they could be reused in any scenarios.
Audio part
import AVFoundation
class AudioPlayer {
// Singleton to keep audio playing anywhere
static let shared = AudioPlayer()
var player: AVAudioPlayer?
private init() {}
func play(url: URL) {
do {
try AVAudioSession.sharedInstance().setMode(.default)
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
player = try AVAudioPlayer(contentsOf: url)
guard let player = player else { return }
player.prepareToPlay()
player.play()
} catch {
print("error occurred")
}
}
}
ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func playAudio(sender: AnyObject) {
let audioSnd = NSBundle.mainBundle().pathForResource("test", ofType: "wav")
// don't use force unwrap
guard let audioFileURL = NSURL(fileURLWithPath: audioSnd) else { return }
// call the shared instance here
AudioPlayer.shared.play(url: url)
}
}