How to detect live changes on TextField in SwiftUI?
Asked Answered
D

6

105

I have a simple TextField that binds to the state 'location' like this,

TextField("Search Location", text: $location)

I want to call a function each time this field changes, something like this:

TextField("Search Location", text: $location) {
   self.autocomplete(location)
}

However this doesn't work. I know that there are callbacks, onEditingChanged - however this only seems to be triggered when the field is focussed.

How can I get this function to call each time the field is updated?

Dissection answered 10/9, 2019 at 17:16 Comment(0)
A
115

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

    }
}
Afoot answered 10/9, 2019 at 17:42 Comment(9)
Hi kontiki! Just a question about your interesting answer (I had never thought to use Binding that way): I was wondering why if I attach a didSet on the location var the didSet doesn't get called at all. Something like @State var location: String = "" { didSet { //do something }} I don't get why the function doesn't get called. Thank you.Fauver
didSet on location never triggers, because what you are changing through the binding, is location.wrappedValue, not location.Afoot
Great solution for adjusting from the textFieldDidChange() in SwiftUI thank you!Artel
I get the Function declares an opaque return type, but has no return statements in its body from which to infer an underlying typeResection
trying to add return to body - can not, says Binding<String> needs to conform to ViewResection
There is one problem with this solution: it triggers the view body to be recreated on every text changeChyack
Recreate view is very lightweight in SwiftUI, so this drawback is still acceptable as we don't have textFieldDidChange - yet. Don't forget to return your body if you get "Function declares an opaque return type, but has no return statements" errorVasques
Thanks 👍 what a great solution! I've used it to perform an action when textfield count >= 9Hintze
The set closure is called three times with every stroke.Confiscate
U
123

SwiftUI 2.0

From iOS 14, macOS 11, or any other OS contains SwiftUI 2.0, there is a new modifier called .onChange that detects any change of the given state:

struct ContentView: View {
    @State var location: String = ""

    var body: some View {
        TextField("Your Location", text: $location)
            .onChange(of: location) {
                print($0) // You can do anything due to the change here.
                // self.autocomplete($0) // like this
            }
    }
}

SwiftUI 1.0

For older iOS and other SwiftUI 1.0 platforms, you can use onReceive:

.onReceive(location.publisher) { 
    print($0)
}
**Note that** it returns **the change** instead of the entire value. If you need the behavior the same as the `onChange`, you can use the **combine** and follow the answer provided by @pawello2222.
Uncaused answered 15/8, 2020 at 12:23 Comment(2)
I wonder why this doesn't work for a TextFIeld in an alert to limit the number of characters that can be set in that textField?Troubadour
But what about 'should' change?Mariomariology
A
115

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

    }
}
Afoot answered 10/9, 2019 at 17:42 Comment(9)
Hi kontiki! Just a question about your interesting answer (I had never thought to use Binding that way): I was wondering why if I attach a didSet on the location var the didSet doesn't get called at all. Something like @State var location: String = "" { didSet { //do something }} I don't get why the function doesn't get called. Thank you.Fauver
didSet on location never triggers, because what you are changing through the binding, is location.wrappedValue, not location.Afoot
Great solution for adjusting from the textFieldDidChange() in SwiftUI thank you!Artel
I get the Function declares an opaque return type, but has no return statements in its body from which to infer an underlying typeResection
trying to add return to body - can not, says Binding<String> needs to conform to ViewResection
There is one problem with this solution: it triggers the view body to be recreated on every text changeChyack
Recreate view is very lightweight in SwiftUI, so this drawback is still acceptable as we don't have textFieldDidChange - yet. Don't forget to return your body if you get "Function declares an opaque return type, but has no return statements" errorVasques
Thanks 👍 what a great solution! I've used it to perform an action when textfield count >= 9Hintze
The set closure is called three times with every stroke.Confiscate
F
49

Another solution, if you need to work with a ViewModel, could be:

import SwiftUI
import Combine

