Custom CameraView bugs whole app when integrating it into my app - swiftui

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.

Related

How to make a custom UIView Appear/Dissapear in SwiftUI

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.

SwiftUI: Centering a subview of a VStack in the main view

I want to center a TextField, which is in a VStack, to the center of the main view / screen. I tried everything I could think of. But no matter what I do SwiftUI is centering the whole VStack and not the one TextField to the center.
Maybe one of you know how to save my day. Thanks is advance.
Here is my code:
import SwiftUI
struct ContentView: View {
// MARK: - Private Properties
#State private var input: String = ""
#State private var inputs: [String] = []
// MARK: - Body
var body: some View {
ZStack(alignment: .bottomTrailing) {
content()
.frame(maxHeight: .infinity)
saveButton()
}
.frame(maxHeight: .infinity)
.background(Color.brown)
}
// MARK: - Views
private func content() -> some View {
VStack {
textField()
subtitle()
entries()
}
.padding(10)
.background(Color.yellow)
}
private func textField() -> some View {
TextField("TextField", text: $input)
.font(Font.largeTitle)
.padding(.horizontal, 20)
.background(Color.red)
}
private func subtitle() -> some View {
Text("Text")
.background(Color.white)
}
private func entries() -> some View {
ForEach(inputs, id: \.self) {
Text($0)
}
.background(Color.purple)
}
private func saveButton() -> some View {
Button(action: { inputs.append(input); input = "" },
label: { Text("Save") })
.padding()
}
}
A hack I use sometimes to centre things when nothing else is working :)
VStack {
Spacer()
your_view()
Spacer()
}
See if this works for you
private func textField() -> some View {
VStack {
Spacer()
TextField("TextField", text: $input)
.font(Font.largeTitle)
.padding(.horizontal, 20)
.background(Color.red)
Spacer()
}
}

Show BottomPlayerView above TabView in SwiftUI

I'm learning swiftUI and I want to make a music app.
I created a view which going to be above the tabView, but I want it to be shown only if user start playing a music.
My App, I use ZStack for bottomPlayer, and I share the bottomPlayer variable through .environmentObject(bottomPlayer) so the child views can use it:
class BottomPlayer: ObservableObject {
var show: Bool = false
}
#main
struct MyCurrentApp: App {
var bottomPlayer: BottomPlayer = BottomPlayer()
var audioPlayer = AudioPlayer()
var body: some Scene {
WindowGroup {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabBar()
if bottomPlayer.show {
BottomPlayerView()
.offset(y: -40)
}
}
.environmentObject(bottomPlayer)
}
}
}
The BottomPlayerView (above the TabView)
struct BottomPlayerView: View {
var body: some View {
HStack {
Image("cover")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text("Artist")
.foregroundColor(.orange)
Text("Song title")
.fontWeight(.bold)
}
Spacer()
Button {
print("button")
} label: {
Image(systemName: "play")
}
.frame(width: 60, height: 60)
}
.frame(maxWidth: .infinity, maxHeight: 60)
.background(Color.white)
.onTapGesture {
print("ontap")
}
}
}
My TabView:
struct TabBar: View {
var body: some View {
TabView {
AudiosTabBarView()
VideosTabBarView()
SearchTabBarView()
}
}
}
And In my SongsView, I use the EnvironmentObject to switch on the bottomPlayerView
struct SongsView: View {
#EnvironmentObject var bottomPlayer: BottomPlayer
var body: some View {
NavigationView {
VStack {
Button {
bottomPlayer.show = true
} label: {
Text("Show Player")
}
}
.listStyle(.plain)
.navigationBarTitle("Audios")
}
}
}
The problem is the bottomPlayer.show is actually set to true, but doesn't appear ...
Where I am wrong?
In your BottomPlayer add theย #Published attribute before the show boolean.
This creates a publisher of this type.
apple documentation

SwiftUI - Change direction of view hide animation

