Essentially I'm trying to find a way to implement a Custom Video player in a SwiftUI iPad application that has all controls hidden except the Fullscreen button.
As far as I understand just hiding specific controls is not possible but I'm wondering if I can build a custom button that puts the video players in full screen. This is the code I have:
//My Custom Video Player class
import SwiftUI
import AVKit
struct CustomVideoPlayer : UIViewControllerRepresentable {
var player : AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false //Important to not show any native videoplayer controls
controller.videoGravity = .resizeAspectFill
loopVideo(player: player)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
func loopVideo(player p: AVPlayer) {
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: p.currentItem, queue: nil) { notification in
p.seek(to: .zero)
p.play()
}
}
}
import SwiftUI
import AVKit
struct ContentView: View {
var player1 = AVPlayer(url: URL(string: "urlPlaceholder")!)
var body: some View {
CustomVideoPlayer(player: player1)
.onAppear {
player1.play()
}
}
I managed to make this work by copying the videoplayer into a .fullScreenCover. This is not the solution to controlling the videoplayer programatically but it is a solution to showing the player in full screen without controls:
ZStack{
CustomVideoPlayer(player: player1)
.onAppear {
player1.play()
}
.frame(height: 470)
.cornerRadius(10)
VStack{
HStack{
Spacer()
FullScreen_Button()
}
Spacer()
}
}.fullScreenCover(isPresented: $fullScreen1, content: {
ZStack{
CustomVideoPlayer(player: player1)
.onAppear {
player1.play()
}
VStack{
HStack{
Spacer()
FullScreen_Button()
}
Spacer()
}
}
})
Luckily the videoplayer will continue playing from the point it was at automatically.
Related
I have a CameraView in my app that I'd like to bring up whenever a button is to be presssed. It's a custom view that looks like this
// The CameraView
struct Camera: View {
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraViewfinder(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// Buttons and controls on top of the CameraViewfinder
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
// More view component code goes here but I've excluded it all for brevity (they don't add anything substantial to the question being asked.
} // [End of CameraView]
It contains a CameraViewfinder View which conforms to the UIViewRepresentable Protocol:
struct CameraViewfinder: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
I wish to add a binding property to this camera view that allows me to toggle this view in and out of my screen like any other social media app would allow. Here's an example
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
I understand that the code to achieve this must be written inside the updateUIView() method. Now, although I'm quite familiar with SwiftUI, I'm relatively inexperienced with UIKit, so any help on this and any helpful resources that could help me better code situations similar to this would be greatly appreciated.
Thank you.
EDIT: Made it clear that the first block of code is my CameraView.
EDIT2: Added Example of how I'd like to use the CameraView in my App.
Judging by the way you would like to use it in the app, the issue seems to not be with the CameraViewFinder but rather with the way in which you want to present it.
A proper SwiftUI way to achieve this would be to use a sheet like this:
#State var showCamera: Bool = false
var body: some View {
mainTabView
.sheet(isPresented: $showCamera) {
CameraView()
.interactiveDismissDisabled() // Disables swipe to dismiss
}
}
If you don't want to use the sheet presentation and would like to cover the whole screen instead, then you should use the .fullScreenCover() modifier like this.
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView()
.fullScreenCover(isPresented: $showCamera)
}
}
Either way you would need to somehow pass the state to your CameraView to allow the presented screen to set the state to false and therefore dismiss itself, e.g. with a button press.
I have a custom camera view which uses UIKit to capture pictures and store it in a CameraViewModel in my SwiftUI project. The CameraPreview is what acts as the view Finder for my camera view and uses AVFoundation:
struct CameraPreview: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
and the I use this in my CameraView.swift body as such
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraPreview(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.overlay(
Group {
if model.willCapturePhoto {
Color.black
}
}
)
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// .animation(.easeInOut)
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
I wish to open the camera View when someone presses a button somewhere on my app, like so
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
But when I do this in my app, no matter where I put the camera overlay, it stays open and the close button does nothing to close the camera view either. I'm pretty sure this is my fundamental lack of how views are constructed in UIKit and how the UIViewRepresentable works but I'd like some help regardless on how I'd achieve the desired effect. Also, any resources to understand how this works would also be greatly appreciated. Thank you.
I have new SwiftUI .keyboard toolbar added. And it works great with Swiftui TextFields. But I consider if it is possible and how can it be done to use this toolbar also with UITextFields wrapped in UIViewRepresentable. I don’t know if I am doing something wrong or this isn’t supported.
I had the same problem and couldn't find an answer, so I tried to recreate this keyboard toolbar in SwiftUI. Here is the code:
struct SomeView: View {
#State var text = ""
#State var focusedUITextField = false
var body: some View {
NavigationStack {
ZStack {
VStack {
Button("Remove UITextField focus") {
focusedUITextField = false
}
TextField("SwiftUI", text: $text)
CustomTextField(hint: "UIKit", text: $text, focused: $focusedUITextField)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal)
if focusedUITextField {
VStack(spacing: 0) {
Spacer()
Divider()
keyboardToolbarContent
.frame(maxWidth: .infinity, maxHeight: 44)
.background(Color(UIColor.secondarySystemBackground))
}
}
}
/*
.toolbar {
ToolbarItem(placement: .keyboard) {
keyboardToolbarContent
}
}
*/
}
}
var keyboardToolbarContent: some View {
HStack {
Rectangle()
.foregroundColor(.red)
.frame(width: 50, height: 40)
Text("SwiftUI stuff")
}
}
}
And for the custom UITextField:
struct CustomTextField: UIViewRepresentable {
let hint: String
#Binding var text: String
#Binding var focused: Bool
func makeUIView(context: Context) -> UITextField {
let uiTextField = UITextField()
uiTextField.delegate = context.coordinator
uiTextField.placeholder = hint
return uiTextField
}
func updateUIView(_ uiTextField: UITextField, context: Context) {
uiTextField.text = text
uiTextField.placeholder = hint
if focused {
uiTextField.becomeFirstResponder()
} else {
uiTextField.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
let parent: CustomTextField
init(parent: CustomTextField) {
self.parent = parent
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.focused = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
parent.focused = false
}
}
}
Unfortunately the animation of the custom toolbar and the keyboard don't match perfectly. I would challenge the guy in the comments from this post How to add a keyboard toolbar in SwiftUI that remains even when keyboard not visible but sadly I can't comment.
Also the background color doesn't match the native toolbar exactly, I don't know which color is used there.
The problem occurs when I put VideoPlayer view inside NavigationView's parent view or child view. In this example the child view will show navigation bar:
struct ParentView: View {
var body: some View {
NavigationView {
VStack {
Text("Parent View")
NavigationLink(destination: ChildView().navigationBarHidden(true)) {
Text("Child View")
}
}
.navigationBarHidden(true)
}
}
}
struct ChildView: View {
var body: some View {
VStack {
Text("Child View")
VideoPlayer(player: AVPlayer())
}
}
}
I was having the same issue. Not sure what's causing it but I ended up replacing the VideoPlayer with a custom one. That removed the space on top.
struct CustomPlayer: UIViewControllerRepresentable {
let src: String
func makeUIViewController(context: UIViewControllerRepresentableContext<CustomPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let player = AVPlayer(url: URL(string: src)!)
controller.player = player
player.play()
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<CustomPlayer>) { }
}
And in your view where you want to use it, do:
CustomPlayer(src: "<the source to the video>")
if you want a custom player with the same initializer as the native one and that updates along with the current state of the application:
struct CustomPlayer: UIViewControllerRepresentable {
let player: AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<CustomPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<CustomPlayer>) {
uiViewController.player = player
}
}
note that on updateUIViewController(_: context:) you should update the view with every variable you declared and expect to update
in my last question I've asked about a method to achieve the following TabView overlay in SwiftUI:
Thanks again #davidev for the quick support.
I modified the solution with an opportunity to apply conditional view modifier. In this case .overlay().
extension View {
#ViewBuilder
func `if`<Content: View>(_ condition: Bool, content: (Self) -> Content) -> some View {
if condition {
content(self)
}
else {
self
}
}
}
The code snipped above empowered me to implement the conditional toolbar to appear when an observedObject is toggled (e.g. by a switch from read to edit mode):
.if(class.observedObject){ view in
view.overlay(ToolbarFromDavidev())
}
This also works but it's really buggy (e.g. the whole view navigation gets reset and I've limited styling opportunities).
This leads me to my question: Does someone of you have a reference implementation which I can use for my orientation? I would like to solve this with a ZStack that I can position over my TabView that I use for navigation. Like it's shown in the GIF above. However, I'm not able to position the ZStack over the TabView. Already tried stuff like ignoring safe areas etc.
Many Thanks!
You could overlay the TabView like so:
TabView {
...
}
.overlay(
VStack {
Spacer()
//Your View
.frame(height: /*Height of the TabBar*/)
}
.edgesIgnoringSafeArea(.all)
)
Now it comes down to finding out the height of the TabBar. Since its dynamic (different iPhone screen sizes) One way would be like so:
#State private var tabBarHeight: CGFloat = .zero
...
TabView {
//NavigationView
.background(
TabBarAccessor { tabBar in tabBarHeight = tabBar.bounds.height }
)
}
.overlay(
VStack {
Spacer()
//Your View
.frame(height: tabBarHeight)
}
.edgesIgnoringSafeArea(.all)
)
An example solution
Everything together - your code from your first answer modified.
Code:
struct ContentView: View {
#State private var tabBarHeight: CGFloat = .zero
#State var isSelecting : Bool = false
var body: some View {
TabView {
NavigationView {
Text("First Nav Page")
}
.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
.background(
TabBarAccessor { tabBar in tabBarHeight = tabBar.bounds.height }
)
NavigationView {
Text("Second Nav Page")
}
.tabItem {
Image(systemName: "gear")
Text("Settings")
}.tag(1)
Text("No Nav Page")
.tabItem{
Image(systemName: "plus")
Text("Test")
}.tag(2)
}
.overlay(
VStack {
Spacer()
Rectangle()
.foregroundColor(.green)
.frame(height: tabBarHeight)
}
.edgesIgnoringSafeArea(.all)
)
}
}
struct TabBarAccessor: UIViewControllerRepresentable {
var callback: (UITabBar) -> Void
private let proxyController = ViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<TabBarAccessor>) ->
UIViewController {
proxyController.callback = callback
return proxyController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TabBarAccessor>) {
}
typealias UIViewControllerType = UIViewController
private class ViewController: UIViewController {
var callback: (UITabBar) -> Void = { _ in }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let tabBar = self.tabBarController {
self.callback(tabBar.tabBar)
}
}
}
}
Note
The TabBarAccessor code I have found from: Here