sheet does not dismiss on swiftUI - swiftui

#Environment(\.presentationMode) var presentationMode
#Environment(\.dismiss) var dismiss
I know just toggle State variable of 'fullScreenCover' or 'sheet' but I found another approach and both above doesn't work.
Here's simple code.
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.dismiss) var dismiss
#State var sheetState = false
#State private var value = ""
var body: some View {
VStack {
Text(value)
Button("Present Sheet") {
sheetState.toggle()
}
}
.sheet(isPresented: $sheetState) {
Button("set1") {
value = "1"
//presentationMode.wrappedValue.dismiss()
dismiss()
}
}
}
}
When I press the button of sheet named 'set1' it change the 'value' set '1' and Text of VStack set "1" correctly but sheet doesn't dismiss.
It absolutely same both approach.
Am I using these things wrong?
here's my result.
https://imgur.com/a/5cjExyv

Technically you are trying to dismiss ContentView which makes no sense.
You have to create a separate View struct for the sheet.
struct ContentView: View {
#State private var sheetState = false
#State private var value = ""
var body: some View {
VStack {
Text(value)
Button("Present Sheet") {
sheetState.toggle()
}
}
.sheet(isPresented: $sheetState) {
SheetView(value: $value)
}
}
}
struct SheetView : View {
#Environment(\.dismiss) var dismiss
#Binding var value : String
var body: some View {
Button("set1") {
value = "1"
dismiss()
}
}
}

Related

SwiftUI publishing an environment change from within view update

