96

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?

Tilak Madichetti
  • 4,110
  • 5
  • 34
  • 52
dylankbuckley
  • 1,507
  • 1
  • 13
  • 21

6 Answers6

111

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

    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • 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. – superpuccio Sep 11 '19 at 15:09
  • 3
    didSet on location never triggers, because what you are changing through the binding, is location.wrappedValue, not location. – kontiki Sep 11 '19 at 15:14
  • Great solution for adjusting from the textFieldDidChange() in SwiftUI thank you! – Kyle Beard Nov 22 '19 at 01:29
  • 1
    I get the _Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type_ – Async- Apr 20 '20 at 20:43
  • trying to add return to body - can not, says _Binding needs to conform to View_ – Async- Apr 20 '20 at 20:43
  • There is one problem with this solution: it triggers the view body to be recreated on every text change – Saket May 12 '20 at 05:20
  • 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" error – Anthonius Aug 21 '20 at 06:53
  • Thanks what a great solution! I've used it to perform an action when textfield count >= 9 – jawad Sep 27 '20 at 14:59
  • The `set` closure is called three times with every stroke. – Davyd Geyl Aug 15 '22 at 04:38
110

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.
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 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? – alionthego Jun 05 '23 at 23:40
47

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)
    }
}
superpuccio
  • 11,674
  • 8
  • 65
  • 93
  • 1
    Hi @superpuccio, if you are going to use a ViewModel, you could just put the code in didSet { ... }. No need for: cancellable/sink. ;-) – kontiki Sep 11 '19 at 05:20
  • 7
    This is a cleaner answer than the accepted, IMO. It could get messy fast if you have multiple bindings within your body var. – PostCodeism Oct 22 '19 at 20:32
  • Does it need to be an observable object? – Zorayr May 29 '20 at 18:03
  • 1
    @Zorayr yes, otherwise you won’t get updates on Published properties. – superpuccio Jun 04 '20 at 07:34
  • @superpuccio - Can you take a look at this ? https://stackoverflow.com/questions/63047679/how-to-solve-deadlock-on-multiple-published-on-2-textfield-in-swiftui – Tariq Jul 23 '20 at 05:50
  • ObservedObject will be recreated when redraw. So the text will lost after redraw. – Frank Cheng Aug 11 '20 at 05:35
  • 1
    @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. – superpuccio Aug 11 '20 at 10:22
  • SwiftUI doesn't use View Models – malhal Nov 03 '20 at 22:27
  • Just a small thing, The SwiftUI framework includes Combine. So "import Combine" isn't needed. – Marcy Dec 09 '22 at 20:15
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)
            }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 1
    This is actually a very nice, clean and clever way of doing this! – smat88dd Oct 23 '20 at 03:46
  • Does this actually work? Wouldn't you need Just(location) to be stored somewhere to subscribe to it and receive its values? – Xaxxus Dec 12 '20 at 08:36
  • @Xaxxus It works perfectly fine. I encourage you to try it yourself. – pawello2222 Dec 12 '20 at 11:27
  • @pawello2222 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. – Xaxxus Dec 14 '20 at 18:08
  • 1
    Best method! This also works on MacOs by the way :) – Dan Sep 26 '22 at 22:56
12

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

vikas kumar
  • 10,447
  • 2
  • 46
  • 52
6

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)
juanjo
  • 3,737
  • 3
  • 39
  • 44
Dave Levy
  • 1,036
  • 13
  • 20
  • 5
    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. – ConfusionTowers May 18 '20 at 02:48
  • 5
    The question specifically is about when the text changes, not for when editing starts/stops. This is an incorrect answer for this question. – Zorayr May 29 '20 at 18:06
  • Oh, you are correct. Useful but not what they were asking. – Dave Levy Jun 03 '20 at 15:32