I am trying to utilize the AVSpeechSynthesizer in swift/xcode to read out some text. I have it working for the most part. I have it set so that if they go back to the previous screen or to the next screen, the audio will stop. But in my instance, I want the speech to continue if another view is presented modally. As an example, I have an exit button that once clicked presents an "Are you sure you want to exit? y/n" type screen, but I want the audio to continue until they click yes and are taken away. I also have another view that can be presented modally, again, wanting the audio to continue if this is the case.
Does anyone have an idea of how I can keep the speech playing when a view is presented modally over top but stop playing when navigating to another view entirely?
Here is my code so far:
//Press Play/Pause Button
#IBAction func playPauseButtonAction(_ sender: Any) {
if(isPlaying){
//pause
synthesizer.pauseSpeaking(at: AVSpeechBoundary.immediate)
playPauseButton.setTitle("Play", for: .normal)
} else {
if(synthesizer.isPaused){
//resume playing
synthesizer.continueSpeaking()
} else {
//start playing
theUtterance = AVSpeechUtterance(string: audioTextLabel.text!)
theUtterance.voice = AVSpeechSynthesisVoice(language: "en-UK")
synthesizer.speak(theUtterance)
}
playPauseButton.setTitle("Pause", for: .normal)
}
isPlaying = !isPlaying
}
//Press Stop Button
#IBAction func stopButtonAction(_ sender: Any) {
if(isPlaying){
//stop
synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate)
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
}
//Leave Page
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
synthesizer.stopSpeaking(at: .immediate)
}
The problem lies in your viewWillDisappear. Any kind of new screen will trigger this, so your code indeed synthesizer.stopSpeaking(at: .immediate) will be invoked, thus stopping your audio. And that includes presentation or pushing a new controller.
Now, how to improve that? You've mentioned this:
I have it set so that if they go back to the previous screen or to the
next screen, the audio will stop
First off, if they go back to the previous screen:
You'd want to execute the same stopping of audio code line inside your
deinit { } method. That will let you know that your screen or controller is being erased from the memory, meaning the controller is gone in your controller stacks (the user went back to the previous screen). This should work 100% fine as long as you don't have retain cycle count issue.
Next, to the next screen, easily, you could include the same code line of stopping your audio inside your function for pushing a new screen.
After a lot of research I was able to get the desired functionality. As suggested by Glenn, the correct approach was to get the stopSpeaking to be called in deinit instead of viewWillDisappear. The issue was that using AVSpeechSynthesizer/AVSpeechSynthesizerDelegate normally would create a strong reference to the ViewController and, therefore, deinit would not be called. To solve this I had to create a custom class that inherits from AVSpeechSynthesizerDelegate but uses a weak delegate reference to a custom protocol.
The custom class and protocol:
import UIKit
import AVFoundation
protocol AudioTextReaderDelegate: AnyObject {
func speechDidFinish()
}
class AudioTextReader: NSObject, AVSpeechSynthesizerDelegate {
let synthesizer = AVSpeechSynthesizer()
weak var delegate: AudioTextReaderDelegate!
//^IMPORTANT to use weak so that strong reference isn't created.
override init(){
super.init()
self.synthesizer.delegate = self
}
func startSpeaking(_ toRead: String){
let utterance = AVSpeechUtterance(string: toRead)
synthesizer.speak(utterance)
}
func resumeSpeaking(){
synthesizer.continueSpeaking()
}
func pauseSpeaking(){
synthesizer.pauseSpeaking(at: .immediate)
}
func stopSpeaking() {
synthesizer.stopSpeaking(at: .immediate)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.delegate.speechDidFinish()
}
}
And then inheriting and using it in my ViewController:
import UIKit
import AVFoundation
class MyClassViewController: UIViewController, AudioTextReaderDelegate{
var isPlaying = false
let audioReader = AudioTextReader()
let toReadText = "This is the text to speak"
#IBOutlet weak var playPauseButton: UIButton!
#IBOutlet weak var stopButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.audioReader.delegate = self
}
//Press Play/Pause Button
#IBAction func playPauseButtonAction(_ sender: Any) {
if(isPlaying){
//Pause
audioReader.synthesizer.pauseSpeaking(at: .immediate)
playPauseButton.setTitle("Play", for: .normal)
} else {
if(audioReader.synthesizer.isPaused){
//Resume Playing
audioReader.resumeSpeaking()
} else {
audioReader.startSpeaking(toReadText)
}
playPauseButton.setTitle("Pause", for: .normal)
}
isPlaying = !isPlaying
}
//Press Stop Button
#IBAction func stopButtonAction(_ sender: Any) {
if(isPlaying){
//Change Button Text
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
//Stop
audioReader.stopSpeaking()
}
//Finished Reading
func speechDidFinish() {
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
//Leave Page
deinit {
audioReader.stopSpeaking()
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
#IBAction func NextStopButton(_ sender: Any) {
audioReader.stopSpeaking()
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
}
I hope this helps someone in the future, because this was driving me up the wall.
Related
I have an app where users can play videos fullscreen. This is accomplished quite simply with AVPlayerViewController.
It works great, but in circumstances where the user is, say, listening to a podcast, if they tap on a video to watch it'll pause the podcast so that the user can watch the video, but when I close the video I would expect the podcast to resume. This is the behavior in most apps, I've confirmed it with Twitter and Tweetbot, for instance. Weirdly, the below code works for Apple Podcasts (it will resume), but no others, and other apps like the aforementioned Tweetbot work universally.
I've had absolutely no luck getting this to work myself however.
Things I've tried:
Just presenting the AVPlayerViewController with the minimum amount of code required: doesn't resume.
On viewDidDisappear of the AVPlayerViewController calling AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation): doesn't resume and spits out a "player is busy" error.
Okay, let's try stopping the player manually with player?.pause() in viewDidDisappear: doesn't resume, still gives a "player is busy" error.
Hmm, maybe give it a second? Try calling setActive after a DispatchQueue.main.asyncAfter of 1 second? No error, but doesn't resume.
I've put it in a test project with a very minimal amount of code:
import UIKit
import AVKit
import AVFoundation
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(buttoned(sender:)), for: .touchUpInside)
button.setTitle("Tap here to view video", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 19.0)
button.frame = view.bounds
view.addSubview(button)
}
#objc private func buttoned(sender: UIButton) {
let videoURL = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
let player = AVPlayer(url: videoURL!)
let playerViewController = CustomVideoController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
playerViewController.player?.play()
}
}
}
class CustomVideoController: AVPlayerViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print(error)
}
}
}
Here's the complete compile-able project (literally just the above code): https://drive.google.com/file/d/1oWb-bNIQv5B1KjMkez6BxuEIh_n6eFxH/view
Play something from like Apple Music, Spotify, Overcast, etc. then run the app and tap view video.
I'm kinda going crazy trying to figure this relatively trivial thing out, as I'm sure I'm just doing something incredibly stupid, but any help would be appreciated.
Have you tried to use player as member var? So you can destroy it after view is disappeared, haven't tested yet, but the code should be like this:
import UIKit
import AVKit
import AVFoundation
class ViewController: UIViewController {
var player:AVPlayer?
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(buttoned(sender:)), for: .touchUpInside)
button.setTitle("Tap here to view video", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 19.0)
button.frame = view.bounds
view.addSubview(button)
}
#objc private func buttoned(sender: UIButton) {
let videoURL = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
player = AVPlayer(url: videoURL!)
let playerViewController = CustomVideoController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
playerViewController.player?.play()
}
}
}
class CustomVideoController: AVPlayerViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
player.pause()
player = nil
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print(error)
}
}
}
(Sorry, I cannot seem to leave comments)
Have you tried testing on a different device (with different iOS version?). Your sample code worked fine on my iPhone 6 (iOS 12.1.2), I tested both Music and Podcasts.
I created the button with Image on MainStoryBoard. When I press that button it plays audio file.
I would like that button to stay highlighted during the time audio file is playing. When the audio is finished, I want the button to return to its default state.
I discovered that audioPlayerDidFinishPlaying(_:successfully:) is a right way, but can not solve this problem. Is it close to the proper way to do it? If not, how should I?
I'd appreciate any advice that will lead to finding the solution to this problem. Thanks.
let url = Bundle.main.bundleURL.appendingPathComponent("audio1.mp3")
override func viewDidLoad() {
super.viewDidLoad()
}
func play(url: URL) {
do {
try player = AVAudioPlayer(contentsOf:url)
player.play()
} catch {
print(error)
}
}
#IBAction func pushButton1(sender: UIButton) {
play(url: url)
}
On viewDidLoad, you can set default property when it is not playing (eg: I'm keeping background color as white) and while playing, make that button as selected(eg: I'm making it color Green)
Again in audioPlayerDidFinishPlaying when it stops playing audio, make it to default state (green to white again):
#IBOutlet weak var yourButton: UIButton!
let url = Bundle.main.bundleURL.appendingPathComponent("audio1.mp3")
override func viewDidLoad() {
super.viewDidLoad()
self.yourButton.backgroundColor = UIColor.white // default state of button
}
#IBAction func pushButton1(sender: UIButton) {
play(url: url)
}
func play(url: URL) {
do {
try player = AVAudioPlayer(contentsOf:url)
player.play()
self.yourButton.backgroundColor = UIColor.green // playing
}
catch {
print(error)
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool) {
self.yourButton.backgroundColor = UIColor.white // back to default state
}
If you look into the image, you'll see a textview, button and a slider.
I have completed all the steps except for the last one which is, when I tap on the slider, it constantly disappear. I know it disappear because I implemented UITapGestureRecognizer.
I guess my question is, I want the slider to disappear everytime I tap anywhere on the screen but when I am using the slider, I dont want the slider to disappear which is happening now every time I release my tap.
I have tried implementing one more UITapGestureRecognizer in sizeRefont with a function to keep sizeRefont.isHidden false but when I do that, the slider will not disappear whenever I tap on the screen.
I tried putting sizeRefont.isHidden = false in sizeRefont action and it doesnt work either.
class ResizeController: UIViewController {
#IBOutlet weak var sizeRefont: UISlider!
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissRefontSize(_:)))
view.addGestureRecognizer(tap)
}
#IBAction func sizeRefont(_ sender: AnyObject) {
let fontSize = CGFloat(sizeRefont.value)
textView.font = UIFont(name: textView.font!.fontName, size: fontSize * 30.0)
}
#IBAction func showSlider(_ sender: Any) {
sizeRefont.isHidden = false
}
func dismissRefontSize(_ sender: UITapGestureRecognizer) {
if sender.location(in: sizeRefont){
sizeRefont.isHidden = false
} else {
sizeRefont.isHidden = true
}
}
}
There is an error on if sender.location(in: sizeRefont) where it says CGPoint is not convertible to Bool
Image
First thing you need to do is you need to Adjust the dismissRefontSize() method to to the following :
func dismissRefontSize(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: view)
If sizeReFont.frame.contains(location) {
// do nothing
}else {
sizeReFont.isHidden = true
}
}
The other thing you need to adjust is creating the tap recognized in your viewDidLoad() to the following:
let tap = UITapGestureRecognizer(target: self, action : #selector(dismissRefontSize(_:)))
I'm creating a camera app in swift and I have a UIButton. I want to propose two options: when user single taps the button - it takes photo and when user holds his finger on a button - it records the movie until user releases the button.
I have functions for recording and taking photo, now I need to distinguish the user action on a button.
Available actions for this button are:
and I tried to start recording on touch down and stop recording on touch up inside, but then I don't know where should I put the code responsible for taking photos. If I put it also in touch down then when user starts recording movie - will also take a photo, and I want to avoid it.
The gesture recognizers for tap and long tap work well with each other to short this out (The tap defers firing until its sure its not a long press).
class ViewController: UIViewController{
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPress))
longPressGestureRecognizer.minimumPressDuration = 1
button.addGestureRecognizer(longPressGestureRecognizer)
}
#objc private func tap(tapGestureRecognizer: UITapGestureRecognizer) {
print("tap")
}
#objc private func longPress (longPressGestureRecognizer: UILongPressGestureRecognizer) {
if longPressGestureRecognizer.state == .began {
print("long press began")
}
}
}
For just taking a single photo, you can just connect the button to an IBAction with Touch Up Inside.
For taking a video you need to first connect the button to an IBOutlet, and then add a UILongPressGestureRecognizer to it to detect when the user long press the button.
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.longPress(gesture:)))
longPressGesture.minimumPressDuration = 1 // Minimum duration to trigger the action
self.button.addGestureRecognizer(longPressGesture)
}
func longPress(gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
print("Began")
// Video starts recording
} else if gesture.state == .ended {
print("End")
// Video stops recording
}
}
#IBAction func takePhoto(_ sender: UIButton) {
// Take photo
}
You can use UILongPressGestureRecognizer for record video and #IBAction - Touch Up Inside for take photo function.
Step 1: In the storyboard, create an UIButton and drag UILongPressGestureRecognizer from Object libray into this button
Step 2: In the ViewController.swift, we have this code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBOutlet weak var button: UIButton!
#IBAction func takePhoto(_ sender: AnyObject) {
label.text = "Take photo"
}
#IBAction func recordVideo(_ sender: AnyObject) {
label.text = "Record video"
}
}
Step 3: Open Assistant editor and connect these #IBOutlet and #IBAction
That's it!
You can use following code to give button action on long press :
I used gesture to detect the log press of the button, and it work for me.
Objective-C
UILongPressGestureRecognizer *lPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(TakePhotoLongPressAction:)];
[self.btnPhoto addGestureRecognizer:lPress];
[lPress release];
- (void)TakePhotoLongPressAction:(UILongPressGestureRecognizer*)gevent
{
if ( gevent.state == UIGestureRecognizerStateEnded ) {
NSLog(#"Button Long Press");
}
}
SWIFT
// add guesture recognizer
let lPress = UILongPressGestureRecognizer(target: self, action: #selector(takePhotoLongPressAction(_:)))
self.button.addGestureRecognizer(lPress)
func takePhotoLongPressAction(gevent: UILongPressGestureRecognizer) {
if gevent.state == UIGestureRecognizerState.Began {
print("Long Press button")
}
}
Please check and let me know if any thing required.
I want to trigger two action on button click and button long click. I have add a UIbutton in my interface builder. How can i trigger two action using IBAction can somebody tell me how to archive this ?
this is the code i have used for a button click
#IBAction func buttonPressed (sender: UIButton) {
....
}
can i use this method or do i have to use another method for long click ?
If you want to perform any action with single tap you and long press the you can add gestures into button this way:
#IBOutlet weak var btn: UIButton!
override func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, #selector (tap)) //Tap function will call when user tap on button
let longGesture = UILongPressGestureRecognizer(target: self, #selector(long)) //Long function will call when user long press on button.
tapGesture.numberOfTapsRequired = 1
btn.addGestureRecognizer(tapGesture)
btn.addGestureRecognizer(longGesture)
}
#objc func tap() {
print("Tap happend")
}
#objc func long() {
print("Long press")
}
This way you can add multiple method for single button and you just need Outlet for that button for that..
#IBOutlet weak var countButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
addLongPressGesture()
}
#IBAction func countAction(_ sender: UIButton) {
print("Single Tap")
}
#objc func longPress(gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began {
print("Long Press")
}
}
func addLongPressGesture(){
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gesture:)))
longPress.minimumPressDuration = 1.5
self.countButton.addGestureRecognizer(longPress)
}
Why not create a custom UIButton class, create a protocol and let the button send back the info to delegte. Something like this:
//create your button using a factory (it'll be easier of course)
//For example you could have a variable in the custom class to have a unique identifier, or just use the tag property)
func createButtonWithInfo(buttonInfo: [String: Any]) -> CustomUIButton {
let button = UIButton(type: .custom)
button.tapDelegate = self
/*
Add gesture recognizers to the button as well as any other info in the buttonInfo
*/
return button
}
func buttonDelegateReceivedTapGestureRecognizerFrom(button: CustomUIButton){
//Whatever you want to do
}