The app has a model that stores the user's current preference for light/dark mode, which the user can change by clicking on a button:
class DataModel: ObservableObject {
#Published var mode: ColorScheme = .light
The ContentView's body tracks the model, and adjusts the colorScheme when the model changes:
struct ContentView: View {
#StateObject private var dataModel = DataModel()
var body: some View {
NavigationStack(path: $path) { ...
}
.environmentObject(dataModel)
.environment(\.colorScheme, dataModel.mode)
As of Xcode Version 14.0 beta 5, this is producing a purple warning: Publishing changes from within view updates is not allowed, this will cause undefined behavior. Is there another way to do this? Or is it a hiccup in the beta release? Thanks!
Update: 2022-09-28
Xcode 14.1 Beta 3 (finally) fixed the "Publishing changes from within view updates is not allowed, this will cause undefined behavior"
See: https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/
Full disclosure - I'm not entirely sure why this is happening but these have been the two solutions I have found that seem to work.
Example Code
// -- main view
#main
struct MyApp: App {
#StateObject private var vm = ViewModel()
var body: some Scene {
WindowGroup {
ViewOne()
.environmentObject(vm)
}
}
}
// -- initial view
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
// -- sheet view
struct SheetView: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Close sheet")
}
}
}
// -- view model
class ViewModel: ObservableObject {
#Published var isPresented: Bool = false
}
Solution 1
Note: from my testing and the example below I still get the error to appear. But if I have a more complex/nested app then the error disappears..
Adding a .buttonStyle() to the button that does the initial toggling.
So within the ContentView on the Button() {} add in a .buttonStyle(.plain) and it will remove the purple error:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.buttonStyle(.plain) // <-- here
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
^ This is probably more of a hack than solution since it'll output a new view from the modifier and that is probably what is causing it to not output the error on larger views.
Solution 2
This one is credit to Alex Nagy (aka. Rebeloper)
As Alex explains:
.. with SwiftUI 3 and SwiftUI 4 the data handling kind of changed. How SwiftUI handles, more specifically the #Published variable ..
So the solution is to have the boolean trigger to be a #State variable within the view and not as a #Published one inside the ViewModel. But as Alex points out it can make your views messy and if you have a lot of states in it, or not be able to deep link, etc.
However, since this is the way that SwiftUI 4 wants these to operate, we run the code as such:
// -- main view
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ViewOne()
}
}
}
// -- initial view
struct ViewOne: View {
#State private var isPresented = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
// SheetView() <-- if using dismiss() in >= iOS 15
}
}
}
// -- sheet view
struct SheetView: View {
// I'm showing a #Binding here for < iOS 15
// but you can use the dismiss() option if you
// target higher
// #Environment(\.dismiss) private var dismiss
#Binding var isPresented: Bool
var body: some View {
Button {
isPresented.toggle()
// dismiss()
} label: {
Text("Close sheet")
}
}
}
Using the #Published and the #State
Continuing from the video, if you need to still use the #Published variable as it might tie into other areas of your app you can do so with a .onChange and a .onReceive to link the two variables:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.onReceive(vm.$isPresented) { newValue in
isPresented = newValue
}
.onChange(of: isPresented) { newValue in
vm.isPresented = newValue
}
}
}
However, this can become really messy in your code if you have to trigger it for every sheet or fullScreenCover.
Creating a ViewModifier
So to make it easier for you to implement it you can create a ViewModifier which Alex has shown works too:
extension View {
func sync(_ published: Binding<Bool>, with binding: Binding<Bool>) -> some View {
self
.onChange(of: published.wrappedValue) { newValue in
binding.wrappedValue = newValue
}
.onChange(of: binding.wrappedValue) { newValue in
published.wrappedValue = newValue
}
}
}
And in use on the View:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.sync($vm.isPresented, with: $isPresented)
// .onReceive(vm.$isPresented) { newValue in
// isPresented = newValue
// }
// .onChange(of: isPresented) { newValue in
// vm.isPresented = newValue
// }
}
}
^ Anything denoted with this is my assumptions and not real technical understanding - I am not a technical knowledgeable :/
Try running the code that's throwing the purple error asynchronously, for example, by using DispatchQueue.main.async or Task.
DispatchQueue.main.async {
// environment changing code comes here
}
Task {
// environment changing code comes here
}
Improved Solution of Rebel Developer
as a generic function.
Rebeloper solution
It helped me a lot.
1- Create extension for it:
extension View{
func sync<T:Equatable>(_ published:Binding<T>, with binding:Binding<T>)-> some View{
self
.onChange(of: published.wrappedValue) { published in
binding.wrappedValue = published
}
.onChange(of: binding.wrappedValue) { binding in
published.wrappedValue = binding
}
}
}
2- sync() ViewModel #Published var to local #State var
struct ContentView: View {
#EnvironmentObject var viewModel:ViewModel
#State var fullScreenType:FullScreenType?
var body: some View {
//..
}
.sync($viewModel.fullScreenType, with: $fullScreenType)

How to dismiss sheet from within NavigationLink

In the following example, I have a view that shows a sheet of ViewOne. ViewOne has a NavigationLink to ViewTwo.
How can I dismiss the sheet from ViewTwo?
Using presentationMode.wrappedValue.dismiss() navigates back to ViewOne.
struct ContentView: View {
#State private var isShowingSheet = false
var body: some View {
Button("Show sheet", action: {
isShowingSheet.toggle()
})
.sheet(isPresented: $isShowingSheet, content: {
ViewOne()
})
}
}
struct ViewOne: View {
var body: some View {
NavigationView {
NavigationLink("Go to ViewTwo", destination: ViewTwo())
.isDetailLink(false)
}
}
}
struct ViewTwo: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss sheet here") {
presentationMode.wrappedValue.dismiss()
}
}
}
This may depend some on platform -- in a NavigationView on macOS, for example, your existing code works.
Explicitly passing a Binding to the original sheet state should work:
struct ContentView: View {
#State private var isShowingSheet = false
var body: some View {
Button("Show sheet", action: {
isShowingSheet.toggle()
})
.sheet(isPresented: $isShowingSheet, content: {
ViewOne(showParentSheet: $isShowingSheet)
})
}
}
struct ViewOne: View {
#Binding var showParentSheet : Bool
var body: some View {
NavigationView {
NavigationLink("Go to ViewTwo", destination: ViewTwo(showParentSheet: $showParentSheet))
//.isDetailLink(false)
}
}
}
struct ViewTwo: View {
#Binding var showParentSheet : Bool
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss sheet here") {
showParentSheet = false
}
}
}

