@@ -350,6 +350,85 @@ binding's wrapped value will be automatically set back to `nil`, otherwise
350350` resignFirstResponder() ` will be called and the binding's wrapped value will
351351be set to ` nil ` once the first responder state has become ` notFirstResponder ` .
352352
353+ ### Avoiding nested view updates
354+
355+ When using a ` firstResponderStateChangeHandler ` to update some state that
356+ triggers a view update in combination with state-driven first responder changes, it
357+ is possible to end up in a situation where you are triggering a view update in the
358+ middle of existing view update cycle which will result in a runtime warning about
359+ undefined behaviour.
360+
361+ This can occur because state-driven first responder changes cause the text field
362+ to become first responder as part of a view update - this means that the change
363+ handler itself will be called during that view update so if it was to trigger another
364+ view update when called, it would happen within the current view update.
365+
366+ In the following example, a warning would occur because the change to the
367+ ` @State ` variable results in a nested view update:
368+
369+ ``` swift
370+ struct ExampleView : View {
371+ @State
372+ var someString: String
373+
374+ @State
375+ var firstText: String
376+
377+ @State
378+ var secondText: String
379+
380+ @State
381+ var secondResponderDemand: FirstResponderDemand
382+
383+ var body: some View {
384+ Text (" The text is: \( someString ) " )
385+ ResponsiveTextField (
386+ placeholder : " First" ,
387+ text : $firstText,
388+ handleReturn : {
389+ // make the second field become first responder
390+ secondResponderDemand = .shouldBecomeFirstResponder
391+ }
392+ )
393+ ResponsiveTextField (
394+ placeholder : " Second" ,
395+ text : $secondText,
396+ firstResponderDemand :
397+ onFirstResponderStateChanged: .init { _ in
398+ // This will be called during the view update triggered
399+ // by mutating `shouldBecomeFirstResponder` in the first
400+ // field's `handleReturn` closure.
401+ // This will trigger a nested state change!
402+ someString = " Hello World"
403+ }
404+ )
405+ }
406+ }
407+ ```
408+
409+ To workaround this problem, rather than the library explicitly calling the state change
410+ handler on the next runloop tick or on an asynchronous ` DispatchQueue ` , which
411+ might not be necessary if there is no nested state change, you can avoid the
412+ problem by ensuring that the view update your state change handler triggers
413+ always happens after the view update completes.
414+
415+ A convenience modifier on ` FirstResponderStateChangeHandler ` , ` receive(on:) `
416+ allows you to do this by passing in a scheduler such as a runloop or dispatch queue.
417+ The above example can be fixed with the following change to the second text field:
418+
419+ ``` swift
420+ ResponsiveTextField (
421+ placeholder : " Second" ,
422+ text : $secondText,
423+ firstResponderDemand :
424+ onFirstResponderStateChanged: .init { _ in
425+ // This will now be triggered on the next runloop tick and
426+ // will not trigger a nested state change warning.
427+ someString = " Hello World"
428+ }.receive (on : RunLoop.main )
429+ )
430+ ```
431+
353432### Example: Using ` @State ` to become first responder on view appear
354433
355434``` swift
0 commit comments