Search code examples
swiftswiftuipublisher

SwiftUI onRecieve modifier triggers when a view re-renders


I'm trying to update input to a textfield using a Just publisher and a local state. Whenever the the view re-renders as result of a change to the local state the onRecieve modifier gets triggered. Here's a code sample to showcase the underlying issue.

struct MyTextField: View {
   @State private var someLocalState = ""
   @State private var inputText = ""

   var body: some View {
       TextField("Enter Text", text: $inputText)
           .onReceive(Just(inputText)) { newValue in
                print(newValue)
                // do something, store result in localstate
                someLocalState = doSomething() // this causes onRecieve to trigger and print again
                if someLocalState meets some condition {
                    inputText = someLocalState
                }
           }
   }
}

I always get an extra print. How do I prevent onRecieve from triggering when someLocalState updates


Solution

  • You shouldn't be using onReceive with Just to observe a @State value.

    Instead, use onChange(of:), which was designed for this purpose exactly - executing side effects when a non-published state changes.

    struct MyTextField: View {
       @State private var someLocalState = ""
       @State private var inputText = ""
    
       var body: some View {
           TextField("Enter Text", text: $inputText)
               .onChange(of: inputText) { newValue in
                    print(newValue)
                    // do something, store result in localstate
                    someLocalState = doSomething() // this causes onRecieve to trigger and print again
               }
       }
    }
    

    onChange(of:) is only available on iOS 14, so if you are targeting iOS 13, you can't use it unfortunately.

    If you need a solution for iOS 13, you should check whether the value has actually changed and only execute the state update in case the value is different than the old value.

      .onChange(of: inputText) { newValue in
        print(newValue)
        guard newValue != inputText else { return }
        // do something, store result in localstate
        someLocalState = doSomething() // this causes onRecieve to trigger and print again
      }