Edit Text Editor SwiftUI

I am trying to edit the text of a binding value from another view. But text editor does not allow me to do so.
First I need to see the previous value of selectedNote.title in TextEditor and then I should be able to edit the title.
struct EditNotePageView: View {
#Binding var selectedNote: CDNoteModel!
#State var text = ""
var body: some View {
TextEditor(selectedNote.title!, text: $text)
}
}
I also tried like this. I can see my selectedNote.title in the simulator but when I tap on it it does nothing.
struct EditNotePageView: View {
#Binding var selectedNote: CDNoteModel!
#State var text = ""
var body: some View {
Text(selectedNote.title!)
.onTapGesture {
text = selectedNote.title!
TextEditor(text: $text)
}
}
}
ZStack Text and textEditor solved the problem with a little pinch of .onAppear
struct EditNotePageView: View {
#Binding var selectedNote: CDNoteModel!
#State var text = ""
var body: some View {
ZStack(alignment: .leading){
Text(selectedNote.title!)
.onAppear {
text = selectedNote.title!
}
TextEditor(text: $text)
}
}
}
You can use onChange property
struct EditNotePageView: View {
#Binding var selectedNote: CDNoteModel!
#State var text = ""
var body: some View {
TextEditor("", text: $text)
.onChange(of: text, perform: { value in
selectedNote.title = value
}
}
}
you could try this:
struct EditNotePageView: View {
#Binding var selectedNote: CDNoteModel
var body: some View {
TextEditor(text: $selectedNote.title)
}
}

Presenting a Sheet modally, #State value is not changing

I have tried much time on it but I couldn't figure out why it is not working.
The problem is when I tap the button, the new value inside the sheet is not updated. It always show the same value which is set up in start.
#State var value:String = "empty"
#State var explorePageIsEnabled:Bool = false
VStack{
Button("tap me"){
value = "the new one"
exploreStatusIsEnabled.toggle()
}
.sheet(isPresented: $exploreStatusIsEnabled, content: {
Text(value)
})
}
Deployment target is IOS 14+
Create a separate struct view for text and use Binding.
struct SheetView: View {
#Binding var value: String
var body: some View {
Text(value)
}
}
struct ContentView: View {
#State private var value: String = "empty"
#State private var explorePageIsEnabled: Bool = false
var body: some View {
VStack{
Button("tap me"){
value = "the new one"
explorePageIsEnabled.toggle()
}
.sheet(isPresented: $explorePageIsEnabled, content: {
SheetView(value: $value)
})
}
}
}

SwiftUI sheet never updated after first launch

I use a modal sheet whose content is updated for each call. However, when the content is marked as #State, the view body is never updated.
Is anyone seeing this as well? Is a workaround available?
This is the calling view:
struct ContentView: View {
#State var isPresented = false
#State var i = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}
.sheet(
isPresented: $isPresented,
content: {
SheetViewNOK(i: self.i)
}
)
}
}
This does work:
struct SheetViewOK: View {
var i: Int
var body: some View {
Text("Hello \(i)") // String is always updated
}
}
This does not work. But obviously, in a real app, I need to use #State because changes made by the user need to be reflected in the sheet's content:
struct SheetViewNOK: View {
#State var i: Int
var body: some View {
Text("Hello \(i)") // String is never updated after creation
}
}
In your .sheet you are passing the value of your ContentView #State to a new #State. So it will be independent from the ContentView.
To create a connection or a binding of your ContentView #State value, you should define your SheetView var as #Binding. With this edit you will pass the binding of your state value to your sheet view.
struct SheetView: View {
#Binding var i: Int
var body: some View {
Text("Hello \(i)")
}
}
struct ContentView: View {
#State var isPresented = false
#State var i: Int = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}.sheet(
isPresented: $isPresented,
content: {
SheetView(i: self.$i)
})
}
}
There are 3 different ways
use a binding
use multiple .sheets
use $item
This is fully explained in this video.
Multiple Sheets in a SwiftUI View