I have already successfully hidden the playback like this controller.showsPlaybackControls = false, but this does not actually disable the controls. I can still drag around in my View to change the Video playback.
Is the an easy way to disable these controls?
I have this Custom Video Player:
struct CustomVideoPlayer: UIViewControllerRepresentable {
var player: AVPlayer
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
controller.videoGravity = .resizeAspectFill
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.restartPlayback), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
class Coordinator: NSObject {
var parent: CustomVideoPlayer
init(parent: CustomVideoPlayer) {
self.parent = parent
}
#objc func restartPlayback() {
parent.player.seek(to: .zero)
}
}
}
This might not be the right way to do it, but for now I got a working solution. I just added .allowsHitTesting(false) on my VideoPlayer.
Related
I am using QLPreviewController to display images in my SwiftUI app. However, I've noticed that the swipe-to-dismiss that comes built-in when presenting QLPreviewController modally in UIKit doesn't work when presenting it using .fullScreenCover in SwiftUI. Also, when presenting modally in UIKit, it adds a UINavigationController around the preview and this doesn't happen when presented from SwiftUI as well.
To illustrate, here is code to display an image using QLPreviewController in UIKit. Please note that this is not code from my app, I am just including this to show how QLPreviewController works from UIKit. My app is 100% SwuftUI:
import UIKit
import QuickLook
class ViewController: UIViewController {
let button = UIButton(type: .system)
let url = Bundle.main.url(forResource: "foo", withExtension: "jpg")!
override func viewDidLoad() {
super.viewDidLoad()
button.setTitle("Preview", for: .normal)
self.view.addSubview(button)
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
button.frame = CGRect(x: (view.frame.size.width - 150)/2, y: (view.frame.size.height - 44)/2, width: 150, height: 44)
}
#IBAction func buttonTapped(_ button: UIButton) {
let previewController = QLPreviewController()
previewController.dataSource = self
present(previewController, animated: true)
}
}
extension ViewController : QLPreviewControllerDataSource {
#objc func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return url as QLPreviewItem
}
}
And here is code doing the same thing in SwiftUI using .fullScreenCover:
import Foundation
import SwiftUI
import QuickLook
struct ContentViewSimple: View {
#StateObject private var previewURL = URLContainer()
var body: some View {
VStack {
Button("Preview", action: {
previewURL.url = Bundle.main.url(forResource: "foo", withExtension: "jpg")
})
}
.fullScreenCover(item: $previewURL.url, content: { url in
QuickLookPreviewSimple(url: url)
})
}
}
class URLContainer : ObservableObject {
#Published var url : URL?
}
struct QuickLookPreviewSimple: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> QLPreviewController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator : NSObject, QLPreviewControllerDataSource {
let parent: QuickLookPreviewSimple
init(parent: QuickLookPreviewSimple) {
self.parent = parent
}
#objc func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return parent.url as QLPreviewItem
}
}
}
As stated previously, the UIKit code presents the image wrapped in a UINavigationController and when you swipe down on the image, the view controller is dismissed. In the SwiftUI code, there is no UINavigationController and swiping down does nothing.
Some things I have tried:
I can wrap the QLPreviewController in a UINavigationController in my SwiftUI code, and that helps to add the toolbar and toolbar buttons and allows me to add a Done button to dismiss, but the swipe down still doesn't work.
I can use .sheet instead of .fullScreenCover, and you can swipe on the navigation bar or toolbar to dismiss, but still swiping down on the image doesn't do anything.
I can use .quickLookPreview in the SwiftUI code instead of .fullScreenCover and that is an easier way to display my image in QLPreviewController and it does add the UINavigationBar, but swiping down also doesn't work.
So is this just an oversight and yet another thing broken in SwiftUI that Apple needs to fix, or is there a way to work around this limitation to allow swipe to dismiss to work on QLPreviewController in SwiftUI? Thanks.
After working on this for a while, I have come up with an acceptable workaround until this is fixed in SwiftUI (I filed feedback with Apple on it).
Instead of using QLPreviewController in a UIViewControllerRepresentable, I am creating a new UIViewController UIViewControllerRepresentable that wraps a QLPreviewController using UIKit present() and dismisses itself when the QLPreviewController is dismissed.
Here is the updated sample code:
struct QuickLookPreviewRep: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> some UIViewController {
return QuickLookWrapper(url: url)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
class QuickLookWrapper : UIViewController, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
var url: URL?
var qlController : QLPreviewController?
required init?(coder: NSCoder) {
fatalError("no supported")
}
init(url: URL) {
self.url = url
super.init(nibName: nil, bundle: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.qlController == nil {
self.qlController = QLPreviewController()
self.qlController?.dataSource = self
self.qlController?.delegate = self
self.present(self.qlController!, animated: true)
}
}
#objc func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return url! as any QLPreviewItem
}
func previewControllerWillDismiss(_ controller: QLPreviewController) {
dismiss(animated: true)
}
}
I have a navigation view from a list of video urls to a destination Videoview and then to a video player. The player uses just the AVPlayer (I don't need controls) to automatically play the video when I navigate to it.
However the video does not stop playing when I hit the 'back' button and navigate away from the PlayerView.
Any thoughts on how to fix this are appreciated.
I've included code from the Navigation view, the pass thru VideoView and the PlayerView as context. TIA.
Navigation view
NavigationView {
List(messages, id: \.self) { message in
NavigationLink(destination: VideoView(videoURL: URL(string:"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4")!, previewLength: 15)) {
Text(message)
}
}.navigationBarTitle("Answers")
VideoView code
struct VideoView: UIViewRepresentable {
var videoURL:URL
var previewLength:Double?
func makeUIView(context: Context) -> UIView {
print("making VideoView")
return PlayerView(frame: .zero, url: videoURL, previewLength: previewLength ?? 30)
}
func updateUIView(_ uiView: UIView, context: Context) {
}
Player Snippet
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
// Create the video player using the URL passed in.
let player = AVPlayer(url: url)
player.volume = 2 // Will play audio if you don't set to zero
// This is the AVPlayer approach to playing video with no controls
player.play() // Set to play once created.
// Add the player to our Player Layer
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill // Resizes content to fill whole video layer.
playerLayer.backgroundColor = UIColor.black.cgColor
previewTimer = Timer.scheduledTimer(withTimeInterval: previewLength, repeats: false, block: { (timer) in
player.seek(to: CMTime(seconds: 0, preferredTimescale: CMTimeScale(1)))
})
layer.addSublayer(playerLayer)
Here is possible approach - to use presentation mode on update, because in this scenario it is updated due to navigation back.
struct VideoView: UIViewRepresentable {
#Environment(\.presentationMode) var presentation // << here !!
var videoURL:URL
var previewLength:Double?
func makeUIView(context: Context) -> UIView {
print("making VideoView")
return PlayerView(frame: .zero, url: videoURL, previewLength: previewLength ?? 30)
}
func updateUIView(_ playerView: PlayerView, context: Context) {
// update might be called several times, so PlayerView should
// be safe for repeated calls
if presentation.wrappedValue.isPresented {
playerView.play()
} else {
playerView.stop()
}
}
}
and PlayerView should be updated to have access to player
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
private var player: AVPlayer // << make property
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
// Create the video player using the URL passed in.
player = AVPlayer(url: url)
player.volume = 2 // Will play audio if you don't set to zero
// don't run .play() here !!!
// ... other code
}
func play() {
player.rate = 1.0
}
func stop() {
player.rate = 0.0
}
}
Okay So I have one view controller that is an AVPlayer. I want that controller to be able to rotate to landscape mode and portrait freely
import Foundation
import UIKit
import AVFoundation
import AVKit
class EventPromoVideoPlayer: UIViewController {
public var eventKey = ""
override var prefersStatusBarHidden: Bool {
return true
}
//URL of promo video that is about to be played
private var videoURL: URL
// Allows you to play the actual mp4 or video
var player: AVPlayer?
// Allows you to display the video content of a AVPlayer
var playerController : AVPlayerViewController?
init(videoURL: URL) {
self.videoURL = videoURL
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.gray
let downSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction(_:)))
downSwipe.direction = .down
view.addGestureRecognizer(downSwipe)
//Setting the video url of the AVPlayer
player = AVPlayer(url: videoURL)
playerController = AVPlayerViewController()
guard player != nil && playerController != nil else {
return
}
playerController!.showsPlaybackControls = false
// Setting AVPlayer to the player property of AVPlayerViewController
playerController!.player = player!
self.addChildViewController(playerController!)
self.view.addSubview(playerController!.view)
playerController!.view.frame = view.frame
// Added an observer for when the video stops playing so it can be on a continuous loop
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player!.currentItem)
//TODO: Need to fix frame of x and y
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
player?.play()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = true
tabBarController?.tabBar.isHidden = true
}
override var supportedInterfaceOrientations:UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.all
}
// Allows the video to keep playing on a loop
#objc fileprivate func playerItemDidReachEnd(_ notification: Notification) {
if self.player != nil {
self.player!.seek(to: kCMTimeZero)
self.player!.play()
}
}
#objc func cancel() {
dismiss(animated: true, completion: nil)
}
#objc func swipeAction(_ swipe: UIGestureRecognizer){
if let swipeGesture = swipe as? UISwipeGestureRecognizer {
switch swipeGesture.direction {
case UISwipeGestureRecognizerDirection.right:
print("Swiped right")
break
case UISwipeGestureRecognizerDirection.down:
print("Swiped Down")
dismiss(animated: true, completion: nil)
break
case UISwipeGestureRecognizerDirection.left:
print("Swiped left")
break
case UISwipeGestureRecognizerDirection.up:
print("Swiped up")
break
default:
break
}
}
}
}
When I dismiss the screen I still need the previous controller to be in portrait. This shouldnt be a problem seeing as portrait mode is locked in as the only orientation in my settings and I want to keep it that way. However I want this one screen to be able to move freely. Any ideas and hints would be greatly appreciated.
The app delegate method does not work for me.
Neither does overriding shouldAutoRotate or supportedInterfaceOrientations in the function
You can use the below method in app delegate.
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let rootViewController = UIApplication.topViewController() {
if (rootViewController.responds(to: Selector(("canRotate")))) || String(describing: type(of: rootViewController)) == "AVFullScreenViewController" {
// Unlock landscape view orientations for this view controller
return .allButUpsideDown;
}
}
// Only allow portrait (standard behaviour)
return .portrait
}
Add the below method in view controller where you want to display as landscape.
func canRotate() -> Void {}
extension UIApplication {
class func topViewController(base: UIViewController? = (UIApplication.sharedApplication().delegate as! AppDelegate).window?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
Im currently using 'AVKit' and 'AVFoundation' to enable the video playing through my app but I can only done by starting the video by tapping a button. But I am currently facing a problem of trying to making an automatic function like playing the video footage right after we start an app on an iOS device.
override func viewDidLoad()
{
super.viewDidLoad()
self.playingVideo()
self.nowPlaying(self)
}
#IBAction func nowPlaying(_ sender: Any)
{
self.present(self.playerController, animated: true, completion:{
self.playerController.player?.play()
})
after I compiled it, the system printed out:
Warning: Attempt to present on whose view is not in the window hierarchy!
Please try following working code.
import AVFoundation
import UIKit
class SomeViewController: UIViewController {
func openVideo() {
let playerController = SomeMediaPlayer()
playerController.audioURL = URL(string: "Some audio/video url string")
self.present(playerController, animated: true, completion: nil)
}
}
class SomeMediaPlayer: UIViewController {
public var audioURL:URL!
private var player = AVPlayer()
private var playerLayer: AVPlayerLayer!
override func viewDidLoad() {
super.viewDidLoad()
self.playerLayer = AVPlayerLayer(player: self.player)
self.view.layer.insertSublayer(self.playerLayer, at: 0)
let playerItem = AVPlayerItem(url: self.audioURL)
self.player.replaceCurrentItem(with: playerItem)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.player.play()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.playerLayer.frame = self.view.bounds
}
// Force the view into landscape mode if you need to
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
get {
return .landscape
}
}
}
I have a ViewController with an AVPlayer, but the video is autorotate even when I have set portrait as the only supported orientation in info.plist and in delegate supportedInterfaceOrientationsFor function.
I have search for documentation without succeed.
Here is some code:
var videoPlayer = AVPlayerViewController()
var player = AVPlayer.init(url: URL.init(fileURLWithPath: urlString))
videoPlayer?.player = player
videoPlayer?.view.frame = self.view.frame
videoPlayer?.showsPlaybackControls = false
self.present(videoPlayer!, animated: false, completion: {action in
self.player?.play()
})
This can be done by subclassing AVPlayerViewController and return UIInterfaceOrientationMask.portrait from supportedInterfaceOrientations here is the code.
class PlayerVideoViewController: AVPlayerViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask{
return .portrait
}}
Thank you to Rhythmic Fistman for the answer.