SwiftUI:当状态变量改变时,视图不会更新。

14

这段示例代码有什么问题吗?Text视图更新有一个字符的延迟。例如,如果在文本字段中输入 "123",Text视图会显示 "12"。

如果我用一个简单的结构体替换 contacts 并改变它的 givenName 属性,那么视图就会正确更新。

请注意,print语句确实打印正确(即,如果您键入 "123",它会先打印 "1",然后是 "12",最后是 "123")。因此,contacts.givenName 的确被更新了。

我看到其他问题的标题类似,但是这段代码似乎没有任何已知问题。

import SwiftUI
import Contacts

struct ContentView: View {
    @State var name: String = ""
    @State var contact = CNMutableContact()


    var body: some View {
         TextField("name", text: $name)
                .onChange(of: name) { newValue in
                    contact.givenName = newValue
                    print("contact.givenName = \(contact.givenName)")
                }
         Text("contact.givenName = \(contact.givenName)")
    }
}

更新: 我给 Text 视图添加了一个 id 属性,并在更新 contact 状态变量时对其进行了递增。虽然不太美观,但它可以工作。其他解决方案似乎对于本不应如此复杂的事情而言过于繁琐。

   struct ContentView: View {
    @State var name: String = ""
    @State var contact = CNMutableContact()
    @State var viewID = 0   // change this to foce the view to update
    
    
    var body: some View {
        TextField("name", text: $name)
            .padding()
            .onChange(of: name) { newValue in
                contact.givenName = newValue
                print("contact.givenName = \(contact.givenName)")
                viewID += 1 // force the Text view to update
            }
        Text("contact.givenName = \(contact.givenName)").id(viewID)
       }
    }

2
一个状态属性应该是一个结构体,但CNMutableContact是一个类。当你改变一个结构体时,你会得到一个新的值,SwiftUI会对此做出反应,但对于一个类来说,当前实例会被更新。 - Joakim Danielson
1个回答

14
这是因为您在CNMutableContact使用了@State
对于值类型,@State的效果最好--每当将新值分配给属性时,它会告诉View重新呈现。但在您的情况下,CNMutableContact是一个引用类型。所以,您没有设置真正的新值,而是修改了已经存在的值。在这种情况下,name更改后才触发您的onChange,所以在contact更改后没有更新,并且您总是落后一步。
但是,您需要@State那样的东西,否则无法改变联系人信息。
有几种解决方法。我认为最简单的方法是将您的CNMutableContact包装在一个ObservableObject中,并在更改其属性时显式调用objectWillChange.send()。这样,View将重新呈现(即使上面没有任何@Published属性)。
class ContactViewModel : ObservableObject {
    var contact = CNMutableContact()
    
    func changeGivenName(_ newValue : String) {
        contact.givenName = newValue
        self.objectWillChange.send()
    }
}

struct ContentView: View {
    @State var name: String = ""
    @StateObject private var contactVM = ContactViewModel()

    var body: some View {
         TextField("name", text: $name)
                .onChange(of: name) { newValue in
                    contactVM.changeGivenName(newValue)
                    print("contact.givenName = \(contactVM.contact.givenName)")
                }
        Text("contact.givenName = \(contactVM.contact.givenName)")
    }
}

另一个选项是将 name 移动到 ViewModel,并使用 Combine 观察变化。这种方式不需要 objectWillChange,因为 sink 在与更改 name 相同的 run loop 上更新 contact,所以 @Published 属性包装器会在对 contact 进行更改后向 View 发出更新信号。

import Combine
import SwiftUI
import Contacts

class ContactViewModel : ObservableObject {
    @Published var name: String = ""
    var contact = CNMutableContact()
    
    private var cancellable : AnyCancellable?
    
    init() {
        cancellable = $name.sink {
            self.contact.givenName = $0
        }
    }
}

struct ContentView: View {
    @StateObject private var contactVM = ContactViewModel()

    var body: some View {
        TextField("name", text: $contactVM.name)
        Text("contact.givenName = \(contactVM.contact.givenName)")
    }
}

1
感谢您提供详细的答案。我已经更新了我的问题,并提供了一个可行的解决方案,但它并不完美。您认为呢? - RawMean
7
我认为尽管它可行,但在这种情况下意图不明确,并且鼓励具有意外行为的惯例(例如使用@State用于引用类型),而我的两个解决方案都非常清晰地说明了实际发生的事情。关于您的评论“其他解决方案似乎对于本不应该如此复杂的问题过于复杂”,我想说,特别是我的第二个解决方案实际上比您最初的实现更简单。 - jnpdx
2
愚蠢的自动更正 -- implantation = implementation - jnpdx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接