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

Raw*_*ean 15 ios swift swiftui

这个示例代码有什么问题吗?视图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)")
    }
}
Run Code Online (Sandbox Code Playgroud)

更新: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)
       }
    }
Run Code Online (Sandbox Code Playgroud)

jn_*_*pdx 14

其原因是用于@State您的CNMutableContact.

@State最适合值类型——每当将新值分配给属性时,它都会告诉View重新渲染。不过,就您而言,CNMutableContact引用类型。因此,您并不是在设置真正的新值,而是在修改已经存在的值。在这种情况下,View唯一会在发生更改时更新name,然后触发您的onChange,因此更改后没有更新contact,您总是落后一步。

但是,您需要类似的东西@State,因为否则您无法改变联系人。

对此有几个解决方案。我认为最简单的方法是将您的包装CNMutableContact在 an 中,并在更改其属性时显式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)")
    }
}
Run Code Online (Sandbox Code Playgroud)

另一种选择是转移name到视图模型并使用组合来观察变化。这是可行的objectWillChange,因为同一运行循环上的sink更新发生更改,因此属性包装器会在更改完成后发出更新信号。contactname@PublishedViewcontact

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)")
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我想说,虽然它*有效*,但在这种情况下,意图并不明确,并且鼓励了意外行为的实践(例如使用“@State”作为引用类型),而我的两个解决方案对于实际内容都有很好的语义清晰度。正在发生。根据您的评论“其他解决方案似乎对于不应该这么复杂的事情来说太过复杂”,我想说我的第二个解决方案实际上比您最初的植入简单。 (7认同)
  • 愚蠢的自动更正——植入=实施 (2认同)

小智 1

Text()您显示的字符比原始字符少一个的原因name

onChange { }当您更新内部闭包时contact.givenName,它不会刷新/重新加载您的整个视图。因为您实际上并没有更新整个contact@State 变量,而是只是更新了@State 变量givenName的一个属性contact

根据变量的规则@State,当您更新整个@State变量时,视图才会重新加载。

基于此我将分享2个更简单的解决方案:

contact解决方案1:完全更新@State变量对象

import SwiftUI
import Contacts

struct ContentView: View {
    @State var name: String = ""
    @State var contact = CNMutableContact()
    
    var body: some View {
        TextField("name", text: $name)
            .padding()
            .onChange(of: name) { oldValue, newValue in
                contact = .init() // Updating the @State variable contact completely
                contact.givenName = newValue
                print("contact.givenName = \(contact.givenName)")
            }
        Text("contact.givenName = \(contact.givenName)")
    }
}
Run Code Online (Sandbox Code Playgroud)

效果很好。但我个人不喜欢这个解决方案,因为它contact每次都会创建新的引用。所以我认为它效率不高。

解决方案 2:使用另一个 @State varcontactName来跟踪联系人姓名

import SwiftUI
import Contacts

struct ContentView: View {
    @State var name: String = ""
    @State var contactName: String = ""
    
    var contact = CNMutableContact() // We don't need @State for this
    
    var body: some View {
        TextField("name", text: $name)
            .padding()
            .onChange(of: name) { oldValue, newValue in
                contact.givenName = newValue
                contactName = newValue
                print("contact.givenName = \(contact.givenName)")
            }
        Text("contact.givenName = \(contactName)")
    }
}
Run Code Online (Sandbox Code Playgroud)

别担心,您的意愿contact.givenName会相应更新。