I have a very basic view that only shows a TextField:
View
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
TextField("Enter a string...", text: $viewModel.string)
}
}
The TextField's text is bound to a string property on the view model:
ViewModel
class ViewModel: ObservableObject {
#Published var string: String = "" {
didSet {
print("didSet string:", string)
}
}
}
I added a didSet property observer to perform a custom action whenever the string changes. For this simple example, I only print a string on the console.
Observation
When I run this code and enter the string "123" into the text field, this is the output I get:
didSet string: 1
didSet string: 1
didSet string: 12
didSet string: 12
didSet string: 123
didSet string: 123
Question:
Why?
Why is the didSet closure called twice for each character I type?
(I would expect it to be called once for each character.)
Is there anything wrong with the code or is this expected behavior somehow? 🤔
I’m seeing this issue on Xcode 14.2 RC and iOS 16.2 RC, but weirdly what fixes it is adding a .textFieldStyle(.plain) or .textFieldStyle(.roundedBorder).
I’m really not sure why having no textFieldStyle would affect this, but the binding calls set:{} twice when I have no textFieldStyle set, and as soon as I add one of those, it behaves normally and only calls set:{} once at a time.
I hope this helps someone!
let binding = Binding<String>(get: {
textvariable
}, set: {
viewModel.setText(query: $0) //add event inside setText
// do whatever you want here
})
Related
I'm curious, has anyone seen this before, or do they know how to solve it? I have a situation where trying to edit the middle contents of a textfield that's in a NavigationStack makes the text cursor move to the end on every keystroke. I suspect it has something to do with SwiftUI's management of views and state, but I am not spotting anything unusual that I might be doing. It's all standard State/Binding stuff. Can anyone point me to a flaw or if it's a SwiftUI bug, can they suggest a work-around?
Here's some minimal sample code to demonstrate (try editing the text to say "Hello World" by inserting the missing "el" to see what I mean):
import SwiftUI
private struct CursorResetting: View {
struct Record: Identifiable {
var string = ""
var id = UUID()
}
#State private var records = [
Record(string: "Hlo World"),
]
#State private var singleRecord = Record(string: "Hlo World")
var body: some View {
NavigationStack {
List($records) { $record in
NavigationLink(record.string) {
TextField("Value", text: $record.string)
// TextField("Value", text: $singleRecord.string)
}
}
}
}
}
I do know that if I use a single Record as the source of my binding rather than an element in an Array, then the problem goes away. (Try swapping in the commented out singleRecord version of the TextField)
List($records) { $record in
TextField("Value", text: $record.string)
According to the official Apple developer documentation, the init(_:text:onEditingChanged:onCommit:) for a TextField is deprecated now: https://developer.apple.com/documentation/swiftui/textfield/init(_:text:oneditingchanged:oncommit:)-6lnin
So far I still can use:
TextField("placeholder", text: $text, onEditingChanged: { _ in print("focus changed") })
But I don't understand how to replace the onEditingChanged with the new FocusState as suggested in the developer documentation. Any hints how to do this?
Yes, you can do it by observing changes to the focus state in an onChange block. The block will be passed the new focus state, and you can use a capture list to capture the old state. For example, if you wanted to know when the user has finished editing a field so that you could perform validation, you could do this:
struct URLForm: View {
enum Field {
case name, url
}
#FocusState private var focus: Field?
#State private var name: String
#State private var urlPath: String
var body: some View {
TextField("Name", text: $name)
.focused($focus, equals: .name)
.onChange(of: focus) { [oldFocus = focus] newFocus in
guard oldFocus == .name, newFocus != .name else { return }
// user has finished editing this field
}
TextField("URL", text: $urlPath)
.focused($focus, equals: .url)
}
}
If I pass a binding to the TextField then whenever you edit the centre of the text the cursor will jump to the end of the line after each character insert.
This only occurs on an actual device (iPadOS). The simulator does not display this behaviour.
My work around is to create a state variable that I set onAppear with the value from the binding and copy onEditingChanged. I then pass this state variable to the TextField instead of the binding variable directly. This breaks the one source of truth.
Does anyone have a better solution.
Swift 5
iOS 14.7
XCODE 13 Beta Aug 10
struct infoSheetView: View {
#Binding var cameraURL: String
#State var tmpCameraURL: String = ""
var body: some View {
VStack {
Form{
Section(header: Text("Camera").font(.title)) {
TextField("Camera URL", text: $tmpCameraURL, onEditingChanged: {_ in
cameraURL = tmpCameraURL
}).autocapitalization(.none).disableAutocorrection(true)
}
}.frame(width:400,height:390)
.onAppear() {
tmpCameraURL = cameraURL
}
}
}
In UIKit, I would have code like this:
#IBOutlet weak var itemNameField: UITextField!
#IBAction func itemNameFieldDone(_ sender: UITextField) {
thisItem.myName = sender.text ?? thisItem.myName
thisItem.modified()
}
In the model object:
func modified() {
dateModified = Date()
let cds = FoodyDataStack.thisDataStack
uuidUser = cds.uuidUser
uuidFamily = cds.uuidFamily
}
In SwiftUI:
TextField($thisItem.myName)
Declarative, nice and short. SwiftUI takes care of updating the myName property as the user types in the TextField, but how do I get the dateModified property to update at the same time?
Use the TextField initializer that includes onEditingChanged and include whatever updating code you need in the closure.
TextField($thisCategory.myName, placeholder: nil, onEditingChanged: { (changed) in
self.thisCategory.modified()
self.dataStack.didChange.send(self.dataStack)
}).textFieldStyle(.roundedBorder)
iOS 14
There is a new modifier called onChange to detect changes of any state:
struct ContentView: View {
#State var text: String = ""
var body: some View {
TextField("Title", text: $text)
.onChange(of: text, perform: { value in
print(text)
})
}
}
You can add didSet observer to myName property in your item type declaration and then call modified from there:
var myName: String = "" {
didSet {
self.modified()
}
}
I'm having a problem trying to get textfields working in SwiftUI.
I get Fatal error: Accessing State> outside View.body whenever I try to run the following code.
Anyone have a suggestion?
struct SearchRoot : View {
#State var text: String = ""
var body: some View {
HStack {
TextField($text,
placeholder: Text("type something here..."))
Button(action: {
// Closure will be called once user taps your button
print(self.$text)
}) {
Text("SEND")
}
}
}
}
I'm running Xcode Version 11.0 beta (11M336w) on macOS 10.15 Beta (19A471t)
Edit: Simplified code, still getting the same error.
struct SearchRoot : View {
#State var text: String = ""
var body: some View {
TextField($text,
placeholder: Text("type something here..."))
}
}
The compiler emits an error if the $ operator is used outside body, in a View.
The button initializer is defined as:
init(action: #escaping () -> Void, #ViewBuilder label: () -> Label)
You're using $ in an escaping closure, in the first snippet of code.
That means the action may outlive (escape) the body, hence the error.
The second snippet compiles and works fine for me.
Eureka! SwiftUI wants a single source of truth.
What I neglected to include in my original code snippets is that this struct is within a tabbed application.
To fix this I needed to define the #State var text: String = "" in the struct that creates the top level TabbedView, then use $Binding in the SearchRoot.
I'm not sure if this is works as designed or just a beta 1 issue, but it's the way it works for now.
struct ContentView : View {
#State private var selection = 0
#State private var text: String = "searching ex"
var body: some View {
TabbedView(selection: $selection){
ShoppingListRoot().body.tabItemLabel(Text("Cart")).tag(0)
SearchRoot(text: $text).body.tabItemLabel(Text("Search")).tag(1)
StoreRoot().body.tabItemLabel(Text("Store")).tag(2)
BudgetRoot().body
.tabItemLabel(Text("Budget"))
.tag(3)
SettingsRoot().body
.tabItemLabel(Text("Settings"))
.tag(4)
}
}
}