Detect when a user stopped / paused writing in TextField - SwiftUI - swiftui

I want to get notified when the user is done writing text in a TextField in SwiftUI.
The senario in as follows:
-the user enters text in a TextField.
-when the user wrote a string that is longer then 3 characters and paused or finished writing I want the app to get notified so I can work with that String.
tried .onEditingChanged but it will call the action with each change and not only when the user finished editing.

Try below code, this way you can check the real time value entered by user in a textfield.
struct MyFormView: View {
#State var typingText: String = "" {
willSet {
if typingText.count == 3 {
print("User now entering 4th character")
}
}
didSet {
print(typingText)
}
}
var body: some View {
let bindText = Binding<String>(
get:{self.typingText},
set:{self.typingText = $0})
return VStack(alignment: .leading, spacing: 5) {
Text("Name")
TextField("Enter Name", text: bindText)
.keyboardType(.default)
.frame(height: 44)
}.padding(.all, CGFloat(10))
}
}

Related

Strange Behaviour with Strong Password Suggestions in SwiftUI

I have a normal Signup view with one email field and 2 password fields, when clicking on the first password field I get the Strong Pass dialog but it only fills the first field and leaves the second field empty as in this screenshot:
If I click on the second password field both are correctly filled:
This is the code:
import SwiftUI
#main
struct StrongPassApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: SignupView(),
label: {
Text("Signup")
})
}
}
}
struct SignupView: View {
#State var email = ""
#State var pass1 = ""
#State var pass2 = ""
var body: some View {
TextField("Email", text: $email)
.textContentType(.username)
SecureField("Password", text: $pass1)
.textContentType(.newPassword)
SecureField("Repeat password", text: $pass2)
.textContentType(.newPassword)
Button(action: {}, label: {
Text("Signup")
})
}
}
Is there anything missing/wrong with the code or is this just a known buggy behaviour?
Edit
Another issue is if I declined the first dialog by clicking on "Choose My Own Password" then typed in something, when I go to the second password field it deletes what I already types and asks me again to use a strong password.
Confirmed on iOS 14.0 and 14.5 Beta 6
Edit 2
Using .password or .oneTimeCode on both secure text fields makes the password suggestion work perfectly. Tested .password and the password got stored in iCloud so I might go with this for now.

SwiftUI detect delete button pressed

I'm using SwiftUI, but I'm coding my own custom text mask, but I need to delete when a user press the "delete" key. I'm using the onChange method, but it is not detecting when special keys are pressed. Currently I'm using:
TextField(self.placeholder, text: self.$text)
.onChange(of: self.text, perform: { value in
print(value)
})
Is there a way to detect if the delete button is pressed?
Or should I use the UITextField instead of TextField ?
Well you could use a hacky way for this.
First we will hold a count of the current characters the text string has.
Whenever the user presses backspace we check in the onChange handler if the previous character count is higher than the new character count
if that is the case we delete the whole string, or whatever you want to do when the delete button is pressed.
import SwiftUI
struct SquareView: View {
var placeholder = "test"
#State var text = "test"
#State var textLen = 4
var body: some View {
VStack {
TextField(self.placeholder, text: self.$text)
.onChange(of: self.text, perform: { value in
if value.count < textLen {
self.text = "" // << removed the whole text but here you can insert anything you want to do when the delete button is pressed
}
textLen = value.count
})
}
}
}
Keep in mind that this is a hacky way and brings risks. For example if users paste something which is shorter than the current text.
MVVM + Combine way:
class RecoveryPasswordViewModel: ObservableObject {
#Published var email: String = ""
#Published var isValidTextFieldStatus = true
var cancellables = Set<AnyCancellable>()
init() {
setTextFieldToValidWhenUserDelete()
}
func setTextFieldToValidWhenUserDelete() {
$email
.scan("", { [weak self] previousInput, currentInput in
if previousInput.count > currentInput.count {
self?.isValidTextFieldStatus = true
}
return currentInput
})
.sink { _ in }
.store(in: &cancellables)
}
}

Set initial value for a TextField in SwiftUI - compare new and old value

