I have a TabView and each Tab has it's own NavigationPath which I am handling inside an ObservableObject.
#MainActor final class Router: ObservableObject {
#Published var homeNavigationPath = NavigationPath()
#Published var searchNavigationPath = NavigationPath()
#Published var notificationsNavigationPath = NavigationPath()
#Published var profileNavigationPath = NavigationPath()
}
I am doing this so each Tab can PopToRoot of it's own accord. Everything seems to be working great, however I get the following message in the Console...
Update NavigationAuthority bound path tried to update multiple times
per frame.
Does anyone know what this means? Am I doing something wrong or is it perfectly fine to have multiple NavigationPaths like this?
Related
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()
}
}
}
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.
I have an app where there are multiple screens with textfields to create some new object. When the user selects "Create" on the last screen, an API call is performed which creates the new object.
From there I want to push the detail page of the newly created object, and (when the view is no longer visible) remove all the screens with textfields (as that is no longer relevant, and would only cause confusion. Luckily there is only one screen that should remain before the detailpage.
In UIKit, this would be performed by doing a push on the navigationController, and then editing the viewControllers array of the navigationController in the viewDidLoad of the new screen.
If I am correct, there is no way to edit the views in a SwiftUI NavigationView, so how can I perform this action in SwiftUI?
I solved it by adding an id to the NavigationView, and then setting this to another id in the viewmodel when it should reset.
Like this:
struct MyView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
// ...
}
.id(viewModel.id)
}
}
In the viewmodel:
class ViewModel: ObservableObject {
#Published var id = UUID().uuidString
func reset() {
id = UUID().uuidString
}
}
This resets the NavigationView as Swift thinks it's a different view because the id changed.
ok I'm working on a first swiftui app as a way of learning and have used conditionals to colour some buttons. The problem is when the related state changes (Bool) the conditional isn't being updated. I thought this was to do with a UI redraw not being triggered due to state however I can't seem to figure out a solution to that.
Background info: task is an instance of the Task class (passed in from another view) which is an array on the ModelData Environment object. when the button is clicked the related value (isHotList) is indeed changed but the image colour isn't changed.
#EnvironmentObject var modelData: ModelData
var task: Task
Button(action: {
task.isHotList.toggle()
}){
Image(systemName: "flame.fill")
.foregroundColor(task.isHotList ? Color.orange : Color.gray)
}
Make the Test an observable object, like
class Task: ObservableObject {
#Published var isHotList = false
// ... other content here
}
and make test property in view wrapped into ObservedObject, like
#EnvironmentObject var modelData: ModelData
#ObservedObject var task: Task // << here !!
Alternatively make the Task as struct and then you can use either #State or #Binding for task property (depending on your design).
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.