SwiftUI @State 变量未更新

Posted

技术标签:

【中文标题】SwiftUI @State 变量未更新【英文标题】:SwiftUI @State variables aren't updated 【发布时间】:2021-05-01 18:13:24 【问题描述】:

我的代码基本上和this question 一样。我遇到的问题是,当tapGesture 事件发生时,工作表显示(调用sheet 代码)但调试显示showUserEditor 为假(在这种情况下,工作表如何显示......?)并且 selectedUserId 仍然是 nil (因此在打开它时会崩溃......)

观点:

struct UsersView: View 
    @Environment(\.managedObjectContext)
    private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
        animation: .default)
    private var users: FetchedResults<User>
    
    @State private var selectedUserId : NSManagedObjectID? = nil
    @State private var showUserEditor = false
    
    var body: some View 
        NavigationView 
            List 
                ForEach(users)  user in
                    UserRowView(user: user)
                        .onTapGesture 
                            self.selectedUserId = user.objectID
                            self.showUserEditor = true
                        
                    
                
            .sheet(isPresented: $showUserEditor) 
                UserEditorView(userId: self.selectedUserId!)
            
        

如果您愿意,我可以发布编辑器和行,但它们似乎与问题无关,因为魔术应该发生在视图中。

【问题讨论】:

至少设置selectedUserId之前设置showUserEditor为true,换行。 或者使用 sheet init 和 item @vadian 我这样做了......这是显而易见的方式,我改变了它并没有回来检查。我将编辑问题以反映这一点。无论如何,据我了解,SwiftUI 重绘周期无关紧要。另外,请注意,当我调试sheet 代码时,即使在打印变量时 showUserEditor 也是错误的,所以我在那里感到困惑......也许我不清楚调试@State 变量时应该寻找什么。 @loremipsum - 找到你的意思:developer.apple.com/documentation/swiftui/view/… 我会试一试。 有一个.sheet init,您可以在其中将项目作为参数传递。当项目的@State 不是nil 时,它会显示一个工作表developer.apple.com/documentation/swiftui/text/… 【参考方案1】:

所以,我仍然没有弄清楚为什么问题中发布的代码不起作用,使用来自 @loremipsum 的指针我通过使用另一个 .sheet() 方法得到了一个工作代码,该方法采用可选的 Item 和不是布尔标志。代码现在看起来像这样并且可以工作,但如果有人能解释为什么发布的代码不起作用,我会很感激。

struct UsersView: View 
    @Environment(\.managedObjectContext)
    private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
        animation: .default)
    private var users: FetchedResults<User>
    
    @State private var selectedUser : User? = nil
    
    var body: some View 
        NavigationView 
            List 
                ForEach(users)  user in
                    UserRowView(user: user)
                        .onTapGesture 
                            self.selectedUser = user
                        
                    .onDelete(perform: deleteItems)
                
        .sheet(item: $selectedUser, onDismiss: nil)  user in
            UserEditorView(user: user)
        
    

【讨论】:

我可以输入答案。并且更好地解释它与在创建工作表时不可变的结构有关已经设置的变量并且由于您正在显示工作表而不会重新加载正文。【参考方案2】:

struct == immutable 和 SwiftUI 决定 struct 何时初始化并重新加载

不建议使用依赖于 SwiftUI 在特定时间更新非包装变量的代码。您无法控制此过程。

要使您的第一个设置工作,您需要对变量使用 SwiftUI 包装器

.sheet(isPresented: $showUserEditor) 
            //struct == immutable SwiftUI wrappers load the entire struct when there are changes
            //With your original setup this variable gets created/set when the body is loaded so the orginal value of nil is what is seen in the next View
            UserEditorView1(userId: $selectedUserId)
        

struct UserEditorView1: View 
    //This is what you orginal View likely looks like it won't work because of the struct being immutable and SwiftUI controlling when the struct is reloaded

    //let userId: NSManagedObjectID? <---- Depends on specific reload steps

    //To make it work you would use a SwiftUI wrapper so the variable gets updated when SwiftUI descides to update it which is invisible to the user
    @Binding var userId: NSManagedObjectID?
    //This setup though now requres you to go fetch the object somehow and put it into the View so you can edit it.
    //It is unnecessary though because SwiftUI provides the .sheet init with item where the item that is set gets passed directly vs waiting for the SwiftUi update no optionals
    var body: some View 
        Text(userId?.description ?? "nil userId")
    

您的答案代码不起作用,因为您的参数是可选的并且Binding 不喜欢可选参数

struct UserEditorView2: View 
    //This is the setup that you posted in the Answer code and it doesn't work becaue of the ? Bindings do not like nil. You have to create wrappers to compensate for this
    //But unecessary because all CoreData objects are ObservableObjects so you dont need Binding here the Binding is built-in the object for editing the variables
    @Binding var user: User?
    
    var body: some View 
        TextField("nickname", text: $user.nickname)
    

现在可以使用易于编辑的 CoreData 对象来处理代码

struct UsersView: View 
    @Environment(\.managedObjectContext)
    private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
        animation: .default)
    private var users: FetchedResults<User>
    //Your list view would use the CoreData object to trigger a sheet when the new value is available. When nil there will not be a sheet available for showing
    @State private var selectedUser : User? = nil
    
    var body: some View 
        NavigationView 
            List 
                ForEach(users)  user in
                    UserRowView(user: user)
                        .onTapGesture 
                            self.selectedUser = user
                        
                
            
        .sheet(item: $selectedUser, onDismiss: nil)  user in //This gives you a non-optional user so you don't have to compensate for nil in the next View
            UserEditorView3(user: user)
        
    

那么工作表中的视图将如下所示

struct UserEditorView3: View 
    //I mentioned the ObservedObject in my comment 
    @ObservedObject var user: User
    var body: some View 
        //If your nickname is a String? you have to compensate for that optional but it is much simpler to do it from here
        TextField("nickname", text: $user.nickname.bound)
    



//This comes from another very popular SO question (couldn't find it to quote it) that I could not find and is necessary when CoreData does not let you define a variable as non-optional and you want to use Binding for editing
extension Optional where Wrapped == String 
    var _bound: String? 
        get 
            return self
        
        set 
            self = newValue
        
    
    public var bound: String 
        get 
            //This just give you an empty String when the variable is nil
            return _bound ?? ""
        
        set 
            _bound = newValue.isEmpty ? nil : newValue
        
    

【讨论】:

以上是关于SwiftUI @State 变量未更新的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI - @State 属性未更新

在 SwiftUI 中使用 @State 属性包装器未更新视图

更新@State后未调用SwiftUI UIViewControllerRepresentable.updateUIViewController

SwiftUI @State 数组未更新在 init() 期间附加的值

Swift UI - 如何从外部访问 @State 变量?

SwiftUI DatePicker 并不总是更新 State<Date> 变量