class ViewModel: ObservableObject {
    @Published var location = "" {
        didSet {
            print("set")
            //do whatever you want
        }
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        TextField("Search Location", text: $viewModel.location)
    }
}
Fauver answered 10/9, 2019 at 21:4 Comment(9)
Hi @superpuccio, if you are going to use a ViewModel, you could just put the code in didSet { ... }. No need for: cancellable/sink. ;-)Afoot
This is a cleaner answer than the accepted, IMO. It could get messy fast if you have multiple bindings within your body var.Lyrism
Does it need to be an observable object?Farman
@Farman yes, otherwise you won’t get updates on Published properties.Fauver
@Fauver - Can you take a look at this ? #63048179Brilliant
ObservedObject will be recreated when redraw. So the text will lost after redraw.Pyrolysis
@FrankCheng Observed objects are not recreated when your view gets redrawn (i.e. when the body of your view is requested again). They are recreated when your view is inside another view's body and that view gets redrawn (i.e. its body is requested again, in that body there's your child view containing the observed object, so your view is entirely recreated with all its properties, including your observed object). If this is your case you should inject the observed object in your view from the outside or, if you can make use of iOS 14, try the new StateObject which is meant exactly for this.Fauver
SwiftUI doesn't use View ModelsMelitta
Just a small thing, The SwiftUI framework includes Combine. So "import Combine" isn't needed.Exhume
H
24

iOS 13+

Use onReceive:

import Combine
import SwiftUI

struct ContentView: View {
    @State var location: String = ""

    var body: some View {
        TextField("Search Location", text: $location)
            .onReceive(Just(location)) { location in
                // print(location)
            }
    }
}
Hospitalize answered 1/9, 2020 at 20:21 Comment(5)
This is actually a very nice, clean and clever way of doing this!Hedgerow
Does this actually work? Wouldn't you need Just(location) to be stored somewhere to subscribe to it and receive its values?Tadeas
@Tadeas It works perfectly fine. I encourage you to try it yourself.Hospitalize
@Hospitalize I ended up needing to so something a bit different for my use case. I have a textfield that has an error label underneath it. The error label only needs to show if there is an error. And the validation to trigger that error label needs to run on keystroke (with a 300 ms) debounce. So what I ended up doing was making an observable object view model for my textfield. With a text and errorText published properties And I .onRecieve(viewModel.$text.debounce(300, runloop.main)) the $text variable to trigger my validation with a .debounce works like a charm.Tadeas
Best method! This also works on MacOs by the way :)Thrifty
B
15

While other answers work might work but this one worked for me where I needed to listen to the text change as well as react to it.

first step create one extension function.

extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler(newValue)
            }
        )
    }
}

now call change on the binding in TextField something like below.

  TextField("hint", text: $text.onChange({ (value) in
      //do something here
  }))

source : HackingWithSwift

Bipartite answered 20/5, 2021 at 16:12 Comment(0)
P
5

What I found most useful was that TextField has a property that is called onEditingChanged which is called when editing starts and when editing completes.

TextField("Enter song title", text: self.$userData.songs[self.songIndex].name, onEditingChanged: { (changed) in
    if changed {
        print("text edit has begun")
    } else {
        print("committed the change")
        saveSongs(self.userData.songs)
    }
})
    .textFieldStyle(RoundedBorderTextFieldStyle())
    .font(.largeTitle)
Philosopher answered 22/11, 2019 at 17:39 Comment(3)
The onEditingChanged is NOT called when the text changes, but when editing starts or stops. So called once when you tap inside the text field, then called next when you hit Done. And it does work that way. If you're looking for something called on each keystroke, this isn't it. But it's a perfectly good solution, for what it is.Decapitate
The question specifically is about when the text changes, not for when editing starts/stops. This is an incorrect answer for this question.Farman
Oh, you are correct. Useful but not what they were asking.Philosopher

© 2022 - 2024 — McMap. All rights reserved.