#State variable getting reset for no reason [ios 15 SwiftUI] - swiftui

I am using a #State variable to store a tag of some navigation Links.
But in ios 15, I notice that whenever I go to background, the state variable is getting reset, which causes that when I go back into the app, the view get popped for no reason.
#State private var userNavigateSelection: String? = nil
NavigationLink(destination: TestView(), tag: TestTag, selection: $userNavigateSelection) { EmptyView() }
I used a onChange method to monitor the changing of "userNavigateSelection", everytime I go into background. "userNavigationSelection" will be changed to nil. I have no idea why.

Perhaps your view containing #State variable is removed from view hierarchy after coming back to foreground. Try moving this variable higher in hierarchy and pass to view containing NavigationLink with #Binding.
Check: https://www.ralfebert.com/swiftui/state-lifetime/

Related

Swiftui nested navigation issue while using navigation link

My swiftui application structure looks like this
Navigation View (enclosing the landing view that is a list view )
On selection of a List item Navigation link directs to a Tab View with three tabs (default first tab)
When I use a sole standalone navigation link inside tab view screens to direct to another screen programatically, it navigates succesfully to the mentioned destination, but my binding doesn't work to come back to the previous screen.
Parent View
#State var showCameraPreviewView : Bool = false
ZStack{
Button("Show camera") {
showCameraPreviewView = true
}
NavigationLink(destination: CameraView(showCameraPreviewView: $showCameraPreviewView),isActive: $showCameraPreviewView){
EmptyView()
}
}
Child View
#Binding var showCameraPreviewView
Button("Assume capture success"){
self.showCameraPreviewView = false
}
Toggling showCameraPreviewView binding to false in the destination doesn't get me back to the current screen.
Looks straight forward, but doesn't work ! anything that I'm doing wrong ?
I can reproduce your issue, quite strange ... seems like the change of showCameraPreviewView is not accepted because the view is still visible. But I found a workaround with dismiss:
EDIT for iOS 14:
struct ChildView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var show: Bool
var body: some View {
Button("Assume capture success"){
show = false
presentationMode.wrappedValue.dismiss()
}
}
}

Binding<String> action tried to update multiple times per frame in SwiftUI

I have a VM that is implemented as follows:
LoginViewModel
class LoginViewModel: ObservableObject {
var username: String = ""
var password: String = ""
}
In my ContentView, I use the VM as shown below:
#StateObject private var loginVM = LoginViewModel()
var body: some View {
NavigationView {
Form {
TextField("User name", text: $loginVM.username)
TextField("Password", text: $loginVM.password)
Every time I type something in the TextField it shows the following message in the output window:
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
It is a message and not an error.
If I decorate my username and password properties with #Published then the message goes away but the body is rendered each time I type in the TextField.
Any ideas what is going on and whether I should use #Published or not. I don't think I will gain anything from putting the #Published attribute since this is a one-way binding and I don't want to display anything on the view once the username changes.
If I decorate my username and password properties with #Published then the message goes away
This is the correct solution. You need to use #Published on those properties because that is how SwiftUI gets notified when the properties change.
the body is rendered each time I type in the TextField
That is fine. Your body method is not expensive to compute.
I don't think I will gain anything from putting the #Published attribute since this is a one-way binding
You cannot be sure SwiftUI will work correctly (now or in future releases) if you don't use #Published. SwiftUI expects to be notified when the value of a Binding changes, even when a built-in SwiftUI component like TextField causes the change.
For the simple case - the state is kept in the same view or in a ModelSupport class, consists of strings or other primitive types, and there's only one of each, #Published will work fine.
I got this error with a model class containing an array of structs and using a List, and every time you type inside a TextField inside a list (or every time you select an item in a list), the view gets refreshed, and the error gets triggered.
I am thus using a DelayedTextField:
struct DelayedTextField: View {
var title: String = ""
#Binding var text: String
#State private var tempText: String = ""
var body: some View {
TextField(title, text: $tempText, onEditingChanged: { editing in
if !editing {
$text.wrappedValue = tempText
}
})
.onAppear {
tempText = text
}
}
}
and the binding update error is no more.

NavigationLink doesn't fire after FullScreenCover is dismissed

I have a button in a view (inside a NavigationView) that opens a full screen cover - a loading screen while some data is processing. When the cover is dismissed, I want to automatically route to the next view programmatically. I'm using a NavigationLink with a tag and selection binding, and the binding value updates when the cover is dismissed, but the routing doesn't happen unless I tap that same "open modal" button again.
import SwiftUI
struct OpenerView: View {
#EnvironmentObject var viewModel: OpenerViewModel
#State private var selection: Int? = nil
#State private var presentLoadingScreen = false
var body: some View {
VStack {
NavigationLink(destination: SecondScreen(), tag: 1, selection: $selection) { EmptyView() }
Button(action: {
viewModel.frequency = 0
self.presentLoadingScreen.toggle()
}, label: {
Text("Open cover")
}).buttonStyle(PlainButtonStyle())
}
.navigationBarTitle("Nav title", displayMode: .inline)
.fullScreenCover(isPresented: $presentLoadingScreen, onDismiss: {
self.selection = 1
}, content: ModalView.init)
}
}
struct ModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Howdy")
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
presentationMode.wrappedValue.dismiss()
}
}
}
}
The first time I hit the Button, the cover opens. Inside the cover is just a DispatchQueue.main.asyncAfter which dismisses it after 2 seconds. When it's dismissed, the onDismiss fires, but then I have to hit the button again to route to SecondScreen.
Any ideas?
Edit: Added the modal's View
I got it working with some changes to the code, and I'm sharing here along with what I think is happening.
I believe the problem is a race condition using a #State boolean to toggle the cover and the navigation. When the cover is being dismissed, my main OpenerView is being recreated - to be expected with state changes. Because of this, I try to set the #State var selection to trigger the navigation change, but before it can do so, the view is recreated with selection = nil.
There seem to be two ways to solve it in my case:
Move the cover boolean to my view model - this worked, but I didn't want it there because it only applied to this view and it's a shared view model for this user flow. Plus, when the modal is dismissed, you see the current OpenerView for a brief flash and then get routed to the SecondScreen.
Keep the cover boolean in #State, but trigger the navigation change in the button immediately after setting the boolean to open the modal. This worked better for my use case because the modal opens, and when it closes, the user is already on the next screen.
I had a similar problem where I was trying to draw a view after dismissing a fullScreenCover. I kept getting an error that said that the view had been deallocated since it was trying to draw to the fullScreenCover.
I used Joe's hints above to make this work. Specifically:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
viewToShow()
}
I had previously tried onChange, onDisappear, onAppear - but none of those fit the use case I needed.

