How to go full screen with VideoPlayer() in SwiftUI? - swift

I'm using the following code in my project and it works great.
But I need to know if its possible to play the video in full screen when the user taps on a button or any other action.
This is my code:
VideoPlayer(player: AVPlayer(url: URL(string: "https://xxx.mp4")!)) {
VStack {
Text("Watermark")
.foregroundColor(.black)
.background(Color.white.opacity(0.7))
Spacer()
}
.frame(width: 400, height: 300)
}
.frame(width: UIScreen.main.bounds.width, height: 300)
So basically, the idea is to make the VideoPlayer to go full screen similar to youtube's video player.
Is this possible using the VideoPlayer() in swiftUI?

They should include an option or have that button by default in VideoPlayer I don't know why apple did this if someone knows add a comment about the same, anyways
There is a simpler method though
struct CustomVideoPlayer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let url: String = "https://xxx.mp4"
let player1 = AVPlayer(url: URL(string: url)!)
controller.player = player1
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}
Try this code and call CustomVideoPlayer() and give it a frame with any height and you will have a fullscreen option on the top left corner just like youtube
EDIT: I tried the accepted solution but I don't think it is going fullscreen

I took #Viraj D's answer and created a Swift package called AZVideoPlayer. Add the package with SPM or feel free to just copy the source code into your project.
The package also adds the following:
Addresses the issue where onDisappear is called when full screen presentation begins by allowing a way to ignore the call in that particular case.
Allows you to have the video continue playing when ending full screen presentation (it pauses by default).
Here's an example:
import SwiftUI
import AVKit
import AZVideoPlayer
struct ContentView: View {
var player: AVPlayer?
#State var willBeginFullScreenPresentation: Bool = false
init(url: URL) {
self.player = AVPlayer(url: url)
}
var body: some View {
AZVideoPlayer(player: player,
willBeginFullScreenPresentationWithAnimationCoordinator: willBeginFullScreen,
willEndFullScreenPresentationWithAnimationCoordinator: willEndFullScreen)
.onDisappear {
// onDisappear is called when full screen presentation begins, but the view is
// not actually disappearing in this case so we don't want to reset the player
guard !willBeginFullScreenPresentation else {
willBeginFullScreenPresentation = false
return
}
player?.pause()
player?.seek(to: .zero)
}
}
func willBeginFullScreen(_ playerViewController: AVPlayerViewController,
_ coordinator: UIViewControllerTransitionCoordinator) {
willBeginFullScreenPresentation = true
}
func willEndFullScreen(_ playerViewController: AVPlayerViewController,
_ coordinator: UIViewControllerTransitionCoordinator) {
// This is a static helper method provided by AZVideoPlayer to keep
// the video playing if it was playing when full screen presentation ended
AZVideoPlayer.continuePlayingIfPlaying(player, coordinator)
}
}