I have seen a lot of examples and tutorial of how to use an empty TextField for collecting new values, but none that shows how to use a TextField to edit a value.
In my use-case, I want the TextField to be prepopulated/prefilled with data from my viewmodel, then as user edits the data, a Save button should be enabled. In my form, I also have a navigationlink that leads to a sub-page where the user can select something from a list, and then be routed back to the form.
It behaves as described as long I use an empty field; the user can type something temporary in the field, navigate to the sub page, and the temp value is still like it was when he left.
struct TextFieldDemo: View {
var model:String // Actual a more complex view model
#State var editedValue:String = ""
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Group{
Text("Some label")
TextField("Placeholder text", text: $editedValue)
}
Divider()
Text("Some navigation link to push in a page where " +
"the user can select something from a list and click back...")
// If the user starts to edit the textfield - follows a navigation link and comes back
// he should be able to continue edit the field where he left of - the text field should
// not have been reset to the original value.
Button(action: {
// Call some save function in the ViewModel
},label: {
Text("SAVE")
}
).disabled(model == editedValue)
}.onAppear(){
// I could have done something like:
// self.editedValue = model
// but it seems like this will fire if the user navigates into the described page and reset
// the TextField to the model value.
}
}
}
struct TextFieldDemo_Previews: PreviewProvider {
static var previews: some View {
TextFieldDemo(model: "The old value")
}
}
To initialize the text field with the value from your model, you need to define your own initializer and use the State(wrappedValue:) initializer for #State vars:
struct TextFieldDemo: View {
var model:String // Actual a more complex view model
#State var editedValue: String
init(model: String) {
self.model = model
self._editedValue = State(wrappedValue: model) // _editedValue is State<String>
}
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Group{
Text("Some label")
TextField("Placeholder text", text: $editedValue)
}
Divider()
Text("Some navigation link to push in a page where " +
"the user can select something from a list and click back...")
// If the user starts to edit the textfield - follows a navigation link and comes back
// he should be able to continue edit the field where he left of - the text field should
// not have been reset to the original value.
Button(action: {
// Call some save function in the ViewModel
},label: {
Text("SAVE")
}
).disabled(model == editedValue)
}.onAppear(){
// I could have done something like:
// self.editedValue = model
// but it seems like this will fire if the user navigates into the described page and reset
// the TextField to the model value.
}
}
}
struct TextFieldDemo_Previews: PreviewProvider {
static var previews: some View {
TextFieldDemo(model: "The old value")
}
}
how about something like this test code. The key is to use the "ObservableObject":
import SwiftUI
class MyModel: ObservableObject {
#Published var model = "model1"
}
struct ContentView: View {
#ObservedObject var myModel = MyModel()
#State var editedValue = ""
var body: some View {
NavigationView {
VStack(alignment: .leading, spacing: 20) {
Group{
Text("Some label")
TextField("Placeholder text", text: Binding<String>(
get: { self.editedValue },
set: {
self.editedValue = $0
self.myModel.model = self.editedValue
})).onAppear(perform: loadData)
}
Divider()
NavigationLink(destination: Text("the nex page")) {
Text("Click Me To Display The next View")
}
// If the user starts to edit the textfield - follows a navigation link and comes back
// he should be able to continue edit the field where he left of - the text field should
// not have been reset to the original value.
Button(action: {
// Call some save function in the ViewModel
self.myModel.model = self.editedValue
},label: {
Text("SAVE")
})
}
}.navigationViewStyle(StackNavigationViewStyle())
}
func loadData() {
self.editedValue = myModel.model
}
}

How to Hide Keyboard in SwiftUI Form Containing Picker?

I have a SwiftUI Form that contains a Picker, a TextField, and a Text:
struct ContentView: View {
var body: some View {
Form {
Section {
Picker(selection: $selection, label: label) {
// Code to populate picker
}.pickerStyle(SegmentedPickerStyle())
HStack {
TextField(title, text: $text)
Text(text)
}
}
}
}
}
The code above results in the following UI:
I am able to easily select the second item in the picker, as shown below:
Below, you can see that I am able to initiate text entry by tapping on the TextField:
In order to dismiss the keyboard when the Picker value is updated, a Binding was added, which can be seen in the following code block:
Picker(selection: Binding(get: {
// Code to get selected segment
}, set: { (index) in
// Code to set selected segment
self.endEditing()
}), label: label) {
// Code to populate picker
}
The call to self.endEditing() is provided in the following method:
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
The following screenshot displays that selecting a different segment of the Picker dismisses the keyboard:
Up to this point, everything works as expected. However, I would like to dismiss the keyboard when tapping anywhere outside of the TextField since I am unable to figure out how to dismiss the keyboard when dragging the Form's containing scroll view.
I attempted to add the following implementation to dismiss the keyboard when tapping on the Form:
Form {
Section {
// Picker
HStack {
// TextField
// Text
}
}
}.onTapGesture {
self.endEditing()
}
Below, the following two screenshot displays that the TextField is able to become the first responder and display the keyboard. The keyboard is then successfully dismissed when tapping outside of the TextField:
However, the keyboard will not dismiss when attempting to select a different segment of the `Picker. In fact, I cannot select a different segment, even after the keyboard has been dismissed. I presume that a different segment cannot be selected because the tap gesture attached to the form is preventing the selection.
The following screenshot shows the result of attempting to select the second value in the Picker while the keyboard is shown and the tap gesture is implemented:
What can I do to allow selections of the Picker's segments while allowing the keyboard to be dismissed when tapping outside of the TextField?
import SwiftUI
struct ContentView: View {
#State private var tipPercentage = 2
let tipPercentages = [10, 15, 20, 25, 0]
#State var text = ""
#State var isEdited = false
var body: some View {
Form {
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
.pickerStyle(SegmentedPickerStyle())
HStack {
TextField("Amount", text: $text, onEditingChanged: { isEdited in
self.isEdited = isEdited
}).keyboardType(.numberPad)
}
}
}.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: isEdited ? .all : .none)
}
}
Form's tap gesture (to finish editing by tap anywhere) is enabled only if text field isEdited == true
Once isEdited == false, your picker works as before.
You could place all of your code in an VStack{ code }, add a Spacer() to it and add the onTap to this VStack. This will allow you to dismiss the keyboard by clicking anywhere on the screen.
See code below:
import SwiftUI
struct ContentView: View {
#State private var text: String = "Test"
var body: some View {
VStack {
HStack {
TextField("Hello World", text: $text)
Spacer()
}
Spacer()
}
.background(Color.red)
.onTapGesture {
self.endEditing()
}
}
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Changing the background color of an HStack or VStack to red simplifies figuring out where the user may click to dismiss.
Copy and paste code for a ready to run example.

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?