SwiftUI: Replace onEditingChanged in TextField with FocusState - swiftui

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

Related

SwiftUI: How to have a ForEach of editable TextFields?

I'm trying to have a component that basically starts with a single TextField for editing your home phone number, and then you can hit the add button to add different types of phone numbers to your account. for right now I just want to allow the user to edit the phone number, but in the future I'll probably make it so that there are actually two TextFields for each PhoneNumberListItem. 1 field for the editable name and 1 field for the editable phone itself. I'm coming from Android/Compose which is maybe where my line of thinking is stuck. appreciate any pointers.
func ListOfMyPhoneNumbers() -> some View {
#State var listOfMyPhones = [PhoneNumberListItem(name: "home", phone: "123")]
return VStack {
ForEach(listOfMyPhones) { i in
TextField(i.name, text: i.$phone).textFieldStyle(.roundedBorder)
}
Button("Add Phone") {
listOfMyPhones.append(PhoneNumberListItem(name: "other", phone: ""))
}.buttonStyle(.borderless)
Spacer()
}
.padding()
}
struct PhoneNumberListItem: Identifiable {
let id = UUID()
let name: String
#State var phone: String
}
Do not use #State var phone: String in your PhoneNumberListItem.
#State is only for use in a View. Just use a var.
Also use a struct for your view. With the following example code you will be able to edit the TextField in a ForEach, like this:
struct PhoneNumberListItem: Identifiable {
let id = UUID()
var name: String // <--- here
var phone: String // <--- here
}
struct ContentView: View {
var body: some View {
PhoneView() // <--- here
}
}
struct PhoneView: View { // <--- here
#State var listOfMyPhones = [PhoneNumberListItem(name: "home", phone: "123")]
var body: some View {
VStack {
// --- here
ForEach($listOfMyPhones) { $item in
TextField("phone", text: $item.phone)
TextField("name", text: $item.name)
}.textFieldStyle(.roundedBorder)
Button("Add Phone") {
listOfMyPhones.append(PhoneNumberListItem(name: "other", phone: ""))
}.buttonStyle(.borderless)
Spacer()
}
.padding()
}
}
As you progress with learning SwiftUI, you will want to use ObservableObject class to manage your data. Have a look at this link, it gives you some good examples of how to manage data in your app :
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

SwiftUI view get views in the body or define view as property

I am trying to get the view inside the body for observing purpose, but looking for different ways the view inside the body can be accessed.
struct ContentView: View {
#State var userNameText: String
var body: some View {
startObservingInput()
return TextField("hello", text: $userNameText)
}
func startObservingInput() {
// How do we get TextField instance here.
// Option 1 - Pass as parmeter here.
// Option 2 - Is there a way to get view from body ex: self.body.textFieldView
// Option 3 - can create as a property in CotentView but the text binding refers to self which will not be allowed before its initalized so that will fail to compile
//var textField = TextField("hello", text: $userNameText)
}
}
Option 1 is simple, where we pass the TextField view.
Option 2 is something I am looking for, if we can get any view inside the hierarchy. In this case Text Field.
Option 3, Tried to create a property but I get the following error.
ex:
struct ContentView: View {
#State var userNameText: String
var textField = TextField("hello", text: $userNameText)
......
}
Cannot use instance member '$userNameText' within property initializer; property initializers run before 'self' is available
SwiftUI is different from what you're probably used to. Unlike UIKit, you don't "store" views in properties. There's no delegates either.
Instead, you directly pass in a property — userNameText — that will be linked to the text field's text. Since this updates itself automatically, you can use the onChange modifier to observe changes.
struct ContentView: View {
#State var userNameText: String
var body: some View {
TextField("hello", text: $userNameText)
.onChange(of: userNameText) { newValue in
print("Text changed to: \(newValue)")
}
}
}
Here is what I did, look at it:
//
// ViewProp.swift
// SwiftDemo1
//
// Created by CreoleMacbookPro on 12/19/22.
//
import SwiftUI
struct ViewProp: View {
#State var userNameText: String = " "
var body: some View {
let textField: TextField<Text> = TextField("hello", text: $userNameText)
let simpleText: Text = Text("Hello, World!")
let _ = print(type(of: textField))
startObservingInput(textField: textField)
Button {
userNameText = "Jatin Bhuva"
} label: {
Text("Press Me..")
}
// textField
}
func startObservingInput(textField: TextField<Text>) -> some View {
textField
// How do we get TextField instance here.
// Option 1 - Pass as parmeter here.
// Option 2 - Is there a way to get view from body ex: self.body.textFieldView
// Option 3 - can create as a property in CotentView but the text binding refers to self which will not be allowed before its initalized so that will fail to compile
//var textField = TextField("hello", text: $userNameText)
}
}
struct ViewProp_Previews: PreviewProvider {
static var previews: some View {
ViewProp()
}
}

SwiftUI List with #FocusState and focus change handling

