How to make a button in Xcode that globally stops AVAudioPlayer? - swift

I'm trying to make a tableviewcell stop all sounds in the app when it is clicked. I know how to stop sounds with soundEffect.stop() soundEffect.currentTime = 0, but I don't know how to apply this to a cell and this doesn't work if the StopSound button is on another viewController with a different class. How do I make a function that globally stops AVAudioPlayer and apply that to a cell?

The problem is that the player is not alone. Try this.
To stop AVAudioPlayer on another ViewController you may use NotificationCenter.
Example...
First time, add function at VC, where you need to stop AVAudioPlayer.
#objc func stopAVonThisVC() {
AVaudioPlayer.stop()
}
Second, add NotificationCenter observer at this VC, and after just post on tap button.
// At VC, where you need to stop AVAudioPlayer.
NotificationCenter.default.addObserver(self, selector:
#selector(stopAVonThisVC), name: NSNotification.Name("Stop at VC1"), object: nil)
// At VC2
NotificationCenter.default.post(name: NSNotification.Name("Stop at VC1"), object: nil)
Sorry for poor english.

Create a BaseViewController for your project and subclass all your view controllers from there,
Define the AVAudioPlayer in BaseViewController and invoke soundEffect.stop() soundEffect.currentTime = 0 from any of your subclass, it should resolve the issue.
Example:
class BaseViewController: UIViewController {
var audioPayer: AVAudioPlayer!
}
class ViewControllerA: BaseViewController {
//start or stop audio player
}
class ViewControllerB: BaseViewController {
//start or stop audio player
}

Related

Swift - Calling a method in a UIViewController from a separate UIView class

Okey, I have been struggling this for a long time now.. Cant get it to work, and I know there is an easy solution that I cannot find.
I have a UIViewController displaying the main app. When it launches there is two containerViews overlaid that displays a count down for the app to start. Countdown is done, and the "overlay" viewcontrollers are removed from superview.
BUT, I need to call the function to remove VC from superview and start the app in the main UIViewController. But I am not able to trigger this function from the separate "overlay" UIView..
If I make an instance of the UIViewController to trigger the startGame() function it works, but then all the labels return nil, and the app crashes.. I believe this is because the UIViewController is triggered twice?
A lot of explaining here.. I will try to show some outtakes in code:
Main UIViewController:
class DMStartVC: UIViewController {
.....
func startGame() {
self.view.viewWithTag(100)?.removeFromSuperview()
self.view.viewWithTag(101)?.removeFromSuperview()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(DMStartVC.update), userInfo: nil, repeats: true)
UITimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(DMStartVC.UIUpdate), userInfo: nil, repeats: true)
}
....
}
The UIView handling the "overlay" container views:
class countdown: UIView {
let dmStartVC = DMStartVC()
...
//when animation is done:
dmStartVC.startGame()
}
This causes crash because all labels in the UIViewController is nil.
How can I reach that .startGame() function?
Only way I have been able to solve this is adding a timer inside the DMStartVC to trigger .startGame()... But that is not a good solution as the timer has to trigger at the exact time the countdown ends.. and it never does.

How do I pause and play background video when navigating from foreground to background?

