How to make a custom UIView Appear/Dissapear in SwiftUI - 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.

Related

Custom CameraView bugs whole app when integrating it into my app

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.

SwiftUI ForEach animation overrides "local" animation

I have a view with an infinite animation. These views are added to a VStack, as follows:
struct PanningImage: View {
let systemName: String
#State private var zoomPadding: CGFloat = 0
var body: some View {
VStack {
Spacer()
Image(systemName: self.systemName)
.resizable()
.aspectRatio(contentMode: .fill)
.padding(.leading, -100 * self.zoomPadding)
.frame(maxWidth: .infinity, maxHeight: 200)
.clipped()
.padding()
.border(Color.gray)
.onAppear {
let animation = Animation.linear.speed(0.5).repeatForever()
withAnimation(animation) {
self.zoomPadding = abs(sin(zoomPadding + 10))
}
}
Spacer()
}
.padding()
}
}
struct ContentView: View {
#State private var imageNames: [String] = []
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(self.imageNames, id: \.self) { imageName in
PanningImage(systemName: imageName)
}
// Please uncomment to see the problem
// .animation(.default)
// .transition(.move(edge: .top))
}
}
.toolbar(content: {
Button("Add") {
self.imageNames.append("photo")
}
})
}
}
}
Observe how adding a row to the VStack can be animated, by uncommenting the lines in ContentView.
The problem is that if an insertion into the list is animated, the "local" infinite animation no longer works correctly. My guess is that the ForEach animation is applied to each child view, and somehow these animations influence each other. How can I make both animations work?
The issue is using the deprecated form of .animation(). Be careful ignoring deprecation warnings. While often they are deprecated in favor of a new API that works better, etc. This is a case where the old version was and is, broken. And what you are seeing is as a result of this. The fix is simple, either use withAnimation() or .animation(_:value:) instead, just as the warning states. An example of this is:
struct ContentView: View {
#State private var imageNames: [String] = []
#State var isAnimating = false // You need another #State var
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(self.imageNames, id: \.self) { imageName in
PanningImage(systemName: imageName)
}
// Please uncomment to see the problem
.animation(.default, value: isAnimating) // Use isAnimating
.transition(.move(edge: .top))
}
}
.toolbar(content: {
Button("Add") {
imageNames.append("photo")
isAnimating = true // change isAnimating here
}
})
}
}
}
The old form of .animation() had some very strange side effects. This was one.

SwiftUI AppStorage, UserDefaults and ObservableObject

I have two different views, ContentView and CreateView.
In CreateView, I get user's inputs by textfield, and once user clicks on Save button, the inputs will be stored in AppStorage.
Then, I want to display the saved inputs on ContentView.
Here, I tried to use State & Binding but it didn't work out well.
How would I use the variable, that is created in CreateView, in ContentView?
what property should I use..
Thanks
Here's the updated questions with the code...
struct ContentView: View {
// MARK: - PROPERTY
#ObservedObject var appData: AppData
let createpage = CreatePage(appData: AppData())
var body: some View {
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: AppData()))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
self.appData.shortcutTitle = self.appData.titleInput
self.appData.shortcutOption = self.appData.optionInput
UserDefaults.standard.set(appData.shortcutTitle, forKey: "title")
UserDefaults.standard.set(appData.shortcutOption, forKey: "option")
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
class AppData: ObservableObject {
#Published var shortcutTitle : String = "Deafult Shortcut"
#Published var shortcutOption : String = "Default Option"
#Published var titleInput : String = ""
#Published var optionInput : String = ""
}
So the problem here is that
when I put new inputs on CreatePage and tab the save button, the new inputs do not appear on ContentView page.The output keeps showing the default values of title and option, not user inputs.
If user makes a new input and hit the save button, I want to store them in AppStorage, and want the data to be kept on ContentView (didn't make the UI yet). Am I using the AppStorage and UserDefaults in a right direction?
If anyone have insights on these issues.. would love to take your advice or references.
You're creating instances of AppData in multiple places. In order to share data, you have to share one instance of AppData.
I'm presuming that you create AppData in a parent view of ContentView since you have #ObservedObject var appData: AppData defined at the top of the view (without = AppData()). This is probably in your WindowGroup where you also must have a NavigationView.
I removed the next (let createpage = CreatePage(appData: AppData())) because it does nothing. And in the NavigationLink, I passed the same instance of AppData.
struct ContentView: View {
// MARK: - PROPERTY
#StateObject var appData: AppData = AppData() //Don't need to have `= AppData()` if you already create it in a parent view
var body: some View {
// I'm assuming there's a NavigationView in a parent view
VStack { //note that I've wrapped the whole view in a VStack to avoid having two root nodes (which can perform differently in NavigationView depending on the platform)
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: appData))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
appData.shortcutTitle = appData.titleInput
appData.shortcutOption = appData.optionInput
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
}
Regarding #AppStorage and UserDefaults, it's a little hard to tell what your intent is at this point with those. But, you shouldn't need to declare AppStorage and call UserDefaults on the same key -- #AppStorage writes to UserDefaults for you. Read more at https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper
you can use a singleton ObservableObject that conforms to NSObject so you can observe everything even older apple objects like progress.
class appData : NSObject , ObservableObject {
static let shared = appData()
#Published var localItems = Array<AVPlayerItem>()
#Published var fractionCompleted : Double = 0
#Published var downloaded : Bool = false
#Published var langdentifier = UserDefaults.standard.value(forKey: "lang") as? String ?? "en" {
didSet {
print("AppState isLoggedIn: \(langIdentifier)")
}
}
var progress : Progress?
override init() {
}
}
then u can use it anywhere in your code like this:
appData.shared.langIdentifier == "en" ? .leading : .trailing
You should be able to simply put AppStorage objects in the ObservableClass and call them from there. There should be no need to put AppStorage in the View and then read from UserDefaults in the class.
class AppData: ObservableObject {
#AppStorage(keyForValue) var valueForKey: ValueType = defaultValue
...
}
Of course, you could make #Published property in the class and define the getter and setter for it so it reads and writes directly to the UserDefaults but at that point, you're just creating more work than simply using AppStorage from the beginning directly in the class.

