简体   繁体   English

@State 变量的值不会改变

[英]Value from @State variable does not change

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.在视图中有一个绑定到 @State 变量的文本字段。 If the save method is called from within the same view everthing works as expected.如果从同一视图中调用 save 方法,一切都会按预期工作。 If the parent view calls the save method on the child view, the changes to the @State variable are not applied.如果父视图调用子视图的保存方法,则不会应用对@State 变量的更改。

Is this a bug in SwiftUI, or am I am missing something?这是 SwiftUI 中的错误,还是我遗漏了什么? 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()) 

TextField will only update your value binding when the return button is pressed. TextField 只会在按下返回按钮时更新您的值绑定。 To get text changes that occur during editing, set up an observed object on Child with didSet.要获取编辑期间发生的文本更改,请使用 didSet 在 Child 上设置观察到的 object。 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())

I think a more SwiftUi way of doing it:我认为更 SwiftUi 的做法是:

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)")
  }
}

Inside Child : value is mutable because it's wrapped with @State .Child内部: value可变的,因为它被@State包裹着。

Inside ContentView : child is immutable because it's not wrapped with @State .ContentView内部: child不可变的,因为它没有@State包装。

Your issue can be fixed with this line: @State var child = Child()您的问题可以通过以下行解决: @State var child = Child()

Good luck.祝你好运。

Child view needs to keep its state as a @Binding . Child视图需要将其 state 保留为@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())

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.基于@nine-stones 的这个推荐(谢谢。)我实现了更多 SwiftUI 方式来解决我的问题,它不允许按我的计划自定义导航项。 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.我想在导航链接以及模态表中使用Child视图。 The problem was how to perform custom cancel actions.问题是如何执行自定义取消操作。 This is why I removed the button implementation and replaced it with a cancelAction closure.这就是我删除按钮实现并将其替换为cancelAction闭包的原因。 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.一件事我仍然不知道为什么 SwiftUI 没有将子上下文应用于saveButton方法内的按钮。

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())

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM