fix(mobile): iOS compose button no longer grows while a tab is pressed#28955
Problem
On iPhone with the floating glass tab bar (iOS 26+), the compose button next to the tabs can grow to about 1.16× (62pt → ~72pt) when you switch tabs, for example Settings → Chats. It stays enlarged until the bar happens to lay out again, which can take a second or more.
Repro: on iPhone, open Settings from the floating tab bar, then tap Chats. Watch the compose button at the bottom right.
Change
WorkspaceTabBar.layoutSubviews sizes compose from the union of the tab buttons. When the workspace-tab-* identifiers aren't found, it falls back to every visible control in the bar. The existing comment notes that iOS 26.6 doesn't copy those identifiers onto its tab buttons. The bar also holds a second set of tab buttons for the glass lens. While a tab is pressed, that set is scaled to about 1.16×, and the fallback measured it too. The row height then went from 54 to 62.64, and compose (row.height + 8) from 62 to 70.64.
The fix measures only controls whose converted size equals their own bounds, so anything drawn under a scaling transform is skipped. If nothing qualifies, compose keeps its last frame, as it already does when the row is empty.
Scope and approval
This is a small, focused fix for an obvious visual bug: one filter in one native view. It changes no behavior beyond the compose frame.
Verification
Full app: I built the dev client (expo prebuild + xcodebuild) and ran it on an iPhone 17 Pro simulator (iOS 27.0), paired with a local dev server that holds only test projects and threads. Taps and press-and-hold were real HID touches delivered with AXe. For measurement only, I temporarily added NSLog to WorkspaceTabBar and layoutCompose; none of that is in this PR.
- iOS 27.0, real identifier path: the identifiers are found on every pass (
identified=4 buttons=4 rowH=54), and compose stays 62pt with and without the fix. The simulator doesn't hit the fallback, so the bug doesn't show there unmodified. - Fallback path (iOS 26.6 behavior): I emulated the missing identifiers by temporarily forcing
identifiedempty. Then I tapped and held Settings → Chats three times, holding 0.05 s, 0.3 s and 1 s.- Before:
identified=0 buttons=8, withrowH=62.64on 6 of 18 passes andcompose=70.64on 3 of 9 layouts. The button visibly grows for about 1–2 s per press. - After:
compose=62.00on 9 of 9 layouts. On the scaled passes the filter keeps 4 of 8 buttons and the row stays 54.
- Before:
- Standalone UIKit harness with the same
WorkspaceTabBarcode (iOS 27.0 simulator): it hit the fallback on its own (identified=0 buttons=8, four buttons at 95×62.6 instead of 82×54). It gaveCOMPOSE size=70.64in 3 of 3 runs before the fix and 62 in 3 of 3 runs after. swiftlint lint --config .swiftlint.yml --stricton the changed file: 0 violations.- Not checked: the fix on a physical device. The bug was reported on a physical iPhone, where the enlarged button measured about 72pt against 62pt, matching the 70.64pt above. I didn't check which iOS version that device runs.
Full app, fallback path (simulator, test data). Tap Settings, then Chats:
| Before: compose jumps while the tab is pressed | After: compose keeps its size |
|---|---|
| External image | External image |
MP4: before · after · harness: before · after
Written with Claude Opus 5.5 in Claude Code.
🤖 Generated with Claude Code
Checks
Added 1 commit
- fix(mobile): keep iOS compose button size while a tab is pressed44ea3d5
Approvability
Verdict: Approved at 44ea3d5
Macroscope's review found this PR approvable — The change excludes magnified tab copies from compose-button sizing, preventing the button from growing during tab presses. Its effects are confined to UI layout, with repeated simulator verification supporting the fix.
Notes:
- All code in this push has already been reviewed. Approvability was decided on eligibility alone.
You can add or adjust custom eligibility rules. Learn more.
No actionable comments were generated in the recent review.
External image Recent review info
-
External image Run configuration
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: main
- File: .coderabbit.config.ts
- Commit: fa31b66
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Advanced
- Run ID:
fa2d92dc-166f-4349-a578-0a1217aec9b5
External image Commits
Reviewing files that changed from the base of the PR and between fa31b66bbb2ae5234205a8628a78e038d917202f and 44ea3d5fcb24c62aa627e33565601cdc35c85cf3.
External image Files selected for processing: 1
apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swift
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
Walkthrough
WorkspaceTabBar.layoutSubviewsnow excludes tab-button candidates whose converted bounds size differs from their own bounds size. Identifier-based selection and the fallback to all tab buttons remain unchanged.Changes: iOS tab-bar measurement
Layer / File(s) Summary Filter tab-button candidates
apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swiftlayoutSubviewsfilters candidates by converted bounds size while retaining identifier-based selection and the existing fallback.Estimated code review effort: 2 (Simple) | ~8 minutes
Change: Bug fix
Possibly related issues
pingdotgg/t3code#26774reports abnormal New chat compose-button sizing during iOS tab navigation. This change affects tab-button measurement related to compose-button sizing.
Possibly related PRs
- pingdotgg/t3code#28950: This PR changes the same
WorkspaceTabBar.layoutSubviewsmeasurement behavior to filter magnified tab-button copies. - pingdotgg/t3code#24492: This PR adds the fallback to measure tab controls when workspace-tab identifiers are missing; this change filters candidates in that fallback.
- pingdotgg/t3code#23841: This PR introduces the
WorkspaceTabBarmeasurement and compose-button positioning code that this change modifies.
Suggested reviewers:
wojas
Merge Risk: External image Minimal · up to 44ea3
No actionable merge-blocking risk is established for this change.
Pre-merge checks: Passed 4
Check name Status Explanation Title check Passed The title clearly and concisely describes the main change: preventing the iOS compose button from growing while a tab is pressed. Linked Issues check Passed Check skipped because no linked issues were found for this pull request. Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request. Description check Passed The description includes all required sections. It explains the problem and reproduction steps, the technical change, the focused scope rationale, detailed verification results, limitations, UI eviden… Full details: Description check
Explanation
The description includes all required sections. It explains the problem and reproduction steps, the technical change, the focused scope rationale, detailed verification results, limitations, UI evidence, and agent attribution.
Finishing Touches
Generate unit tests (beta)
- Create a new PR
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Heads-up from a fork: the size check here breaks when the compact tab bar has five items.
The app already plans for five items: NATIVE_WORKSPACE_TAB_SLOTS (workspace-tabs-state.ts#L149-L155) reserves four section slots plus Settings. A compact bar with four sections plus Settings is an expected layout, not an unusual one.
My fork adds a Notes tab, so the bar shows Chats, PRs, Notes, Usage and Settings. With five items the exact == size check rejects every tab button (possibly scaling, possibly floating-point rounding from fractional item widths), so row is null and compose never moves. Today's four-item bar doesn't hit this now, but a fifth item would.
Skipping only the magnified buttons still fixes what this PR fixed, and it works with a five-item row:
| 1 | --- a/apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swift |
| 2 | +++ b/apps/mobile/modules/t3-native-controls/ios/T3SidebarTabBarView.swift |
| 3 | @@ -11,7 +11,7 @@ private final class WorkspaceTabBar: UITabBar { |
| 4 | let candidates = identified.isEmpty ? tabButtons(in: self) { _ in true } : identified |
| 5 | - let buttons = candidates.filter { convert($0.bounds, from: $0).size == $0.bounds.size } |
| 6 | + let buttons = candidates.filter { convert($0.bounds, from: $0).width <= $0.bounds.width + 0.5 } |
| 7 | let row = buttons.reduce(CGRect.null) { $0.union(convert($1.bounds, from: $1)) } |
| 8 | if !row.isNull { onContentLayout?(row) } |
I can't say I fully understand what's happening here, 5 items and 6 items fail very differently, but with the fix, even six items works:
I checked this on an iPhone 16 Pro (iOS 26) with a Release build: compose sits beside the five-item bar again, and still doesn't grow while a tab is pressed.
Follow-up: joaoheusi/t3code#93 found the same bug on the stock four-item bar (393pt iPhone 15 Pro; the 402pt iPhone 17 Pro was fine), and it explains the difference between five and six items that I mentioned above.
Converting a translated rect isn't exact, so at some widths a button fails the == check from rounding alone. With five items only the last tab failed: row ended one tab early, and compose landed on Settings. With six, every button failed, so compose was never placed at all. More items means more fractional widths, which means more failures. Nothing is being scaled.
So this isn't only a fork problem. #93's symmetric 1pt tolerance (abs(size.width - bounds.width) < 1 && abs(size.height - bounds.height) < 1) fixes it and still excludes the lens copy. It would be worth sending upstream.