Here's how I managed to do it. I hope this helps someone else in the future.
This is a rough idea but it works as it was intended to.
Basically, I've put the VideoPlayer in a VStack and gave it a full width and height.
I gave the VStack a set height and full width.
I gave the VStack a tapGesture and changed its width and height on double tap and this way, all its content (VideoPlayer) will resize accordingly.
This is the code:
struct vidPlayerView: View {
#State var animate = false
var body: some View {
VStack{
VideoPlayer(player: AVPlayer(url: URL(string: "https://xxxx.mp4")!)) {
VStack {
Image(systemName: "info.circle")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
Spacer()
}
.frame(width: 400, height: 300, alignment: .topLeading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Spacer()
}.onTapGesture(count: 2) {
withAnimation {
self.animate.toggle()
}
}
.frame(maxWidth: self.animate ? .infinity : UIScreen.main.bounds.width, maxHeight: self.animate ? .infinity : 300, alignment: .topLeading)
}
}
The above code can be executed by taping on a button or any other actions.

Related

Using VStack with AsyncImage inside LazyVStack causes images to reload on scrolling

I'm trying to display a long list of images with titles using the new AsyncImage in SwiftUI. I noticed that when I put VStack around AsyncImage and scroll through images it's reloading images every time I scroll up or down. When there's no VStack I see no reloading and images seem to stay cached.
Is there a way to make VStack not to reload images on scrolling so I can add text under each image?
Here's a working example. Try scrolling with and without VStack.
import SwiftUI
struct TestView: View {
let url = URL(string: "https://picsum.photos/200/300")
let columns: [GridItem] = [.init(.fixed(110)),.init(.fixed(110)),.init(.fixed(110))]
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(0..<20) { _ in
// VStack here causes to images to reload on scrolling
VStack {
AsyncImage(url: url) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "photo")
.imageScale(.large)
.frame(width: 110, height: 110)
}
}
}
}
}
}
}
I fully agree with you that this is a weird bug. I would guess that the reason it's happening has something to do with the way LazyVGrid chooses to layout views, and that using a VStack here gives it the impression there is more than one view to show. This is a poor job on Apple's part, but this is how I solved it: just put the VStacks internal to the AsyncImage. I'm not entirely sure what the original error is, but I do know that this fixes it.
struct MyTestView: View {
let url = URL(string: "https://picsum.photos/200/300")
let columns: [GridItem] = [.init(.fixed(110)),.init(.fixed(110)),.init(.fixed(110))]
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(0..<20) { i in
AsyncImage(url: url) { image in
VStack {
image
.resizable()
.aspectRatio(contentMode: .fit)
Text("label \(i)")
}
} placeholder: {
VStack {
Image(systemName: "photo")
.imageScale(.large)
.frame(width: 110, height: 110)
Text("image broken")
}
}
}
}
}
}
}

AVFoundation audio player only updates volume from the slider when player is stopped. I want the volume to update in real time as I drag the slider

What I'm building: an app where users can play different ambient noises at different volumes.
The problem: I want the volume to change automatically as I drag the slider. In its current state, it will only reflect changes to the volume if I stop the player and then play it again.
Example of the problem: I tap on a sound to play it (in this case, the sound of a campfire). It starts playing normally. I drag the volume slider to a different value to change the volume. This SHOULD change the volume of the sound I'm hearing, but there is no change in volume. I can only hear the sound with the new volume level if I tap the sound to turn it off, then tap it again to turn it back on. Only then does it reflect the new volume level.
Where I think the problem is: I believe the problem is inside of the function playCampFire(). This is where I inserted the volume functionality inside of the do{} statement. I think because the code is only executed whenever the playCampFire() function is executed, which only happens when the sound is tapped. I've tried putting the .volume in different parts of the file, instead of just having it in the playCampFire() function, but I haven't had luck with that.
Here is my code:
import SwiftUI
import AVFoundation
var campFirePlayer: AVAudioPlayer!
var campFireIsTapped: Bool = false
struct CF_RowView: View {
#State var SFname: String
#State var nameOfSound: String
#State var urlOfSound: String
#State var slidervalue: Float
var body: some View {
HStack {
Image(systemName: SFname)
.frame(width: 20)
// Button to turn the sound on or off
Button(action: {
campFireIsTapped.toggle()
campFireIsTapped ? self.playCampFire() : self.stopCampFire()
}) {
Text(nameOfSound).frame(width: 150, alignment: .leading)
}
.padding(.leading, 10)
.foregroundColor(Color.primary)
Spacer()
Slider(value: $slidervalue)
.frame(width: 130, alignment: .trailing)
}.frame(width:320)
}
// function to play the sound
func playCampFire() {
let url = Bundle.main.url(forResource: urlOfSound, withExtension: "aif")
guard url != nil else {
return
}
do {
campFirePlayer = try AVAudioPlayer(contentsOf: url!)
campFirePlayer?.play()
campFirePlayer?.numberOfLoops = -1
campFirePlayer.volume = slidervalue
} catch {
print("\(error)")
}
}
// function to turn off the sound
func stopCampFire() {
let url = Bundle.main.url(forResource: urlOfSound, withExtension: "aif")
guard url != nil else {
return
}
do {
campFirePlayer = try AVAudioPlayer(contentsOf: url!)
campFirePlayer?.stop()
} catch {
print("\(error)")
}
}
}
and here is a screenshot of the preview...
Question: Where should I put .volume in my code so that it updates automatically as soon as I drag the volume slider, without me having to stop the sound and play it again?
I'd recommend using onChange to detect when the value is updated. Just update your code to say this:
var body: some View {
HStack {
Image(systemName: SFname)
.frame(width: 20)
// Button to turn the sound on or off
Button(action: {
campFireIsTapped.toggle()
campFireIsTapped ? self.playCampFire() : self.stopCampFire()
}) {
Text(nameOfSound).frame(width: 150, alignment: .leading)
}
.padding(.leading, 10)
.foregroundColor(Color.primary)
Spacer()
Slider(value: $slidervalue)
.frame(width: 130, alignment: .trailing)
}
.frame(width:320)
.onChange(of: self.slidervalue) { value in
self.campFirePlayer.volume = value
}
}

