Skip to content

Commit 1120491

Browse files
committed
Updated README
1 parent a65067d commit 1120491

File tree

1 file changed

+79
-0
lines changed

1 file changed

+79
-0
lines changed

README.md

+79
Original file line numberDiff line numberDiff line change
@@ -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
351351
be 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

Comments
 (0)