SwiftUI: How to only run code when the user stops typing in a TextField?
The possible approach is to use debounce
from Combine framework. To use that it is better to create separated view model with published property for search text.
Here is a demo. Prepared & tested with Xcode 12.4 / iOS 14.4.
import Combine
class SearchBarViewModel: ObservableObject {
@Published var text: String = ""
}
struct SearchBarView: View {
@StateObject private var vm = SearchBarViewModel()
var body: some View {
ZStack {
TextField("Search", text: $vm.text)
.onReceive(
vm.$text
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
) {
guard !$0.isEmpty else { return }
print(">> searching for: \($0)")
}
}
}
}
How to create a function when the user stops typing in swift
I would use something like:
func textFieldDidChangeSelection(_ textField: UITextField) {
checkmarkImageView.isHidden = True
let timer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
}
And:
@objc func fireTimer() {
checkmarkImageView.isHidden = False
}
Instead of creating the imageView each time, set it up as a IBOutlet that's initially hidden.
Detect when user stopped / paused typing in Swift
UPDATE 2016/09/01:
We can use NSTimers
or (since swift 2.0) NSObject's performSelector
and friends.
Aproach 1 : performSelector
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
NSObject.cancelPreviousPerformRequests(
withTarget: self,
selector: #selector(ViewController.getHintsFromTextField),
object: textField)
self.perform(
#selector(ViewController.getHintsFromTextField),
with: textField,
afterDelay: 0.5)
return true
}
func getHintsFromTextField(textField: UITextField) {
print("Hints for textField: \(textField)")
}
Approach 2: NSTimer
var timer: NSTimer? = nil
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
timer?.invalidate()
timer = Timer.scheduledTimer(
timeInterval: 0.5,
target: self,
selector: #selector(ViewController.getHints),
userInfo: ["textField": textField],
repeats: false)
return true
}
func getHints(timer: Timer) {
var userInfo = timer.userInfo as! [String: UITextField]
print("Hints for textField: \(userInfo["textField"])")
}
Note I am passing the textField
to delayed functions. It is not always required but it could make your life easier when textField
is not easy to access or when dealing with various text fields.
How NSTimer
approach is different from performSelector
?
When you call performSelector
the target is retained (in swift the target is always self
) but when you use NSTimer
the target is NOT retained. This means, if you use NSTimer
s, you have to make sure target (in this case self
) is alive by the time the timer fires. Otherwise the a crash will occur.
(BTW: performSelector
uses NSTimer
internally )
If you are interested in GCD timers this gist a good place start:
maicki/TimerWithGCD.md
App freeze when tap on textfield in conditional section
So with Xcode 13.2.1 on Intel and iOS 15.2 I can only get this example code to lock-up in the simulator and on a real device if I type a number of more than three digits in the 'Amount EUR' and then attempt to 'Set the Max to 999' i.e. a three digit number.
In previous versions of SwiftUI the only input binding TextField would work with was String. It seems likely that although the current version accepts binding to numerics the way it is doing this is to map, and the underlying mechanism remains that of the original String based TextField (typing in the TextField with an attached keyboard still unfortunately renders non-numeric characters regardless of keyboardType as it did the last time I experimented https://gist.github.com/shufflingB/23daafa5253c3355cdf18934599cd54c)
For whatever reason this new(ish) mapping process appears to be getting confused when more than three digits are entered and the TextField is then by default automatically adding a number grouping character.
Two work-arounds seem to address the problem:
Push the change to
amount
triggered bySet the Max ...
to a later state update cycle usingDispatchQueue.main.asyncAfter
Give an option to remove the troublesome grouping character from the TextField.
struct ContentView: View {
@Environment(\.presentationMode) var presentationMode
@State private var displaySection = true
@State private var amount: Double?
@FocusState private var isFocused: Bool
var body: some View {
NavigationView {
Form {
if displaySection {
Section {
VStack {
HStack {
Text("Amount EUR")
Spacer()
TextField("Type amount", value: $amount, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.focused($isFocused)
/* Alternative to Dispatch, remove number grouping
TextField("Type amount", value: $amount, format: .number.grouping(.never))
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.focused($isFocused)
*/
}
Text("Set MAX (999)")
.frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture {
isFocused = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <- Push change to subsequent update cycle
amount = 999
}
}
}
}
}
}
}
}
}
How to detect when a TextField loses the focus in SwiftUI for iOS?
You can use the initializer init(_:text:onEditingChanged:onCommit:)
present in textfield. There you will be getting an action triggered when you begin editing and end editing. You can find out the minimal example below.
import SwiftUI
struct ContentView: View {
@State private var greeting: String = "Hello world!"
var body: some View {
TextField("Welcome", text: $greeting, onEditingChanged: { (editingChanged) in
if editingChanged {
print("TextField focused")
} else {
print("TextField focus removed")
}
})
}
}
Hope this helps.
Editing a TextField in preview
Since the comment was a legit answer I just copy paste it here. Glad it worked for you as well
There has been an Xcode bug in the past were live preview TextField input was broken. Seems to be back :(. I just experienced the same on 13.2.1. running the preview on iOS 15.2. switching to an iOS 14.3 simulator seems to fix it in my case. (wait for the Preparing iPhone Simulator for Previews to finish). Related to
How to detect live changes on TextField in SwiftUI?
You can create a binding with a custom closure, like this:
struct ContentView: View {
@State var location: String = ""
var body: some View {
let binding = Binding<String>(get: {
self.location
}, set: {
self.location = $0
// do whatever you want here
})
return VStack {
Text("Current location: \(location)")
TextField("Search Location", text: binding)
}
}
}
Related Topics
How to Add Openssl to a Swift Project
Pattern Matching in a Swift for Loop
Split String by Components and Keep Components in Place
How to Segue Values When My Viewcontroller Is Embedded in an Uinavigationcontroller
How How to Perform Multiple Alamofire Requests That Are Finished One After Another
Given a Function Parameter of Type [Int]; Can It Be Constrained to Not Be Empty
Apple Push Notifications Without Developer Account
Limiting Concurrent Access to a Service Class with Rxswift
How to Get Core Data Entity by It's Objectid
Swift. Declaring Private Functions in Internal Protocol
Cannot Convert Value of Type 'Binding<String>' to Expected Argument Type 'Binding<String>'
How to Detect the 2D Images Using Arkit and Realitykit
Scrolling Delegate in Tableview
Swift Callkit Sometimes Can't Activate Loudspeaker After Received Call (Only Incoming Call)
Text from [String] to a Label.Text Isn't Working the First Time