SwiftUI: Full screen View over NavigationBar and TabBar

I'm trying to add a full screen View over my app in SwiftUI. The purpose of this is to have a "shade" that fades in that will darken the screen and bring focus to a custom pop-up, disabling content in the background. See below for a visual example:
The View that I'm trying to add this shade over is embedded in a complex NavigationView stack (several layers deep, accessed via a NavigationLink) and also has a visible TabBar. So far I've tried embedding the NavigationView in a ZStack and adding a Rectangle() on top but to no avail, the NavigationBar and TabBar still sit on top of this view. I've also tried using using the .zIndex() modifier but this seems to have done nothing.
Any help is much appreciated,
Thanks
You do not need work on zIndex, because you cover the all screen! Even you do not need work on disable your current View for using PopUp, because again PopUp is already on top layer. zIndex would be helpful when you did not cover the screen, here is a way:
struct ContentView: View {
#State private var isPresented: Bool = Bool()
var body: some View {
NavigationView {
VStack {
Button("Show Custom PopUp View") { isPresented.toggle() }
}
.navigationTitle("Navigation Title")
}
.customPopupView(isPresented: $isPresented, popupView: { popupView })
}
var popupView: some View {
RoundedRectangle(cornerRadius: 20.0)
.fill(Color.white)
.frame(width: 300.0, height: 200.0)
.overlay(
Image(systemName: "xmark").resizable().frame(width: 10.0, height: 10.0)
.foregroundColor(Color.black)
.padding(5.0)
.background(Color.red)
.clipShape(Circle())
.padding()
.onTapGesture { isPresented.toggle() }
, alignment: .topLeading)
.overlay(Text("Custom PopUp View!"))
.transition(AnyTransition.scale)
.shadow(radius: 10.0)
}
}
struct CustomPopupView<Content, PopupView>: View where Content: View, PopupView: View {
#Binding var isPresented: Bool
#ViewBuilder let content: () -> Content
#ViewBuilder let popupView: () -> PopupView
let backgroundColor: Color
let animation: Animation?
var body: some View {
content()
.animation(nil, value: isPresented)
.overlay(isPresented ? backgroundColor.ignoresSafeArea() : nil)
.overlay(isPresented ? popupView() : nil)
.animation(animation, value: isPresented)
}
}
extension View {
func customPopupView<PopupView>(isPresented: Binding<Bool>, popupView: #escaping () -> PopupView, backgroundColor: Color = .black.opacity(0.7), animation: Animation? = .default) -> some View where PopupView: View {
return CustomPopupView(isPresented: isPresented, content: { self }, popupView: popupView, backgroundColor: backgroundColor, animation: animation)
}
}
This is how I would so it.
struct ContentView: View {
#State var showingShade = false
var body: some View {
ZStack{
// Your other views goes here
if showingShade{
Rectangle()
.ignoresSafeArea()
.foregroundColor(.black)
.opacity(0.5)
}
}
}
}
And then simply set showingShade = true when you want the shade to appear. It might be a good idea to use the same var as your PopUp.
For disabling the view you can use the .disabled() modifier on the specific view you want to disable.