SwiftUI How To Hide The Navigation Bar While Keeping The Back Button

So I'm trying to hide the navigationBar in a Details view in SwiftUI. I've technically gotten it to work by using an init() in a different view, but the issue is that it's making the navigationBar transparent for the whole app, which I only want it in one view. The reason I haven't used an init() in the DetailsView is because I have a variable that needs an input, so I wasn't sure how to do that! Here is the code for the initializer:
init() {
let navBarAppearance = UINavigationBar.appearance()
navBarAppearance.backgroundColor = .clear
navBarAppearance.barTintColor = .clear
navBarAppearance.tintColor = .black
navBarAppearance.setBackgroundImage(UIImage(), for: .default)
navBarAppearance.shadowImage = UIImage()
}
Here's What The Content View and Details View code is like with the init() inside the detailsView:
// ContentView //
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<5) { i in
NavigationLink(destination: DetailsView(test: 1)) {
Text("DetailsView \(i)")
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle("Test App")
}
}
}
// DetailsView //
struct DetailsView: View {
var test: Int
var body: some View {
ScrollView {
Text("More Cool \(test)")
Text("Cool \(test)")
Text("Less Cool \(test)")
}
}
init(test: Int) {
self.test = 8
let navBarAppearance = UINavigationBar.appearance()
navBarAppearance.backgroundColor = .clear
navBarAppearance.barTintColor = .clear
navBarAppearance.tintColor = .black
navBarAppearance.setBackgroundImage(UIImage(), for: .default)
navBarAppearance.shadowImage = UIImage()
}
}
struct DetailsView_Previews: PreviewProvider {
static var previews: some View {
DetailsView(test: 8)
}
}
It's a heavily edited version of my code, but it shows the problem I have. With no variables needing to be passed in, the init() worked to remove the bar in only that view. However, with that variable input, not only does it change all the views to the "8" for the number, but it also doesn't even hide the navigationBar. I'm not sure if I'm just doing something wrong nor if this is even the right way to do it, but any help would be appreciated!
Also, on a side note, does anyone know how to hide the statusBar in iOS 14 with the NavigationView?
I think you try to use UIKit logic instead of the SwiftUI one. This is what I would do to hide the navigation bar with a back button on the top leading side of your view.
As for hiding the status bar, I would use .statusBar(hidden: true).
But it seems not to work on iOS14. It may be a bug... You can refer yourself to the Apple documentation on this topic.
struct DetailsView: View {
#Environment(\.presentationMode) var presentation
var test: Int
var body: some View {
ZStack(alignment: .topLeading) {
ScrollView {
Text("More Cool \(test)")
Text("Cool \(test)")
Text("Less Cool \(test)")
}
Button(action: { presentation.wrappedValue.dismiss() }) {
HStack {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
.imageScale(.large)
Text("Back")
.font(.title3)
.foregroundColor(.blue)
}
}
.padding(.leading)
.padding(.top)
}
.navigationTitle(Text(""))
.navigationBarHidden(true)
.statusBar(hidden: true)
}
}

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}