I have a background video playing when I start my app. I want the video to pause and resume where it left off if I happen to hit the home button. Here is my code:
class ViewController: UIViewController
{
let moviePlayerController = AVPlayerViewController()
var aPlayer = AVPlayer()
func playBackgroundMovie()
{
if let url = NSBundle.mainBundle().URLForResource("IMG_0489", withExtension: "m4v")
{
aPlayer = AVPlayer(URL: url)
}
moviePlayerController.player = aPlayer
moviePlayerController.view.frame = view.frame
moviePlayerController.view.sizeToFit()
moviePlayerController.videoGravity = AVLayerVideoGravityResizeAspect
moviePlayerController.showsPlaybackControls = false
aPlayer.play()
view.insertSubview(moviePlayerController.view, atIndex: 0)
}
func didPlayToEndTime()
{
aPlayer.seekToTime(CMTimeMakeWithSeconds(0, 1))
aPlayer.play()
}
override func viewDidLoad()
{
// Do any additional setup after loading the view, typically from a nib.
super.viewDidLoad()
playBackgroundMovie()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.didPlayToEndTime), name: AVPlayerItemDidPlayToEndTimeNotification, object: nil)
}
Do I have to perform some actions in the app delegate? I looked at previous videos, but I think some of them are using outdated versions of swift. Or are just a little too confusing for me.
You should be able to register for a backgrounding/foregrounding notification via NSNotificationCenter to play or pause your aPlayer like so:
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: #selector(pauseVideoForBackgrounding), name: UIApplicationDidEnterBackgroundNotification, object: nil)
There are several UIApplication notification names available including UIApplicationWillEnterForegroundNotification. You can leverage as many notifications as necessary to let your video player class know what's happening with the app state.
If you're supporting iOS 8 or lower, make sure to also remove your player class as an observer from any NSNotifications you've added to your view controller.
Yes, all of the application specific actions happen within your appdelegate protocol. Look to put your pause code in your appdelegate's applicationDidEnterBackground function, and your resume code in applicationWillEnterForeground.
I would check out Apple's docs on the AppDelegate protocol, particularly these two functions, as there's a little more to take into consideration in regards to the amount of time you're given to finish up tasks before your app becomes inactive (though if you're just pausing and resuming an AVPlayer you should be fine).

Accessing UINavigationController from rootVC Subview (subview loaded from Nib)

The main ViewController is embedded in a UINavigationController subclass, and the VC has a subview that is loaded from a nib. The subview is called MenuView, and contains UIButtons that will link to other VCs.
To keep my main ViewController less unruly, I have put all these buttons into a subview that loads from a nib that animates the menu opening and closing.
However, I would like to present other view controllers from these, sometimes "Modally", sometimes "Show". What I have done seems to work, but I just want to know if this is alright, or if I have caused some unwanted effects that I'm unaware of (like a strong reference cycle that would cause a memory leak, or something). Or is there a better way to do this?
Some code:
In MenuView.swift
class MenuView: UIView {
var navigationController = CustomNavigationController()
func combinedInit(){
NSBundle.mainBundle().loadNibNamed("MenuViewXib", owner: self, options: nil)
addSubview(mainView)
mainView.frame = self.bounds
}
#IBAction func optionsAction(sender: AnyObject) {
self.navigationController.performSegueWithIdentifier("presentOptions", sender: self)
}
In ViewController.swift
menuView.navigationController = self.navigationController as! CustomNavigationController
Short answer: No, it is not alright to access a view controller from within some view in the hierarchy, because that would break all the MVC rules written.
UIView objects are meant to display UI components in the screen and are responsible for drawing and laying out their child views correctly. That's all there is. Nothing more, nothing less.
You should handle those kind of interactions between views and controllers always in the controller in which the view in question actually belong. If you need to send messages from a view to its view controller, you can make use of either the delegate approach or NSNotificationCenter class.
If I were in your shoes, I would use a delegate when view needs some information from its view controller. It is more understandable than using notification center as it makes it much easier to keep track of what's going on between. If the view controller needs some information from a view (in other words, the other way around), I'd go with the notification center.
protocol MenuViewDelegate: class {
func menuViewDidClick(menuView: MenuView)
}
class MenuView: UIView {
var weak delegate: MenuViewDelegate?
#IBAction func optionsAction(sender: AnyObject) {
delegate?.menuViewDidClick(self)
}
}
Let's look at what's going on at the view controller side:
class MenuViewController: UIViewController, MenuViewDelegate {
override func viewDidLoad() {
...
self.menuView.delegate = self
}
func menuViewDidClick(menuView: MenuView) {
navigationController?.performSegueWithIdentifier("presentOptions", sender: self)
}
}
For more information about communication patterns in iOS, you might want to take a look at this great article in order to comprehend how they work.

Swift iOS: Perform a Segue from an Instance in a ViewController to another ViewController