SwiftUI onDrag does not show preview image when running on device

I noticed an issue with the drag and drop features in SwiftUI that are available since iOS 13.4. The drag and drop operation with the .onDrag and .onDrop modifiers works fine in the simulator, but on a real device (iPhone and iPad) you just see a transparent rect, instead of the view while dragging the view.
Does anyone have a solution to get the correct preview image while the view is dragged?
struct MainView: View {
#State var isDropTarget = false
var body: some View {
VStack{
Image(systemName: "doc.text")
.font(.system(size: 40))
.frame(width: 150, height: 150)
.onDrag { return NSItemProvider(object: "TestString" as NSString) }
Color.orange
.opacity(isDropTarget ? 0.5 : 1)
.onDrop(of: ["public.text"], isTargeted: $isDropTarget) { items in
for item in items {
if item.canLoadObject(ofClass: NSString.self) {
item.loadObject(ofClass: String.self) { str, _ in
print(str ?? "nil")
}
}
}
return true
}
}
}
While iOS 15 did not fix this bug in my testing, there is a new API that allows you to specify the preview View to display: onDrag(_:preview:). You can recreate the view being dragged, in this case your Image, for the preview.

SwiftUI - triggered breakpoints twice

I am trying to figure out why a breakpoint triggered just once for the first time I used my app and when I did the same steps one more time the breakpoints triggered twice for the same line of code, nothing changed on runtime I just used navigation forward and back. I demo this issue with a comment on my video below. It's 5 minutes video with explanation, but you will see the issue in 2 minutes:
here is a link to a video I made
https://youtu.be/s3fqJ4YhQEc
If you don't want to watch a video I will add a code at the bottom of the question:
here is a link to a repo I used to record a video: https://github.com/matrosovDev/SwiftUINavigatioPopBackIssue
I am also use Apple Feedback Assistant, so guys help me with this as well. I assumed that there is something with deallocating view that was not deallocated in a right way but they answered with this:
There is no notion of “deallocating” structs in Swift. SwiftUI manages
view lifetimes. If you’re writing code that somehow depends on the
existance or non-existance of a particular View at a particular point
in time, that code is wrong.
What leads you to believe that the SecondView isn’t “deallocated”?
The code that leads you to that conclusion is making incorrect
assumptions about the SwiftUI view lifecycle.
Anyway you may see on the video breakpoints triggered twice and even before I actually showing SecondView.
Code example:
struct FirstView: View {
#EnvironmentObject var simpleService: SimpleService
#State var showSecondView = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
self.downloadImageFromURL()
}) {
Text("Next")
.fontWeight(.bold)
.font(.title)
.padding(EdgeInsets(top: 20, leading: 40, bottom: 20, trailing: 40))
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
}
NavigationLink(destination: SecondView(), isActive: $showSecondView) { EmptyView() }
}
}
}
func downloadImageFromURL() {
simpleService.image = nil
if let url = URL(string: "https://placebear.com/200/300") {
simpleService.downloadImage(from: url) { (image) in
self.showSecondView = true
}
}
}
}
struct CircleImage: View {
var image: Image
var body: some View {
image
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.aspectRatio(contentMode: .fill)
}
}
struct SecondView: View {
#EnvironmentObject var simpleService: SimpleService
var body: some View {
NavigationView {
CircleImage(image: simpleService.image!) // Called twice next time I visit SecondView
.frame(height: 140)
.frame(width: 140)
}.onAppear(perform: fetch)
}
private func fetch() {
print(Date())
}
}