Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: View does not update when state variable changes

Tags:

ios

swift

swiftui

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)
       }
    }
like image 249
RawMean Avatar asked Nov 22 '25 08:11

RawMean


1 Answers

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)")
    }
}
like image 187
jnpdx Avatar answered Nov 25 '25 00:11

jnpdx



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!