You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Note fields corrupt typed text and force the cursor to the end (TextFieldWithToolBarString)
Describe the bug
Two problems in the free-text Note fields, both in TextFieldWithToolBarString.
1. Typed characters get duplicated or dropped. Typing at a normal pace into a meal Note produces wrong text. Typing sausage has produced sausagege. Characters vanish and come back, and the field feels like it is autocompleting, even though autocorrect is explicitly disabled on it. Intermittent, and only when the screen is busy. See To Reproduce, because it will not reproduce on an idle screen.
2. The cursor is forced to the end of the text. Tapping into the middle of an existing note puts the caret where you tapped for an instant, then moves it to the end. There is no way to position the caret anywhere but the end, so a typo in the middle of a note cannot be fixed without deleting everything after it.
These are related. See Technical Details: the mechanism behind problem 1 also moves the caret, so it is a second and independent cause of problem 2.
Both affect every field that uses TextFieldWithToolBarString, which is all four of these:
Trio/Sources/Modules/History/View/CarbEntryEditorView.swift:172 (note, editing a past carb entry)
Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift:34 (Name Of Dish, for a meal preset)
Attach a Log
Will attach if useful, though this is a pure UI issue and I would not expect the log to show anything.
To Reproduce
For the text corruption. The main thread has to be busy, or this will not reproduce. The write to the binding is deferred to the main queue, so on an idle screen it drains between keystrokes and nothing goes wrong. Typing sausage into a quiet meal screen ten times in a row will look perfectly fine.
Go to the meal entry screen.
Enter or change a carb value, which triggers an insulin recalculation and a forecast chart redraw.
Immediately tap the Note field and type a word of seven or more characters at a normal pace, for example sausage.
Characters briefly disappear and reappear. Retyping the missing ones while the restore is still pending produces duplicates, observed as sausagege.
Catching a loop cycle mid-typing works too and is probably the harsher test, but it is awkward to time deliberately. See #1114 for a report of the treatments window going laggy exactly when a loop initiates.
Reported with tapped keys, not swipe typing.
To be clear about what is observed and what is derived: the corruption itself is observed. The requirement for a busy main thread is inferred from the deferred write in the code, and I've has not tested loop timing against it. If it turns out to reproduce on a completely idle screen, that is worth knowing, and would suggest the window is wider than the code alone implies.
For the cursor:
Tap the Note field and type two words, for example chicken burrito.
Dismiss the keyboard.
Tap the field again, aiming at the middle of the first word.
The caret appears where you tapped, then jumps to the end of the text after first character is entered.
Expected behavior
Typed characters appear exactly once, in order. Tapping positions the caret where you tapped, as in every other text field on iOS.
Screenshots
Including screen recording to show cursor placement issue.
ScreenRecording_08-16-2026.10-52-29_1.MP4
Setup Information (please complete the following information):
Smartphone:
Hardware: iPhone 16 Pro
OS Version: iOS 27.0 beta
Pump:
Manufacturer: Insulet
Model: Omnipod 5
CGM:
Device: Dexcom G7
Manager app: Dexcom App
Trio Version:
Version Number: 0.8.4.54 (build 1)
Repo: nightscout/trio
Git Reference: dev 29a5417 (2026-08-09)
Technical Details
Problem 1: the binding is written asynchronously from a stale snapshot
Trio/Sources/Views/TextFieldWithToolBar.swift, in the UITextFieldDelegate extension:
letnewText= currentText.replacingCharacters(in: range, with: string)
// Update the binding text state
DispatchQueue.main.async{self.parent.text = newText
}
return true
newText is computed from currentText, the field's value before this keystroke is applied, and the write to the binding is deferred. Meanwhile return true lets UIKit apply the keystroke immediately.
updateUIView at :381 then reconciles:
publicfunc updateUIView(_ textField:UITextField, context:Context){if textField.text != text {
textField.text = text
}
Typing sausage, with the last two keystrokes landing faster than the main queue drains:
Time
Event
textField.text
parent.text
t0
types g, schedules A = sausag
sausag
sausa
t1
types e, schedules B = sausage
sausage
sausa
t2
A runs, updateUIView clobbers
sausag
sausag
t3
B runs, updateUIView restores
sausage
sausage
At t2 the character is visibly lost. At t3 the pending write restores the full string wholesale. The duplication is the user losing that race. Seeing the tail of the word vanish at t2, they retype it, and the t3 restore lands first, so the retyped characters append to an already-correct string. sausag, restored to sausage, plus a retyped ge, gives sausagege. The reporter described it as the field appearing to autocomplete while their own typing still went in, which is precisely this: the restore is the phantom autocomplete.
This only bites when the main queue is slow enough that a keystroke lands before the previous one's write has drained. updateUIView fires on every SwiftUI re-render of the parent, and the treatments view re-renders while recalculating insulin and redrawing the forecast chart, which is also when the main thread is most likely to be stalled. That combination is why this is intermittent rather than constant, and why it needs a busy screen to reproduce.
Scope: the numeric variant is not affected
Worth stating plainly, since #1114 mentions digits doubling in the bolus field and the two could easily be triaged together. They are not the same defect.
The only two DispatchQueue.main.async calls in TextFieldWithToolBar.swift are at :420 and :449, both inside TextFieldWithToolBarString. The numeric TextFieldWithToolBar at :4-312 binds a plain SwiftUI TextField to a @State string and updates through synchronous onChange handlers, with no deferred write anywhere in its typing path. It cannot exhibit the race described here.
The doubled digits in #1114 are more likely the input lag in that report causing a tap to register twice, which would be a symptom of the freeze rather than of this.
This matches use in practice. I've only ever seen the corruption in the Note field, never in carbs, fat, protein, or bolus, which is what the code above predicts.
The same mechanism moves the caret
Assigning .text on a UITextField resets selectedTextRange to the end of the document. So every stale rewrite at t2 also sends the caret to the end, mid-typing. This is independent of the editingDidBegin handler below.
Problem 2: the caret is also forced to the end on focus
The DispatchQueue.main.async is why it is visible rather than silent. UIKit sets the caret from the tap during the touch, and the reset runs on the next runloop turn, so the caret is seen to land and then move. moveCursorToEnd() is a general UITextField extension at :313-319.
Why this looks inherited rather than intended
TextFieldWithToolBarString at :327 is the string counterpart to the numeric TextFieldWithToolBar at :4, which is pure SwiftUI and uses @FocusState, and has neither of these problems.
Forcing the caret to the end is reasonable for append-only numeric entry. But TextFieldWithToolBarString is only ever used for free text: all four call sites above are notes or names, and none is numeric, so no current caller benefits from it.
Suggested fix
For problem 1, update the binding synchronously rather than through DispatchQueue.main.async, or preserve and restore selectedTextRange around the assignment in updateUIView and guard against applying a stale value.
For problem 2, remove the editingDidBegin target from TextFieldWithToolBarString, or gate it behind an opt-in parameter defaulting to off. Since no current call site is numeric, removing it outright looks safe.
Additional context
Same struct, same defaults, all inherited from the numeric variant and arguably wrong for free text. Happy to split these into a separate issue, but they are one file and likely one fix:
Line
Setting
Effect on a note field
:333
autocorrectionType = .no
no autocorrect, no spellcheck, and no predictive text bar
:332
autocapitalizationType = .none
never capitalizes, including at the start of a sentence
:330
textAlignment = .right
notes are right-aligned
:350
adjustsFontSizeToFitWidth = true
text shrinks as it is typed rather than scrolling
Separately, all four call sites pass maxLength: 25. Typing simply stops at the limit with no indication why. Twenty five characters is tight for a meal note, and tighter still for Name Of Dish in AddMealPresetView.swift:34.
Note fields corrupt typed text and force the cursor to the end (TextFieldWithToolBarString)
Describe the bug
Two problems in the free-text Note fields, both in
TextFieldWithToolBarString.1. Typed characters get duplicated or dropped. Typing at a normal pace into a meal Note produces wrong text. Typing
sausagehas producedsausagege. Characters vanish and come back, and the field feels like it is autocompleting, even though autocorrect is explicitly disabled on it. Intermittent, and only when the screen is busy. See To Reproduce, because it will not reproduce on an idle screen.2. The cursor is forced to the end of the text. Tapping into the middle of an existing note puts the caret where you tapped for an instant, then moves it to the end. There is no way to position the caret anywhere but the end, so a typo in the middle of a note cannot be fixed without deleting everything after it.
These are related. See Technical Details: the mechanism behind problem 1 also moves the caret, so it is a second and independent cause of problem 2.
Both affect every field that uses
TextFieldWithToolBarString, which is all four of these:Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift:280(meal Note)Trio/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift:60(note)Trio/Sources/Modules/History/View/CarbEntryEditorView.swift:172(note, editing a past carb entry)Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift:34(Name Of Dish, for a meal preset)Attach a Log
Will attach if useful, though this is a pure UI issue and I would not expect the log to show anything.
To Reproduce
For the text corruption. The main thread has to be busy, or this will not reproduce. The write to the binding is deferred to the main queue, so on an idle screen it drains between keystrokes and nothing goes wrong. Typing
sausageinto a quiet meal screen ten times in a row will look perfectly fine.sausage.sausagege.Catching a loop cycle mid-typing works too and is probably the harsher test, but it is awkward to time deliberately. See #1114 for a report of the treatments window going laggy exactly when a loop initiates.
Reported with tapped keys, not swipe typing.
To be clear about what is observed and what is derived: the corruption itself is observed. The requirement for a busy main thread is inferred from the deferred write in the code, and I've has not tested loop timing against it. If it turns out to reproduce on a completely idle screen, that is worth knowing, and would suggest the window is wider than the code alone implies.
For the cursor:
chicken burrito.Expected behavior
Typed characters appear exactly once, in order. Tapping positions the caret where you tapped, as in every other text field on iOS.
Screenshots
Including screen recording to show cursor placement issue.
ScreenRecording_08-16-2026.10-52-29_1.MP4
Setup Information (please complete the following information):
Smartphone:
Pump:
CGM:
Trio Version:
29a5417(2026-08-09)Technical Details
Problem 1: the binding is written asynchronously from a stale snapshot
Trio/Sources/Views/TextFieldWithToolBar.swift, in theUITextFieldDelegateextension:newTextis computed fromcurrentText, the field's value before this keystroke is applied, and the write to the binding is deferred. Meanwhilereturn truelets UIKit apply the keystroke immediately.updateUIViewat:381then reconciles:Typing
sausage, with the last two keystrokes landing faster than the main queue drains:textField.textparent.textg, schedules A =sausagsausagsausae, schedules B =sausagesausagesausaupdateUIViewclobberssausagsausagupdateUIViewrestoressausagesausageAt t2 the character is visibly lost. At t3 the pending write restores the full string wholesale. The duplication is the user losing that race. Seeing the tail of the word vanish at t2, they retype it, and the t3 restore lands first, so the retyped characters append to an already-correct string.
sausag, restored tosausage, plus a retypedge, givessausagege. The reporter described it as the field appearing to autocomplete while their own typing still went in, which is precisely this: the restore is the phantom autocomplete.This only bites when the main queue is slow enough that a keystroke lands before the previous one's write has drained.
updateUIViewfires on every SwiftUI re-render of the parent, and the treatments view re-renders while recalculating insulin and redrawing the forecast chart, which is also when the main thread is most likely to be stalled. That combination is why this is intermittent rather than constant, and why it needs a busy screen to reproduce.Scope: the numeric variant is not affected
Worth stating plainly, since #1114 mentions digits doubling in the bolus field and the two could easily be triaged together. They are not the same defect.
The only two
DispatchQueue.main.asynccalls inTextFieldWithToolBar.swiftare at:420and:449, both insideTextFieldWithToolBarString. The numericTextFieldWithToolBarat:4-312binds a plain SwiftUITextFieldto a@Statestring and updates through synchronousonChangehandlers, with no deferred write anywhere in its typing path. It cannot exhibit the race described here.The doubled digits in #1114 are more likely the input lag in that report causing a tap to register twice, which would be a symptom of the freeze rather than of this.
This matches use in practice. I've only ever seen the corruption in the Note field, never in carbs, fat, protein, or bolus, which is what the code above predicts.
The same mechanism moves the caret
Assigning
.texton aUITextFieldresetsselectedTextRangeto the end of the document. So every stale rewrite at t2 also sends the caret to the end, mid-typing. This is independent of theeditingDidBeginhandler below.Problem 2: the caret is also forced to the end on focus
Trio/Sources/Views/TextFieldWithToolBar.swift:342Trio/Sources/Views/TextFieldWithToolBar.swift:419-421The
DispatchQueue.main.asyncis why it is visible rather than silent. UIKit sets the caret from the tap during the touch, and the reset runs on the next runloop turn, so the caret is seen to land and then move.moveCursorToEnd()is a generalUITextFieldextension at:313-319.Why this looks inherited rather than intended
TextFieldWithToolBarStringat:327is the string counterpart to the numericTextFieldWithToolBarat:4, which is pure SwiftUI and uses@FocusState, and has neither of these problems.Forcing the caret to the end is reasonable for append-only numeric entry. But
TextFieldWithToolBarStringis only ever used for free text: all four call sites above are notes or names, and none is numeric, so no current caller benefits from it.Suggested fix
For problem 1, update the binding synchronously rather than through
DispatchQueue.main.async, or preserve and restoreselectedTextRangearound the assignment inupdateUIViewand guard against applying a stale value.For problem 2, remove the
editingDidBegintarget fromTextFieldWithToolBarString, or gate it behind an opt-in parameter defaulting to off. Since no current call site is numeric, removing it outright looks safe.Additional context
Same struct, same defaults, all inherited from the numeric variant and arguably wrong for free text. Happy to split these into a separate issue, but they are one file and likely one fix:
:333autocorrectionType = .no:332autocapitalizationType = .none:330textAlignment = .right:350adjustsFontSizeToFitWidth = trueSeparately, all four call sites pass
maxLength: 25. Typing simply stops at the limit with no indication why. Twenty five characters is tight for a meal note, and tighter still forName Of DishinAddMealPresetView.swift:34.