简体   繁体   中英

SwiftUI: View does not update when state variable changes

Is there anything wrong with this sample code? The Text view updates with a one character delay. For example, if I type "123" in the textfield, the Text view displays "12".

If I replace contacts with a simple structure and change its givenName property, then the view updates correctly.

Note that the print statement does print correctly (ie, if you type "123" it prints "1" then "12" then "123". So the contacts.givenName does get update as it should.

I have see other questions with a similar title, but this code does not seem to have the problems described in any of the questions that I have seen.

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

Update: I added an id to the Text view and increment it when I update the contact state variable. It's not pretty but it works. Other solutions seem to be too involved fro something that shouldn't be this complicated.

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

The cause of this is using @State for your CNMutableContact .

@State works best with value types -- whenever a new value is assigned to the property, it tells the View to re-render. In your case, though, CNMutableContact is a reference type . So, you're not setting a truly new value, you're modifying an already existing value. In this case, the View only updates when name changes, which then triggers your onChange , so there's no update after the contact changes and you're always left one step behind.

But, you need something like @State because otherwise you can't mutate the contact.

There are a couple of solutions to this. I think the simplest one is to wrap your CNMutableContact in an ObservableObject and call objectWillChange.send() explicitly when you change a property on it. That way, the View will be re-rendered (even though there aren't any @Published properties on it).

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

Another option is moving name to the view model and using Combine to observe the changes. This works without objectWillChange because the sink updates contact on the same run loop as name gets changed, so the @Published property wrapper signals the View to update after the change to contact has been made.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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