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.