onReceive in SwiftUI view causes infinite loop

In a SwiftUI app, I have an ObservableObject that keeps track of user settings:
class UserSettings: ObservableObject {
#Published var setting: String?
}
I have a view model to control the state for my view:
class TestViewModel: ObservableObject {
#Published var state: String = ""
}
And I have my view. When the user setting changes, I want to get the view model to update the state of the view:
struct HomeView: View {
#EnvironmentObject var userSettings: UserSettings
#ObservedObject var viewModel = TestViewModel()
var body: some View {
Text(viewModel.state)
.onReceive(userSettings.$setting) { setting in
self.viewModel.state = setting
}
}
}
When the UserSettings.setting is changed in another view it causes onReceive on my view to get called in an infinite loop, and I don't understand why. I saw this question, and that loop makes sense to me because the state of the ObservableObject being observed is being changed on observation.
However, in my case I'm not changing the observed object (environment object) state. I'm observing the environment object and changing the view model state which redraws the view.
Is the view redrawing what's causing the issue here? Does onReceive get called everytime the view is redrawn?
Is there a better way of accomplishing what I'm trying to do?
EDIT: this is a greatly simplified version of my problem. In my app, the view model takes care of executing a network request based on the user's settings and updating the view's state such as displaying an error message or loading indicator.
Whenever you have an onReceive with an #ObservedObject that sets another (or the same) published value of the #ObservedObject you risk creating an infinite loop if those published attributes are being displayed somehow.
Make your onReceive verify that the received value is actually updating a value, and not merely setting the same value, otherwise it will be setting/redrawing infinitely. In this case, e.g.,:
.onReceive(userSettings.$setting) { setting in
if setting != self.viewModel.state {
self.viewModel.state = setting
}
}
From described scenario I don't see the reason to duplicate setting in view model. You can show the value directly from userSettings, as in
struct HomeView: View {
#EnvironmentObject var userSettings: UserSettings
#ObservedObject var viewModel = TestViewModel()
var body: some View {
Text(userSettings.setting)
}
}
You might be able to prevent infinite re-rendering of the view body by switching your #ObservedObject to #StateObject.

Implementing business and other logic in SwiftUI

(This is my first SwiftUI project; please be kind if this is a stupid question.)
I have a collection of objects which are displayed in a Picker. The picker selection is $selectedIndex, where
#State private var selectedIndex: Int = 0
I also have a
#State private var opts: OptsStruct = OptsStruct()
where elements of the OptsStruct structure are bound to SwiftUI views. The value of opts needs to change when the selectedIndex changes, because the opts property is the option shown in and selected by the Picker. (Also, I want to save the current value of selectedIndex in UserDefaults.) The problem is that I don't understand how to express these actions in SwiftUI.
I tried
#State private var selectedIndex: Int = 0 {
mutating didSet {
// save selectedIndex to UserDefaults
opts = f(selectedIndex)
}
but this causes a Segmentation Fault.
Where is the 'correct' place to put this logic. (And in general, can someone suggest some reading on how to connect changes to SwiftUI #States with general business logic.)
Thanks,
Rick
The idea of a #State variable is for it to be the single source of truth (wikipedia). This means that one variable should be the only thing that contains the "state" of your picker. In this case, I suggest using this:
$opts.selectionIndex
as the Binding for your picker. selectionIndex would then be a Int property of your OptsStruct type.