First of all, I have to say that there is so much information on this platform, because of everyone, who is helping. Thank you very much.
At the moment, I have reached a point, where I am not able to go further on my own.
This is my problem:
I have a MainScreen ViewController which is embeded in a Navigation Controller.
From this MainScreen, I can show with a Segue a GameViewController which creates a Gamescene.
In this Gamescene, I have a button, which when it is tapped, shows a Popup and pauses the Gamescene.
This Popup contains two buttons:
A button to resume the game and make the popup disappear.
A button which should end the Gamescene and return to the Mainscreen.
And here is my problem.
I am not able to create the function for the End Button. I have tried different constellations with segues, but it didn't end well.
I think that the difficulty here is, that this button is in an separate class. The hierarchy here, as I understand it, would be:
GameViewController -> GameScene -> Popup -> EndButton.
Do you have any ideas how I could solve this problem? Would there be an other way to solve this transition without using segues?
Thank you very much for your help in advance!
Kind regards,
Phil
Update:
In my Gamescene the popup basically looks like this:
class GameScene: SKScene, SKPhysicsContactDelegate {
// ....
var pauseButton : UIButton! = UIButton()
var pausePopup : PausePopup! // my Popup class with the End button
// ....
override func didMoveToView(view: SKView) {
//configuring the button
pause_button.addTarget(self, action: "pauseButtonPressed", forControlEvents: UIControlEvents.TouchUpInside)
self.view?.addSubview(pause_button)
}
func pauseButtonPressed() {
self.pausePopup = PausePopup(nibName: "uxPausePopup", bundle: nil)
self.scene!.view!.paused = true
self.pausePopup.showInView(self.viewPlaceHolder, animated: true, scene: self)
}
// this is where my popup appears.
In my Popup class I have an action for the EndButton which is located on the Popup and from which I would like to return to the main screen:
#IBAction func showMainScreen(sender: AnyObject) {
// this does not work ...
self.performSegueWithIdentifier("showMainScreen", sender: nil)
}
You cannot call other view from showInView, you need to dismiss the popOver and call the mainView from GameScene, to do so you can create a competition handler in your PausePopup or a protocol to pass the result of your popOver to GameScene deal with the result

Swift: Long press cancelled by movie playing. How do I persist the long press?

I'm building some functionality similar to SnapChat. Press and hold on a view, it brings up a movie to play, then returns to the main view controller when the movie finishes (currently working), or when the user picks up their finger (not working. That's what this question is about).
My problem lies in the IBAction, when the video comes up, the UIGestureRecognizerState changes from Began to Cancelled.
I'd like for the state to not change until the user lifts their finger, which should register UIGestureRecognizerState as Ended
Any tips on how to do that would be awesome. Thanks!
class ViewController: UIViewController {
var moviePlayer: MPMoviePlayerViewController!
#IBAction func handleLongPress(recognizer: UILongPressGestureRecognizer) {
println("\(recognizer)")
if recognizer.state == UIGestureRecognizerState.Began {
playVideo()
}
if recognizer.state == UIGestureRecognizerState.Ended {
self.moviePlayer.moviePlayer.stop()
}
}
func videoHasFinishedPlaying(notification: NSNotification){
println("Video finished playing")
}
func playVideo() {
// get path and url of movie
let path = NSBundle.mainBundle().pathForResource("IMG_8602", ofType:"MOV")
let url = NSURL.fileURLWithPath(path!)
moviePlayer = MPMoviePlayerViewController(contentURL: url)
presentMoviePlayerViewControllerAnimated(moviePlayer)
// construct the views
moviePlayer.view.frame = self.view.bounds
self.view.addSubview(moviePlayer.view)
// remove controls at top and bottom of video
moviePlayer.moviePlayer.controlStyle = MPMovieControlStyle.None
NSNotificationCenter.defaultCenter().addObserver(self, selector: "videoHasFinishedPlaying:",
name: MPMoviePlayerPlaybackDidFinishNotification, object: nil)
}
}
Edit: One possible solution
So this was a total fluke, but I figured out how to do this in this context.
I simply removed the line presentMoviePlayerViewControllerAnimated(moviePlayer) from my playVideo function.
Not entirely sure why that worked but it worked for my context.
This is ONE possibility but I'm open to better suggestions.
Add a 'master view' on top of where you need the touch event
Add the gesture recognizer to that view
Handle the logic within the gesture as you are now, but the subview that needs to be added needs to be added below the view thats receiving the touch.
You can now get the .ended state when the user lifts their finger.
Word of caution. If your view has other touch events make sure this 'master view' is not over top of them because it will intercept those events and hog it for its gesture (unless you set it up to recognize them simultaneously). If thats the case, you can make this 'master view' only a small port of the screen where you need to listen for the touch event without interruption