Issue in Setting Value to #State variable in SwiftUI [duplicate] - swiftui

Hello I am running into a problem here and I do not have a consistent behavior between my .sheet() view when running on ios13 or ios14
I got a view like this :
#State private var label: String = ""
#State private var sheetDisplayed = false
///Some code
var body: some View {
VStack {
Button(action: {
self.label = "A label"
self.isDisplayed = true
}) {
Text("test")
}
}.sheet(isPresented: $sheetDisplayed, onDismiss: {
self.label = ""
}) {
Text(self.label)
}
}
On ios 13 this work as expected btn click -> set label -> call sheet -> display "A label" in a Text view.
On ios14 I got an empty string in self.label when in sheet closure, hence it does not display anything.
Did I missed something ? Is it an iOS 14 bug or did I had it wrong on ios13 and that got corrected.
PS: I have a couple of other variables that are passed in the closure I simplified it.

Your code have expectation of view update/creation order, but in general it is undefined (and probably changed in iOS 14).
There is explicit way to pass information inside sheet - use different sheet creator, ie. .sheet(item:...
Here is working reliable example. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var item: Item?
struct Item: Identifiable {
let id = UUID()
var label: String = ""
}
var body: some View {
VStack {
Button(action: {
self.item = Item(label: "A label")
}) {
Text("test")
}
}.sheet(item: $item, onDismiss: {
self.item = nil
}) {
Text($0.label)
}
}
}

This is some really strange behaviour in iOS 14, which doesn't appear to be documented.
Using the other answer here and the comment on this thread, I used #Binding to solve the issue as it seemed the cleanest and most SwiftUI-esq solution.
I have no idea why this behaviour has changed, and it seems less intuitive than before, so I'm assuming its a bug!
An example:
struct MainView: View {
#State private var message = ""
#State private var showSheet = false
var body: some View {
Button(action: {
self.message = "This will display the correct message"
self.showSheet = true
}, label: {
Text("Test Button")
})
.sheet(isPresented: self.$showSheet) {
SheetView(message: self.$message)
}
}
}
struct SheetView: View {
#Binding var message: Int
var body: some View {
Text(self.message)
}
}

The behaviour changed with SwiftUI 2.0, so it affects MacOS 11 as well, just adding a binding to the view fixes it even when that binding is never used, which makes me think this is an implementation bug.
Additionally just using the details state variable in a Text() within the body of the view also fixes it.
struct MyViewController : View {
#State var details: String?
#State var showDetails = false
// #Binding var havingAbindingFixesIt: String?
var body: some View {
VStack {
// Text(details ?? "")
Text("Tap here for details")
.onTapGesture {
self.details = "These are the details"
self.showDetails.toggle()
}
.sheet(isPresented: $showDetails) { Text(details ?? "") }
}
}
}

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)

SwiftUI iOS 16, close multiple modals not working

I'm trying to present multiple modals on top of my home view, but when I try to dismiss all modals, there is only the first one that close...
(I know, there is a lot of subject about this, but I didn't found any solution that was working for me...)
Any ideas?
Here is my testing code:
struct ContentView: View {
#State var presentA = false
var body: some View {
Button("Present A") { presentA = true }
.sheet(isPresented: $presentA) { ContentViewA(presentAll: $presentA) }
}
}
struct ContentViewA: View {
#Binding var presentAll: Bool
#State var presentB = false
var body: some View {
Button("Present B") { presentB = true }
.sheet(isPresented: $presentB) { ContentViewB(presentAll: $presentAll) }
}
}
struct ContentViewB: View {
#Binding var presentAll: Bool
var body: some View {
Button("Close all") {
presentAll = false
}
}
}
So when I touch the "Close all" button, I go back to the ContentViewA instead of the ContentView...
In my memory this was working with the previous version of SwiftUI but it seems that's not working anymore...
What am I doing wrong?
I don't think it is a valid user flow and SwiftUI does not handle it. The possible workaround is the same as for UIKit
Tested with Xcode 14b3 / iOS 16
var body: some View {
Button("Close all") {
UIApplication.shared.keyWindow?
.rootViewController?
.dismiss(animated: false, completion: nil) // false is important !!
}
}

#Appstorage is not updating based on my picker selection - SwiftUI - WatchApp

I have a picker that updates an variable bmr.
the picker in not updating the value. I put a test Text on the second screen to see if I can call the new value, its always showing the default.
import SwiftUI
struct ProfileView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Picker(selection: $bmr, label: Text("Enter your BMR:")) {
Text("50").tag(50)
Text("60").tag(60)
Text("70").tag(70)
Text("80").tag(80)
}
NavigationLink(destination: ContentView()) {
Text("Save")
}
}
}
}
struct ContentView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Text("Your BMR: \(bmr ?? 50)")
}
}
}
I am learning, so I don't know what to really try. I tried binding and unbinding, its not really working.
I tried to store variables and pass through views previously to test if it would update, and it worked fine. But passing info through views doesn't work with my app as I need this data to to be stored even if I exit.
The issue is bmr is an Int? while your tags are Int. Since they are not the same thing, the selection won't update it. The trick is to cast your tag as an Int? like this:
struct ProfileView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Picker(selection: $bmr, label: Text("Enter your BMR:")) {
Text("50").tag(50 as Int?)
Text("60").tag(60 as Int?)
Text("70").tag(70 as Int?)
Text("80").tag(80 as Int?)
}
NavigationLink(destination: ContentView()) {
Text("Save")
}
}
}
}
The tag and the selection types must EXACTLY match.

