@@ -350,6 +350,85 @@ binding's wrapped value will be automatically set back to `nil`, otherwise
350
350
` resignFirstResponder() ` will be called and the binding's wrapped value will
351
351
be set to ` nil ` once the first responder state has become ` notFirstResponder ` .
352
352
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
+
353
432
### Example: Using ` @State ` to become first responder on view appear
354
433
355
434
``` swift
0 commit comments