I've been perusing SO, and elsewhere, trying to find a way, if possible, to change the direction of how a view is hidden. The examples I've found hide a view by replacing it with an EmptyView, or changing the view's frame dimensions to zero. I am trying to hide a view by 'collapsing' it vertically, but everywhere I've looked the collapse/hide animation happens upwards.
In my case, a view will have a button underneath it that will collapse (hide) or expand (show) the view. The view and button are embedded in a scroll view. What I'd like to have happen is that when the view is wholly or partially scrolled up off the top of the screen, and the collapse button is tapped, the view should 'collapse downward' such that the button remains where it is. Everything I've tried causes the button to move upward off the screen.
I think that what has to happen is that the view origin's y value needs to change. But what would happen to all other views above this view? I've tried to accomplish what I've described by changing the transition but it's not having any affect.
I feel like I'm really missing something basic here so thanks for any help.
struct Collapsible<Content: View>: View {
private var content: () -> Content
#Binding var isCollapsed: Bool
#State var isOffscreen: Bool = false
init(isCollapsed: Binding<Bool>, content: #escaping () -> Content) {
self._isCollapsed = isCollapsed
self.content = content
}
var body: some View {
VStack {
content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: isCollapsed ? 0 : nil)
.clipped()
.animation(.default, value: isCollapsed)
.transition(transition.combined(with: .opacity))
}
.background(
GeometryReader { proxy -> Color in
DispatchQueue.main.async {
let frame = proxy.frame(in: CoordinateSpace.global)
self.isOffscreen = frame.origin.y < 0
var _ = print("isOffscreen: \(self.isOffscreen)")
}
return Color.clear
}
)
}
var transition: AnyTransition {
if isOffscreen {
return AnyTransition.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))
} else {
return AnyTransition.asymmetric(insertion: .move(edge: .top), removal: .move(edge: .bottom))
}
}
}
struct ContentView: View {
#State var isCollapsed = false
var body: some View {
ScrollView {
VStack {
Color.clear.frame(height: 250)
Collapsible(isCollapsed: $isCollapsed) {
HStack {
Text("Content")
}
.frame(maxWidth: .infinity)
.padding([.top, .bottom], 75)
.background(.secondary)
.border(.blue, width: 2)
}
Button(action: {
self.isCollapsed.toggle()
}, label: {
Text(isCollapsed ? "Expand" : "Collapse")
})
.buttonStyle(PlainButtonStyle())
Color.clear.frame(height: 1000)
}
.padding()
}
}
}

SwiftUI `view` should be removed from the super view stack after some time

I would like to implement a toast view in SwiftUI, I am able to do that, but after some time I want to remove the view from the stack. How can we remove current view from the stack?
Here is my code.
// Actual View
struct FormView: View {
var body: some View {
Text("Hello")
.toast() // here is the toast message presentation
}
}
// Toast View
struct ToastMessage<Content: View>: View {
#State var present: Bool = false
let contentView: Content
init( contentView: #escaping () -> Content) {
self.contentView = contentView()
}
var body: some View {
ZStack {
contentView
if present {
HStack {
Text("๐Ÿ‘‹ You're logged in. !!")
.font(.headline)
.foregroundColor(.white)
}
.padding()
.background(Color.black.opacity(0.65))
.cornerRadius(32)
.frame(maxWidth: .infinity, alignment: .center)
.position(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.origin.y + 80)
.transition(.move(edge: .top))
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 2))
}
}
.onAppear {
withAnimation {
present = true
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
present = false
}
}
}
}
}
extension View {
func toast() -> some View {
ToastMessage(contentView: {self})
}
}
Even after 5 seconds, still, this toast view is present in the View's stack. I'm expecting the ToastMessage view should be removed from the FormView after 5 seconds.
Your help would be greatly appreciated and thanks in advance.
Right now, ToastMessage is still in the hierarchy because toast() returns ToastMessage no matter what. Then, within toast message, there's a conditional about whether or not the actual message is displayed.
If you don't want ToastMessage in the hierarchy at all, you'd need to move the #State on level up so that you can use a conditional to determine whether or not it gets displayed.
// Actual View
struct FormView: View {
#State private var showToast = true
var body: some View {
Text("Hello")
.toast(show: $showToast)
.onAppear {
withAnimation { showToast = true }
}
}
}
// Toast View
struct ToastMessage<Content: View>: View {
#Binding var present: Bool
let contentView: Content
init(present: Binding<Bool>, contentView: #escaping () -> Content) {
_present = present
self.contentView = contentView()
}
var body: some View {
ZStack {
contentView
HStack {
Text("๐Ÿ‘‹ You're logged in. !!")
.font(.headline)
.foregroundColor(.white)
}
.padding()
.background(Color.black.opacity(0.65))
.cornerRadius(32)
.frame(maxWidth: .infinity, alignment: .center)
.position(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.origin.y + 80)
.transition(.move(edge: .top))
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 2))
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
withAnimation {
present = false
}
}
}
}
}
extension View {
#ViewBuilder func toast(show: Binding<Bool>) -> some View {
if show.wrappedValue {
ToastMessage(present: show,contentView: {self})
} else {
self
}
}
}
That being said, if you don't want to move the state up, there's not necessarily any harm in keeping ToastMessage in the hierarchy.