SwiftUI sheet infinite loop?

Is this a bug in SwiftUI? If you tap "Test" it goes into a loop, putting up sheet after sheet forever. I can't see why.
This is happening on iOS and iPadOS 15
struct ContentView: View {
#State private var showSheet = false
#State private var name: String = ""
#FocusState private var isFocused: Bool
var body: some View {
VStack {
Button("Test") { setState() }
}
.sheet(isPresented: $showSheet) {
VStack {
Text("\(showSheet.description), \(name)")
TextField("folder name", text: $name)
focused($isFocused)
}
}
}
private func setState() {
print("setState")
showSheet = true
}
}
This is a typo, and normally should be just closed as such, but I think the reason is interesting enough to warrant an answer here:
struct ContentView: View {
#State private var showSheet = false
#State private var name: String = ""
#FocusState private var isFocused: Bool
var body: some View {
VStack {
Button("Test") { setState() }
}
.sheet(isPresented: $showSheet) {
VStack {
Text("\(showSheet.description), \(name)")
TextField("folder name", text: $name)
focused($isFocused) //<-- THIS LINE IS MISSING A `.` -- it should be .focused($isFocused)
}
}
}
private func setState() {
print("setState")
showSheet = true
}
}
Because you've omitted a . on the focused line, focused is returning a modifier on ContentView itself rather than on the TextField. That means that it's adding a copy of ContentView to the view hierarchy below TextField (since focused returns a modified version of self) rather than modifying the TextField in place.
This is causing the loop, but because technically it's valid code, it doesn't generated a compiler error.

SwiftUI #State and .sheet() ios13 vs ios14

Hello I am running into a problem here and I do not have a consistent behavior between my .sheet() view when running on ios13 or ios14
I got a view like this :
#State private var label: String = ""
#State private var sheetDisplayed = false
///Some code
var body: some View {
VStack {
Button(action: {
self.label = "A label"
self.isDisplayed = true
}) {
Text("test")
}
}.sheet(isPresented: $sheetDisplayed, onDismiss: {
self.label = ""
}) {
Text(self.label)
}
}
On ios 13 this work as expected btn click -> set label -> call sheet -> display "A label" in a Text view.
On ios14 I got an empty string in self.label when in sheet closure, hence it does not display anything.
Did I missed something ? Is it an iOS 14 bug or did I had it wrong on ios13 and that got corrected.
PS: I have a couple of other variables that are passed in the closure I simplified it.
Your code have expectation of view update/creation order, but in general it is undefined (and probably changed in iOS 14).
There is explicit way to pass information inside sheet - use different sheet creator, ie. .sheet(item:...
Here is working reliable example. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var item: Item?
struct Item: Identifiable {
let id = UUID()
var label: String = ""
}
var body: some View {
VStack {
Button(action: {
self.item = Item(label: "A label")
}) {
Text("test")
}
}.sheet(item: $item, onDismiss: {
self.item = nil
}) {
Text($0.label)
}
}
}
This is some really strange behaviour in iOS 14, which doesn't appear to be documented.
Using the other answer here and the comment on this thread, I used #Binding to solve the issue as it seemed the cleanest and most SwiftUI-esq solution.
I have no idea why this behaviour has changed, and it seems less intuitive than before, so I'm assuming its a bug!
An example:
struct MainView: View {
#State private var message = ""
#State private var showSheet = false
var body: some View {
Button(action: {
self.message = "This will display the correct message"
self.showSheet = true
}, label: {
Text("Test Button")
})
.sheet(isPresented: self.$showSheet) {
SheetView(message: self.$message)
}
}
}
struct SheetView: View {
#Binding var message: Int
var body: some View {
Text(self.message)
}
}
The behaviour changed with SwiftUI 2.0, so it affects MacOS 11 as well, just adding a binding to the view fixes it even when that binding is never used, which makes me think this is an implementation bug.
Additionally just using the details state variable in a Text() within the body of the view also fixes it.
struct MyViewController : View {
#State var details: String?
#State var showDetails = false
// #Binding var havingAbindingFixesIt: String?
var body: some View {
VStack {
// Text(details ?? "")
Text("Tap here for details")
.onTapGesture {
self.details = "These are the details"
self.showDetails.toggle()
}
.sheet(isPresented: $showDetails) { Text(details ?? "") }
}
}
}