Passing data between views in SwiftUI? - swiftui

I'm trying to pass a few simple data (strings and URL and images) from one view to another view in my swiftui app.
This simple task proves to be a headache at the moment.
This is what I have done so far:
in my firstView I have this:
struct Book {
var title: String
var author: String
}
struct firstView: View {
#State private var showAudioPlayer = false
#Binding var tabSelection: Int
init(tabSelection: Binding<Int>) {
_tabSelection = tabSelection
}
var body: some View {
NavigationView {
ZStack {
Text("Tap Here")
.onTapGesture {
self.showAudioPlayer.toggle()
}
}
}
.fullScreenCover(isPresented: $showAudioPlayer, content: secondView.init)
}
}
And this is what I have in my secondView:
struct secondView: View {
#Environment(\.presentationMode) var presentationMode
var book: Book
var body: some View {
NavigationView {
ZStack{
VStack {
}
}
.background(Color.clear)
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(
leading: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.down.circle")
.foregroundColor(Color.black)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
}
}
However, when I try to compile my code, I get this error:
The error itself doesn't make much sense and I'm stuck.
what am I doing wrong?

Memberwise Initializers for Structure Types
The compiler takes a peak into the struct and sees two non-optional properties and thinks they should be in the constructor. It doesn't know the #Environment is going to magically be set by the framework, so it includes that property. As #Baglan said above, you are on the hook for the Book and need to pass it in.
The error message is saying "you asked me to instance audioPlayerView and I need a couple of parameters to make that happen", but you really only need one.

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.

Dismissing view using ObservableObject class provokes Publishing changes from within view updates is not allowed

When dismissing a fullScreenCover using a variable inside an ObservableObject (lines commented with 1.-) it shows the "Publishing changes from within view updates is not allowed, this will cause undefined behavior." message in the console, but using a #State variable (lines commented with 2.-) does not show the warning. I do not understand why.
Here is the code:
import SwiftUI
final class DismissWarningVM: ObservableObject {
#Published var showAnotherView = false
}
struct DismissWarningView: View {
#StateObject private var dismissWarningVM = DismissWarningVM()
#State private var showAnotherView = false
var body: some View {
VStack {
HStack {
Spacer()
Button {
// 1.- This line provokes the warning
dismissWarningVM.showAnotherView = true
// 2.- This line DO NOT provokes the warning
//showAnotherView = true
} label: {
Text("Show")
}
}
.padding(.trailing, 20)
Spacer()
Text("Main view")
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.white)
// 1.- This line provokes the warning
.fullScreenCover(isPresented: $dismissWarningVM.showAnotherView) {
// 2.- This line DO NOT provokes the warning
//.fullScreenCover(isPresented: $showAnotherView) {
AnotherView()
}
}
}
struct AnotherView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
VStack(spacing: 30) {
Text("Another view")
Button {
dismiss()
} label: {
Text("Dismiss")
.foregroundColor(.red)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
}
}
struct DismissWarningView_Previews: PreviewProvider {
static var previews: some View {
DismissWarningView()
}
}
Fixed. It was a problem of the XCode version. I was trying with 14.0.1 version but after updating to 14.1 version the warning is no longer shown
#StateObject isn't designed for view model objects. In SwiftUI the View struct is the view model already you don't another one. Remember SwiftUI is diffing these structs and creating/updating/removing UIView objects automatically for us. If you use view model objects then you'll have viewModel object -> View struct -> UIView object which is a big mess and will lead to bugs. #StateObject is designed for when you need a reference type in an #State which isn't very often nowadays given we have .task and .task(id:) for asynchronous features.
You can achieve what you need like this:
struct WarningConfig {
var isPresented = false
// mutating func someLogic() {}
}
struct SomeView: View {
#State private var config = WarningConfig()
...
.fullScreenCover(isPresented: $config.isPresented) {
WarningView(config: $config)

SwiftUI passing selected date from modal to parent variables

Need help with this please.
I have a view with 2 date variables and I want to show a modal which have the datepicker and let user pick different dates for these variables.
Currently I have two buttons that show the same sheet but pass different variable to the modal.
The problem the variable don’t update after dismissing the modal.
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State private var secOneDate = Date()
#State private var secTwoDate = Date()
#State private var isDatepickerPresented = false
var body: some View {
VStack {
HStack{
Button{
isDatepickerPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.indigo)
}
.sheet(isPresented: $isDatepickerPresented){
DatePickView(selectDate: $secOneDate)
}
Text("SecOneDate: \(secOneDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
HStack{
Button{
isDatepickerPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.mint)
}
.sheet(isPresented: $isDatepickerPresented)
{
DatePickView(selectDate: $secTwoDate)
}
Text("SecTwoDate: \(secTwoDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
}
}
}
import SwiftUI
struct DatePickView: View {
#Environment(\.dismiss) private var dismiss
#Binding var selectDate: Date
var body: some View {
VStack(alignment: .center, spacing: 20) {
HStack {
Text("\(selectDate)")
.padding()
Spacer()
Button {
dismiss()
} label: {
Image(systemName: "delete.backward.fill")
.foregroundColor(.indigo)
}
}.padding()
DatePicker("", selection: $selectDate)
.datePickerStyle(.graphical)
}
}
}
First of all, thank you for your minimal, reproducible example: it is clear and can be immediately used for debugging. Answering to your question:
The problem with your code is that you have only one variable that opens the sheet for both dates. Even though you are correctly passing the two different #Bindings, when you toggle isDatepickerPresented you are asking SwiftUI to show both sheets, but this will never happen. Without knowing, you are always triggering the first of the sheet presentations - the one that binds secOneDate. The sheet that binds secTwoDate is never shown because you can't have two sheets simultaneously.
With that understanding, the solution is simple: use two different trigger variables. Here's the code corrected (DatePickView doesn't change):
struct Example: View {
#State private var secOneDate = Date()
#State private var secTwoDate = Date()
#State private var isDatepickerOnePresented = false
#State private var isDatepickerTwoPresented = false
var body: some View {
VStack {
HStack{
Button{
isDatepickerOnePresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.indigo)
}
.sheet(isPresented: $isDatepickerOnePresented){
DatePickView(selectDate: $secOneDate)
}
Text("SecOneDate: \(secOneDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
HStack{
Button{
isDatepickerTwoPresented = true
} label: {
Image(systemName: "calendar")
.imageScale(.large)
.foregroundColor(.mint)
}
.sheet(isPresented: $isDatepickerTwoPresented) {
DatePickView(selectDate: $secTwoDate)
}
Text("SecTwoDate: \(secTwoDate.formatted(date: .abbreviated, time: .shortened))")
}
.padding()
}
}
}

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)
}
}

Aligning a Text and TextField inside the Section of a Form

Another simple SwiftUI tasks that is causing me more trouble than it should.
I can't figure a way to align the Text and TextField correctly.
None of the HSTack alignment seem to yield acceptable results.
import SwiftUI
struct SignIn: View {
#State var email: String = ""
var body: some View {
VStack {
Text("Sign In")
.font(.largeTitle)
Form {
Section {
HStack {
Text("ID")
TextField("Email", text: $email)
}
}
}
}
}
}
struct SignIn_Previews: PreviewProvider {
static var previews: some View {
SignIn()
}
}
You mentioned trying different HStack alignments, did you try .firstTextBaseline or .lastTextBaseline? Both of these align the Text and TextField correctly for me. So that line becomes
HStack(alignment: .lastTextBaseline) {
Screenshot of Result
Full code:
import SwiftUI
struct SignIn: View {
#State var email: String = ""
var body: some View {
VStack {
Text("Sign In")
.font(.largeTitle)
Form {
Section {
HStack(alignment: .lastTextBaseline) {
Text("ID")
TextField("Email", text: $email)
}
}
}
}
}
}
struct SignIn_Previews: PreviewProvider {
static var previews: some View {
SignIn()
}
}
One solution is to use another TextField instead of a Text:
HStack {
TextField("", text: .constant("ID"))
.fixedSize()
.disabled(true)
TextField("Email", text: $email)
Spacer()
}
It's kinda ugly though.
I was having this same issue, and the answer is simpler than I thought. I ended up adding the modifier .aspectRatio(.fit) and that fixed it for me.
Hope this helps!