I want to use a List, #FocusState to track focus, and .onChanged(of: focus) to ensure the currently focused field is visible with ScrollViewReader. The problem is: when everything is setup together the List rebuilds constantly during scrolling making the scrolling not as smooth as it needs to be.
I found out that the List rebuilds on scrolling when I attach .onChanged(of: focus). The issue is gone if I replace List with ScrollView, but I like appearance of List, I need sections support, and I need editing capabilities (e.g. delete, move items), so I need to stick to List view.
I used Self._printChanges() in order to see what makes the body to rebuild itself when scrolling and the output was like:
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
...
And nothing was printed from the closure attached to .onChanged(of: focus). Below is the simplified example, the smoothness of scrolling is not a problem in this example, however, once the List content is more or less complex the smooth scrolling goes away and this is really due to .onChanged(of: focus) :(
Question: Are there any chances to listen for focus changes and not provoke the List to rebuild itself on scrolling?
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#FocusState var focus: Field?
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
.focused($focus, equals: .fieldId($0))
}
}
.onChange(of: focus) { _ in
print("Not printed unless focused manually")
}
}
}
if you add printChanges to the beginning of the body, you can monitor the views and see that they are being rendered by SwiftUI (all of them on each focus lost and focus gained)
...
var body: some View {
let _ = Self._printChanges() // <<< ADD THIS TO SEE RE-RENDER
...
so after allot of testing, it seams that the problem is with .onChange, once you add it SwiftUI will redraw all the Textfields,
the only BYPASS i found is to keep using the deprecated API as it works perfectly, and renders only the two textfields (the one that lost focus, and the one that gained the focus),
so the code should look this:
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
// #FocusState var focus: Field? /// NO NEED
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
// .focused($focus, equals: .fieldId($0)) /// NO NEED
}
}
// .onChange(of: focus) { _ in /// NO NEED
// print("Not printed unless focused manually") /// NO NEED
// } /// NO NEED
.focusable(true, onFocusChange: { focusNewValue in
print("Only textfileds that lost/gained focus will print this")
})
}
}
I recommend to consider separation of list row content into standalone view and use something like focus "selection" approach. Having FocusState internal of each row prevents parent view from unneeded updates (something like pre-"set up" I assume).
Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#State private var inFocus: Field?
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100, id: \.self) {
ExtractedView(i: $0, inFocus: $inFocus)
}
}
.onChange(of: inFocus) { _ in
print("Not printed unless focused manually")
}
}
struct ExtractedView: View {
let i: Int
#Binding var inFocus: Field?
#State private var text: String = ""
#FocusState private var focus: Bool // << internal !!
var body: some View {
TextField("Enter the text for \(i)", text: $text)
.focused($focus)
.id(Field.fieldId(i))
.onChange(of: focus) { _ in
inFocus = .fieldId(i) // << report selection outside
}
}
}
}

SwiftUI Picker unable to transfer data from Picker to Binding variable

Hopefully you can see what I'm trying to achieve from the code below but simply put, I'm trying to update .selectedTown which is binded to my Picker. The row tapped on will bind to .selectedTown which will then update the Text 'Your selected town is: [.selectedTown]'
However, the selected row is not binding and the text remains 'Your selected town is: '
struct ContentView: View {
struct Town: Identifiable {
let name: String
let id = UUID()
}
private var towns = [
Town(name: "Bristol"),
Town(name: "Oxford"),
Town(name: "Portsmouth"),
Town(name: "Newport"),
Town(name: "Glasgow"),
]
#State private var selectedTown: String = ""
var body: some View {
NavigationView {
VStack {
Form {
Section {
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)")
}
}
.pickerStyle(.inline)
.labelsHidden()
} header: {
Text("Random Towns")
}
}
Text("Your selected town is: \(selectedTown)")
.padding()
}
.navigationTitle("Random")
}
}
}
Hopefully this is just a small fix but I've tried for what seems a day to find a solutino and am now stuck. Any help would be gratefully received,
Simon
The types don't match. your array is a towns: [Town] and your selectedTown: String
Option 1 is to change the variable
#State private var selectedTown: Town = Town(name: "Sample")
Option 2 is to add a tag
Text("\($0.name)").tag($0.name)
Option 3 is change the variable and the tag
#State private var selectedTown: Town? = nil
Text("\($0.name)").tag($0 as? Town)
The "best" option depends on what you use selectedTown for.
The type of selection should be same as picked item or use tag, like below
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)").tag($0.name) // << here !!
}
}
Tested with Xcode 13.2 / iOS 15.2

number input with SwiftUI

The new SwiftUI is fantastic to play with... I'm trying to use Forms instead of Eureka. A couple of questions:
What is the best way to let the user enter a number? I used to do that with a UIPickerView, see image .
With SwiftUI I only found Textfield, as in the following code:
import SwiftUI
struct SettingsView : View {
#State var email = ""
#State var amount = ""
var body: some View {
NavigationView {
Form {
Section(header: Text("Email")) {
TextField("Your email", text: $email)
.textFieldStyle(.roundedBorder)
}
Section(header: Text("Amount")) {
TextField("Amount", text: $amount)
.textFieldStyle(.roundedBorder)
}
.navigationBarTitle("Settings")
}
}
}
}
When you click in the field, the ABC keyboard comes up. The user can select '123' to get the number keyboard. But I would like to see a number pad instead.
Also, the keyboard blocks the view (if you have more fields); the view doesn't scroll up to make room for the keyboard.
Is it possible to get rid of the keyboard when the user clicks outside a TextField?
And is there a way to 'validate the entries'? For instance, the amount should be between 10 and 1.000?