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.
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With