If you set AVPlayerViewController.showsPlaybackControls to false, the controls will not show at all. Even if you tap the screen.
I want the controls to start out hidden, but still be able to summon them by tapping. If I set the mentioned property to true, they start out visible. (Yes they fade after a few seconds.) Is there a way to start hidden, but still be accessible?
UPDATE: I ended up making my own controls for better customization. It's more difficult but worth the time. Please read Apple's sample code for reference. It's about implementing PiP but also about making custom controls: https://developer.apple.com/library/prerelease/ios/samplecode/AVFoundationPiPPlayer/Introduction/Intro.html
UPDATE: When tapped, AVPlayerViewController only fires touchesBegan event, and not touchesEnded event. But it's enough to show the controls.
First you need to hide the control. Put this code right before you present AVPlayerViewController
YourAVPlayerViewController.showsPlaybackControls = false
Then subclass AVPlayerViewController and add this function:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.showsPlaybackControls = true
super.touchesBegan(touches, withEvent: event)
}
OLD SOLLUTION:
I've just solved this. The main idea is to put a UIView on top of the AVPlayerViewController to reveive tap gesture, and hide that UIView when it is no longer needed.
Here's the code:
import AVKit
import UIKit
// Create a custom AVPlayerViewController
#available(iOS 8.0, *)
final class CustomAVPlayerViewController: AVPlayerViewController {
// Create a UIView to put on top of all
lazy var topView = UIView(frame: CGRectMake(0, 0, width, height))
override func viewDidLoad() {
super.viewDidLoad()
// For sure, set it to clearcolor
// (DON'T set alpha = 0 because it will stop receiving user interaction)
topView.backgroundColor = UIColor.clearColor()
// Add it to the view of AVPlayerViewController
self.view.addSubview(topView)
// Bring it to front
self.view.bringSubviewToFront(topView)
// Add a tap gesture recognizer
topView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleTap"))
}
// Handle the tap
func handleTap() {
// Show the control
self.showsPlaybackControls = true
// Hide the topView. You can unhide it when needed later.
self.topView.hidden = true
}
}
And when you need to hide the controls, do this:
var AVViewController = CustomAVPlayerViewController()
...
// Hide controls
AVViewController.showsPlaybackControls = false
// Show topView
AVViewController.topView.hidden = false
I think I've solved this using dynamic gesture recognizer relationships. The solution avoids custom controls (for consistency), uses only public API and does not subclass AVPlayerViewController (which is explicitly disallowed, as noted in other answers).
Here's how:
Make a container view controller that embeds AVPlayerViewController. (This is useful regardless of the controls, because you need to put the playback logic somewhere.)
Set showsPlaybackControls to false initially.
Add a UITapGestureRecognizer to recognize the initial tap.
In the action method for the gesture recognizer, set showsPlaybackControls to true.
So far, it would work, but the controls would disappear immediately on that initial tap. To fix that, set yourself as a delegate for the gesture recognizer, implement gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: and return true for any other single-tap gesture recognizer.
Here's the actual implementation in Swift; check andreyvit/ModalMoviePlayerViewController repo for the latest code:
import UIKit
import AVKit
import AVFoundation
public class ModalMoviePlayerViewController: UIViewController {
private let fileName: String
private let loop: Bool
private var item: AVPlayerItem!
private var player: AVPlayer!
internal private(set) var playerVC: AVPlayerViewController!
private var waitingToAutostart = true
public init(fileName: String, loop: Bool = true) {
self.fileName = fileName
self.loop = loop
super.init(nibName: nil, bundle: nil)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
let url = NSBundle.mainBundle().URLForResource(fileName, withExtension: nil)!
item = AVPlayerItem(URL: url)
player = AVPlayer(playerItem: item)
player.actionAtItemEnd = .None
player.addObserver(self, forKeyPath: "status", options: [], context: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ModalMoviePlayerViewController.didPlayToEndTime), name: AVPlayerItemDidPlayToEndTimeNotification, object: item)
playerVC = AVPlayerViewController()
playerVC.player = player
playerVC.videoGravity = AVLayerVideoGravityResizeAspectFill
playerVC.showsPlaybackControls = false
let playerView = playerVC.view
addChildViewController(playerVC)
view.addSubview(playerView)
playerView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
playerView.frame = view.bounds
playerVC.didMoveToParentViewController(self)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ModalMoviePlayerViewController.handleTap))
tapGesture.delegate = self
view.addGestureRecognizer(tapGesture)
}
deinit {
player.pause()
player.removeObserver(self, forKeyPath: "status")
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func togglePlayPause() {
if isPlaying {
pause()
} else {
play()
}
}
func restart() {
seekToStart()
play()
}
func play() {
if player.status == .ReadyToPlay {
player.play()
} else {
waitingToAutostart = true
}
}
func pause() {
player.pause()
waitingToAutostart = false
}
var isPlaying: Bool {
return (player.rate > 1 - 1e-6) || waitingToAutostart
}
private func performStateTransitions() {
if waitingToAutostart && player.status == .ReadyToPlay {
player.play()
}
}
public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
performStateTransitions()
}
#objc func didPlayToEndTime() {
if isPlaying && loop {
seekToStart()
}
}
private func seekToStart() {
player.seekToTime(CMTimeMake(0, 10))
}
public override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if !playerVC.showsPlaybackControls {
playerVC.showsPlaybackControls = true
}
super.touchesBegan(touches, withEvent: event)
}
}
extension ModalMoviePlayerViewController: UIGestureRecognizerDelegate {
#IBAction func handleTap(sender: UIGestureRecognizer) {
if !playerVC.showsPlaybackControls {
playerVC.showsPlaybackControls = true
}
}
/// Prevents delivery of touch gestures to AVPlayerViewController's gesture recognizer,
/// which would cause controls to hide immediately after being shown.
///
/// `-[AVPlayerViewController _handleSingleTapGesture] goes like this:
///
/// if self._showsPlaybackControlsView() {
/// _hidePlaybackControlsViewIfPossibleUntilFurtherUserInteraction()
/// } else {
/// _showPlaybackControlsViewIfNeededAndHideIfPossibleAfterDelayIfPlaying()
/// }
public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if !playerVC.showsPlaybackControls {
// print("\nshouldBeRequiredToFailByGestureRecognizer? \(otherGestureRecognizer)")
if let tapGesture = otherGestureRecognizer as? UITapGestureRecognizer {
if tapGesture.numberOfTouchesRequired == 1 {
return true
}
}
}
return false
}
}
thegathering's answer is good. I would override touchesCancelled instead so that the controls do not show and then hide again.
override public func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
super.touchesCancelled(touches, withEvent: event)
// toggle the player controls on if they were set to off
if !self.showsPlaybackControls {
self.showsPlaybackControls = true
}
}
A simple way to do it in Swift 3 is to set myController.showsPlaybackControls = false, and to overlay the whole player view with a button or gesture recognizer. I embed it into another view in another controller on a storyboard to make this simple and to not override the player controller. The trick then is to hide the button after being clicked once, because the player controller will thereafter track taps to show/hide the controls.
#IBAction func enableControls(button:UIButton)
{
controller?.showsPlaybackControls = true
button.isHidden = true //The button is only needed once, then the player takes over.
}
Related
Referring to my last question:
Spritekit: passing from UIButtons to buttons as SKSpriteNode
I’m working on a SpriteKit game: the user can tap and swipe on the screen to move sprites inside the scene and I added gesture recognizers in my ViewController for that.
Then I create a HUD to keep 4 buttons programmatically made with which the user could add other sprites to the scene.
I want my buttons fade and scale a little when pressed and then turn back to the original state, but it seems that they conflict with viewController’s gesture recognizers: buttons fade and scale down, but they stay in that state, don’t go back to normal state.
What can I do?
This is the Button class:
import SpriteKit
protocol ButtonDelegate: NSObjectProtocol {
func buttonClicked(sender: Button)
}
class Button: SKSpriteNode {
weak var delegate: ButtonDelegate!
var buttonTexture = SKTexture()
init(name: String) {
buttonTexture = SKTexture(imageNamed: name)
super.init(texture: buttonTexture, color: .clear, size: buttonTexture.size())
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var touchEndedCallback: (() -> Void)?
weak var currentTouch: UITouch?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if isUserInteractionEnabled {
setScale(0.9)
self.alpha = 0.5
if let currentTouch = touches.first {
let touchLocation = currentTouch.location(in: self)
for node in self.nodes(at: touchLocation) {
delegate?.buttonClicked(sender: self)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
setScale(1.0)
self.alpha = 1.0
touchEndedCallback?()
print("tapped!")
}
}
This is the code I used in View Controller:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let skView = view as! SKView
skView.isMultipleTouchEnabled = false
skView.presentScene(scene)
#IBAction func didTap(_ sender: UITapGestureRecognizer) {
game.myCode
}
#IBAction func didPan(_ sender: UIPanGestureRecognizer) {
let currentPoint = sender.translation(in: self.view)
if let originalPoint = panPointReference {
if abs(currentPoint.x - originalPoint.x) > (SquareSize * 0.9) {
if sender.velocity(in: self.view).x > CGFloat(0) {
//game.myCode
panPointReference = currentPoint
} else {
//game.myCode
panPointReference = currentPoint
}
}
} else if sender.state == .began {
panPointReference = currentPoint
}
}
#IBAction func didSwipe(_ sender: UISwipeGestureRecognizer) {
//game.myCode
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive shouldReceiveTouch: UITouch) -> Bool {
if UITouch .isKind(of: Button.self) {
return false
}
return true
}
func buttonClicked(sender: Button) {
//myCode
}
}
SOLVED IT!
Ok, it was simpler than I think.
(I took a break).
- I deleted UIgestures and added them programmatically;
- in my GameScene’s touchesEnded method I created a conditional to check which one of my node has been touched.
- in View Controller I add the gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) method, to avoid conflicts with gesture recognizers attached to different view (my nodes are in two different views):
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.view != otherGestureRecognizer.view {
return false
}
return true
}
I hope this will help…
I don't think they are interfering with the inbuilt gesture recognizers of the View Controller as you are not adding any gestures of your own, you are just overriding touchesBegin and touchesEnded.
Does "tapped" get printed out?
It is possible that touchesEnd() is not getting called, try implementing touchesCancelled() as well and see if that gets called.
In my app I have a small menu I made which is basically a UIView with two button on it. The menu opens when the user taps a button and closes also when the user taps the same button. I'd like the menu to close when the user taps anywhere outside of the menu UIView.
The menu:
You can also apply this easy way
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapBlurButton(_:)))
self.view.addGestureRecognizer(tapGesture)
func tapBlurButton(_ sender: UITapGestureRecognizer) {
if //checkmenuopen
{
closemenuhere
}
}
For that when you show the small menu, add below it a invisible button (UIColor.clear) with the entire screen as a frame. And it's action is to dismiss the menu of yours.
Make sure when you dismiss the small menu to dismiss thus button as well.
Hope this helps!
You can use basically touches began function
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("TAPPED SOMEWHERE ON VIEW")
}
There are several solutions to your case:
1- Implementing touchesBegan(_:with:) method in your ViewController:
Tells this object that one or more new touches occurred in a view or
window.
Simply, as follows:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// do something
}
2- Add a UITapGestureRecognizer to the main view of your ViewController:
override func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doSomething(_:)))
view.addGestureRecognizer(tapGesture)
}
func doSomething(_ sender: UITapGestureRecognizer) {
print("do something")
}
Or -of course- you could implement the selector without the parameter:
override func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doSomething))
view.addGestureRecognizer(tapGesture)
}
func doSomething() {
print("do something")
}
3- You could also follow Mohammad Bashir Sidani's answer.
I would suggest to make sure add the appropriate constraints to your button whether it has been added programmatically or by storyboard.
I'm not sure the code below will work in your case, just a advice.
class ViewController: UIViewController {
var closeMenuGesture: UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
closeMenuGesture = UITapGestureRecognizer(target: self, action: #selector(closeMenu))
closeMenuGesture.delegate = self
// or closeMenuGesture.isEnable = false
}
#IBAction func openMenu() {
view.addGestureRecognizer(closeMenuGesture)
// or closeMenuGesture.isEnabled = true
}
#IBAction func closeMenu() {
view.removeGestureRecognizer(closeMenuGesture)
// or closeMenuGesture.isEnabled = false
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return touch.view === self.view // only valid outside menu UIView
}
}
And I never be in this situation so not sure making enable/disable closeMenuGesture is enough to ensure other controls work normally, or to add/remove closeMenuGesture is more insured.
So I have a simple UIPopoverPresentationController that displays some content.
User can dismiss it by tapping anywhere on the screen (default popover behaviour).
I want the popover to be dismissed if the user does any kind of tap or gesture on the screen. Preferably drag gesture.
Any idea if this is possible? And how?
try using touchesBegan:withEvent method
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
if touch.view == self.view {
self.dismiss()
} else {
return
}
}
}
VC is the view presented in the popover.
in the presentViewController:animated:completion: block
[self presentViewController:vc animated:YES completion:^{
UIView *v1 = vc.view.superview.superview.superview;
for (UIView* vx in v1.subviews) {
Class dimmingViewClass = NSClassFromString(#"UIDimmingView");
if ([vx isKindOfClass:[dimmingViewClass class]])
{
UIPanGestureRecognizer* pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(closePopoverOnSwipe)];
[vx addGestureRecognizer:pan];
}
}
}];
you have a UIDimmingView that holds the tap gesture that will close. just add to it. I am using the Class dimmingViewClass = NSClassFromString(#"UIDimmingView"); to avoid making direct use of undocumented APIs. I have not tried yet to send this hack to apple, but will try next week. I hope it will pass. But I tested this and it did call my selector.
I resolved this problem using custom view:
typealias Handler = (() -> Void)?
final class InteractionView: UIView {
var dismissHandler: Handler = nil
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.dismissHandler?()
}
}
In the viewDidAppear configure this view and add to popover containerView:
fileprivate func configureInteractionView() {
let interactionView = InteractionView(frame: self.view.bounds)
self.popoverPresentationController?.containerView?.addSubview(interactionView)
interactionView.backgroundColor = .clear
interactionView.isUserInteractionEnabled = true
interactionView.dismissHandler = { [weak self] in
self?.hide()
}
}
fileprivate func hide() {
self.dismiss(animated: true, completion: nil)
}
my solution for this problem.
for example if you create a class UIViewController named MyPopoverViewController to present PopViewController.
then in the viewDidLoad() or viewWillAppear(_ animated:) Method add two GestureRecognizer as follows:
protocal MyPopoverControllerDelegate {
func shouldDismissPopover()
}
class MyPopoverViewController : UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// back trace to root view, if it is a UIWindows, add PanGestureRecognizer
// and LongPressGestureRecognizer, to dismiss this PopoverViewController
for c in sequence(first: self.view, next: { $0.superview}) {
if let w = c as? UIWindow {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(dismissPopover(gesture:)))
w.addGestureRecognizer(panGestureRecognizer)
let longTapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(dismissPopover(gesture:)))
w.addGestureRecognizer(longTapGestureRecognizer)
}
}
#objc private func dismissPopover(gesture: UIGestureRecognizer) {
delegate?.shouldDismissPopover()
}
}
then in your main ViewController, which this PopOverViewController presents, implements the Method of the Protocol.
extension YourMainViewController: MyPopoverControllerDelegate {
func shouldDismissPopover() {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
i am having a AVPlayerViewControllerin and custom uicollectionviewcell
how can i disable the call to didSelectItemAtIndexPath when touched on the AVPlayer from AVPLayerViewController?
there are a bunch of other elements on the cell, wich should trigger the didSelectItemAtIndexPath
actually on the player controls it works, but once they fade out, another touch on the avplayer triggers the didselect.
having an uibutton in the same cell, cancels the touches correctly.
One hacky solution is to override your cell's hitTest and cancel the selection when the player view is tapped :
class MyCell : UITableViewCell {
#IBOutlet private weak var videoContainer:UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Settings up the video inside the cell :
let videoURL = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!
let player = AVPlayer(url: videoURL)
playerViewController = AVPlayerViewController()
playerViewController.player = player
player.play()
videoContainer.addSubview(playerViewController.view)
playerViewController.view.pinEdgesToSuperviewEdges()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let convertedPoint = videoContainer.convert(point, from: self)
if let hitView = videoContainer.hitTest(convertedPoint, with: event) {
// The tap is somewhere inside the video view,
// disable user interaction on the cell and continue
self.isUserInteractionEnabled = false
return hitView
}
else {
// Tap is outside the video,
// use behavior that will trigger `didSelectItemAtIndexPath`
self.isUserInteractionEnabled = true
return super.hitTest(point, with: event)
}
}
}
I have the following code to pause game scene:
class GameProcessScene: SKScene {
...
var onPause: Bool = false {
willSet {
self.paused = newValue
self.view?.paused = newValue
}
}
...
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
let location = touch.locationInNode(self)
let possiblePauseNode = self.nodeAtPoint(location)
if (possiblePauseNode.name == "pauseButton") {
if (self.paused) {
onPause = false
} else {
onPause = true
}
return
}
...
}
}
It works to pause by pressing button. But i also want to pause game when it appears from background. I am trying to do this from ViewController class:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseGameScene", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
func pauseGameScene() {
let view = self.view as SKView
let scene = view.scene
switch scene {
case is MainMenuScene:
println("MainMenu")
case let game as GameProcessScene:
game.onPause = true
default:
println("default")
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
But it doesnt work when game appears from background.
Can anybody help? Thanks!
UPD1: Also tried UIApplicationWillResignActiveNotification, UIApplicationDidEnterBackgroundNotification but game.onPause = true still doesnt work. It changes "SKScene.paused" property to true, but the actions are still executing when game enters foreground, even game scene was paused before it goes background.
UPD2: I moved observers and selectors from ViewController class to GameProcessScene: SKScene class - same effect, "paused" property changes, but in fact gameplay is continuing.
I solved my problem by creating a property called isPaused and overriding the paused property to always follow the isPaused property. Note : This disables the SKSCene from automatically pausing when going to background mode. You have to manually set the isPaused property. So only use it in scenes where you actually need the pause mode on resume.
class GameScene: SKScene,SKPhysicsContactDelegate {
// Initializers
var isPaused : Bool = false {
didSet {
self.paused = isPaused
}
}
override var paused : Bool {
get {
return isPaused
}
set {
super.paused = isPaused
}
}
}
In ViewController
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseGameScene", name: UIApplicationWillResignActiveNotification, object: nil)
}
func pauseGameScene() {
println("will become inactive")
let view = self.view as SKView
let scene = view.scene
switch scene {
case let game as GameScene:
game.isPaused = true
default:
println("default")
}
}
Did you try put your code into
override func viewWillAppear(animated: Bool) {
//Put your pauseGameScene(); code here
}
This method called before your view appears every times. The viewDidLoad() run only once after your instance is initialized.