2

I have created a View that provides a convinient save button and a save method. Both can then be used inside a parent view. The idea is to provide these so that the navigation bar items can be customized, but keep the original implementation.

Inside the view there is one Textfield which is bound to a @State variable. If the save method is called from within the same view everthing works as expected. If the parent view calls the save method on the child view, the changes to the @State variable are not applied.

Is this a bug in SwiftUI, or am I am missing something? I've created a simple playbook implementation that demonstrates the issue.

Thank you for your help.

import SwiftUI import PlaygroundSupport struct ContentView: View { // Create the child view to make the save button available inside this view var child = Child() var body: some View { NavigationView { NavigationLink( destination: child.navigationBarItems( // Set the trailing button to the one from the child view. // This is required as this view might be inside a modal // sheet, and we need to add the cancel button as a leading // button: // leading: self.cancelButton trailing: child.saveButton ) ) { Text("Open") } } } } struct Child: View { // Store the value from the textfield @State private var value = "default" // Make this button available inside this view, and inside the parent view. // This makes sure the visibility of this button is always the same. var saveButton: some View { Button(action: save) { Text("Save") } } var body: some View { VStack { // Simple textfield to allow a string to change. TextField("Value", text: $value) // Just for the playground to change the value easily. // Usually it would be chnaged through the keyboard input. Button(action: { self.value = "new value" }) { Text("Update") } } } func save() { // This always displays the default value of the state variable. // Even after the Update button was used and the value did change inside // the textfield. print("\(value)") } } PlaygroundPage.current.setLiveView(ContentView()) 

5 Answers 5

1

I think a more SwiftUi way of doing it:

import SwiftUI import PlaygroundSupport struct ContentView: View { var body: some View { return NavigationView { // tell the child view where to render it's navigation item // Instead of configuring navigation items. NavigationLink(destination: Child(navigationSide: .left)) { Text("Open") } } } } struct Child: View { enum NavigationSide { case left, right } // If you really want to encapsulate all state in this view then @State // is a good choice. // If the parent view needs to read it, too, @Binding would be your friend here @State private var value: String = "default" // no need for @State as it's never changed from here. var navigationSide = NavigationSide.right // wrap in AnyView here to make ternary in ui code easier readable. var saveButton: AnyView { AnyView(Button(action: save) { Text("Save") }) } var emptyAnyView: AnyView { AnyView(EmptyView()) } var body: some View { VStack { TextField("Value", text: $value) Button(action: { self.value = "new value" }) { Text("Update") } } .navigationBarItems(leading: navigationSide == .left ? saveButton : emptyAnyView, trailing: navigationSide == .right ? saveButton : emptyAnyView) } func save() { print("\(value)") } } 
Sign up to request clarification or add additional context in comments.

Comments

1

TextField will only update your value binding when the return button is pressed. To get text changes that occur during editing, set up an observed object on Child with didSet. This was the playground I altered used from your example.

struct ContentView: View { var child = Child() var body: some View { NavigationView { NavigationLink( destination: child.navigationBarItems( trailing: child.saveButton ) ) { Text("Open") } } } } class TextChanges: ObservableObject { var completion: (() -> ())? @Published var text = "default" { didSet { print(text) } } } struct Child: View { @ObservedObject var textChanges = TextChanges() var saveButton: some View { Button(action: save) { Text("Save") } } var body: some View { VStack { TextField("Value", text: $textChanges.text).multilineTextAlignment(.center) Button(action: { print(self.textChanges.text) }) { Text("Update") } } } func save() { print("\(textChanges.text)") } } PlaygroundPage.current.setLiveView(ContentView()) 

4 Comments

Thank you @jnblanchard. I can confirm that this indeed works. Do you have any additional information on why? If I put a Text("\(name)") into my code (after the TextField), it is updated immediately after I type another letter. Also, the return key does not seem to affect any change with my code.
I used an observable object to connect the view with text data. On the observable object is a property observer (didSet) that is responsible for changes in the text value. Strange, I usually notice the value updating after pressing the return when using swiftui. But I kind of remember it not working unless I was using a device, and not the simulator.
Sorry for being unclear. I could see how you changed it and why it works. My question was more if you have any clue why the @State implementation does not work.
Mostly because state will provide an immutable wrappers. To really get down to the value you’ll need to unwrap the binding. I haven’t tried this, but by maybe using the wrappedValue property on your state you may reach the changed value.
0

Inside Child: value is mutable because it's wrapped with @State.

Inside ContentView: child is immutable because it's not wrapped with @State.

Your issue can be fixed with this line: @State var child = Child()

Good luck.

2 Comments

This doesn't change the outcome
While I would understand that it is immutable, the context from which the binding is changed should be the Child view. Changing var child to @State unfortunalty does not make any difference.
0

Child view needs to keep its state as a @Binding. This works:

import SwiftUI import PlaygroundSupport struct ContentView: View { @State var v = "default" var body: some View { let child = Child(value: $v) return NavigationView { NavigationLink( destination: child.navigationBarItems(trailing: child.saveButton) ) { Text("Open") } } } } struct Child: View { @Binding var value: String var saveButton: some View { Button(action: save) { Text("Save") } } var body: some View { VStack { TextField("Value", text: $value) Button(action: { self.value = "new value" }) { Text("Update") } } } func save() { print("\(value)") } } PlaygroundPage.current.setLiveView(ContentView()) 

2 Comments

This works indeed as expected. Unfortunately it exposes a lot of boilerplate to the outside view, which was just supposed to modify the navigation bar items. Do you have any additional information why this works?
I think the saveButton you assign as a navigationBarItem is captured including its action too early for your flow. Too much boilderplate: Well, my response was kinda assuming that your ContentView would be interested in the value that was saved from the ChildView. then it would make sense to provide the default value, too, right? If you really want to have everything local to your Child view (data and layout) I would re-think how you setup your views in general: It feels awkward to setup navigation items in the parent view. might be confusing later on. I'll create another answer showing
0

Based on this commend from @nine-stones (thank you!) I implemented a more SwiftUI way so solve my problem. It does not allow the customization of the navigation items as I planned, but that was not the problem that needed to be solved. I wanted to use the Child view in a navigation link, as well as inside a modal sheet. The problem was how to perform custom cancel actions. This is why I removed the button implementation and replaced it with a cancelAction closure. Now I can display the child view wherever and however I want.

One thing I still do not know why SwiftUI is not applying the child context to the button inside the saveButton method.

Still, here is the code, maybe it helps someone in the future.

import SwiftUI import PlaygroundSupport struct ContentView: View { @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { NavigationLink( destination: Child( // Instead of defining the buttons here, I send an optional // cancel action to the child. This will make it possible // to use the child view on navigation links, as well as in // modal dialogs. cancelAction: { self.presentationMode.wrappedValue.dismiss() } ) ) { Text("Open") } } } } struct Child: View { // Store the value from the textfield @State private var value = "default" @Environment(\.presentationMode) var presentationMode var cancelAction: (() -> Void)? // Make this button available inside this view, and inside the parent view. // This makes sure the visibility of this button is always the same. var saveButton: some View { Button(action: save) { Text("Save") } } var body: some View { VStack { // Simple textfield to allow a string to change. TextField("Value", text: $value) // Just for the playground to change the value easily. // Usually it would be chnaged through the keyboard input. Button(action: { self.value = "new value" }) { Text("Update") } } .navigationBarItems( leading: self.cancelAction != nil ? Button(action: self.cancelAction!, label: { Text("Cancel") }) : nil, trailing: self.saveButton ) } func save() { // This always displays the default value of the state variable. // Even after the Update button was used and the value did change inside // the textfield. print("\(value)") } } PlaygroundPage.current.setLiveView(ContentView()) 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.