Skip to content

Commit 597a74c

Browse files
committed
fix(ios): preserve custom tab colors on iOS 26
1 parent 1f24108 commit 597a74c

22 files changed

Lines changed: 108 additions & 4 deletions

artifacts/issue-532/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ The transparent and opaque example screens use the same tabs and article content
2020
- agent-device: 0.20.10
2121
- Metro: repository example bundle on port 8085
2222

23+
No iOS 26.x runtime is installed in the available environment. The requested reproduction range was iOS 26 or newer, so the iOS 27.0 runtime exercises the `#available(iOS 26.0, *)` implementation and satisfies that range. These captures must not be represented as exact iOS 26.x evidence; an iOS 26.x compatibility run remains optional follow-up evidence if that runtime becomes available.
24+
2325
## Reproduction
2426

2527
The installed example app was opened with the repository's Metro bundle. For both existing example routes, the Article tab was captured at `What is Lorem Ipsum?`, scrolled down three equivalent agent-device scroll units, and explicitly verified at `Where can I get some?` before the bottom capture.
@@ -37,7 +39,7 @@ Before the fix, `Four Tabs - Transparent scroll edge appearance` and `Four Tabs
3739

3840
Runtime inspection confirmed that the JavaScript prop reached native code and that UIKit held the expected `UITabBarAppearance` values: the opaque route had `systemBackgroundColor` with a visible background, and the transparent route had a hidden scroll-edge background. On iOS 26 and newer, the floating Liquid Glass tab bar does not render those appearance-object background states as an opaque backing surface. Setting the public `UITabBar.backgroundColor` does control that backing surface.
3941

40-
The fix therefore sets the tab bar's own background only when the requested appearance is opaque (or `translucent={false}` already requires opacity), using `barTintColor` when supplied and dynamic `systemBackground` otherwise. It clears that value for transparent/default configurations. The existing `UITabBarAppearance` configuration remains responsible for item styling, shadows, and pre-iOS-26 behavior.
42+
The fix therefore sets the tab bar's own background on iOS 26 and newer when the requested appearance is opaque, when `translucent={false}` already requires opacity, or when a `barTintColor` is supplied for a non-transparent appearance. It preserves the supplied dynamic `UIColor`; otherwise opaque configurations use dynamic `systemBackground`. Transparent always clears the backing, and default/unconfigured configurations without a custom color retain the platform default. The existing `UITabBarAppearance` configuration remains responsible for item styling, shadows, and pre-iOS-26 behavior.
4143

4244
After the fix, the opaque route has a solid theme background while transparent and default preserve the platform Liquid Glass appearance. All cases were recorded at the same top and bottom article positions:
4345

@@ -51,6 +53,15 @@ After the fix, the opaque route has a solid theme background while transparent a
5153
- [Opaque, bottom](verification/screenshots/opaque-bottom.png)
5254
- [Opaque scroll recording](verification/videos/opaque-scroll.mp4)
5355

56+
The top/bottom screenshots above were refreshed from the final remediated binary on 2026-09-03. Additional compatibility evidence from the same simulator covers the adversarial-review findings:
57+
58+
- [Custom background, top](verification/compatibility/custom-background-top.png) and [bottom](verification/compatibility/custom-background-bottom.png)
59+
- Opaque minimize behavior: [expanded](verification/compatibility/opaque-minimize-expanded.png), [collapsed](verification/compatibility/opaque-minimize-collapsed.png), and [restored](verification/compatibility/opaque-minimize-restored.png)
60+
- Opaque hide/show: [shown](verification/compatibility/opaque-hide-shown.png), [hidden](verification/compatibility/opaque-hide-hidden.png), and [restored](verification/compatibility/opaque-hide-restored.png)
61+
- Dynamic appearance: [dark](verification/compatibility/opaque-dark.png) and [light restored](verification/compatibility/opaque-light-restored.png)
62+
- [`translucent={false}`](verification/compatibility/translucent-false.png)
63+
- [Custom tab bar](verification/compatibility/custom-tabbar.png)
64+
5465
## Validation
5566

5667
- iOS simulator build: Xcode 27.0, iOS 27.0 SDK, deployment target iOS 15.1 — passed
@@ -62,4 +73,4 @@ After the fix, the opaque route has a solid theme background while transparent a
6273
- `git diff --check` — passed
6374
- Device workflow: open, interactive snapshot, navigate, capture top, record and scroll, explicitly verify bottom content, capture bottom, close — passed
6475

65-
The final automated checks and adversarial-review disposition are recorded alongside this evidence before the Draft PR is created.
76+
The adversarial-review findings and the exact remaining runtime verification are recorded in [review/dispositions.md](review/dispositions.md). The successful Fable JSON is preserved unchanged at [review/fable-review.json](review/fable-review.json).
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Issue #532 Fable review dispositions
2+
3+
This file records the single successful Claude Code Fable/High review and the single GPT-5.6-Sol/High remediation pass for the four findings in `fable-review.json`. The review JSON is intentionally unchanged, and Fable was not rerun after remediation.
4+
5+
## 1. Custom `barTintColor` with default/unconfigured scroll-edge appearance
6+
7+
**Fixed.** The finding was valid. On iOS 26 and newer, `updateTabBarAppearance` now gives a non-transparent configured `barTintColor` to the tab bar backing even when `scrollEdgeAppearance` is default or unset. The precedence is explicit:
8+
9+
| Configuration | iOS 26+ tab-bar backing |
10+
| --- | --- |
11+
| `scrollEdgeAppearance="transparent"` | cleared, including when a color was supplied |
12+
| default/unconfigured plus `barTintColor` | supplied color |
13+
| `scrollEdgeAppearance="opaque"` plus `barTintColor` | supplied color |
14+
| `scrollEdgeAppearance="opaque"` without a color | dynamic `systemBackground` |
15+
| `translucent={false}` without a color | dynamic `systemBackground` |
16+
| default/unconfigured, translucent, no color | cleared so UIKit retains its default |
17+
18+
Repository evidence: `apps/example/src/App.tsx` exposes “Four Tabs - Custom Background Color of Tabs” with `#87CEEB`; `apps/example/src/Examples/FourTabs.tsx` maps that value to `tabBarStyle.backgroundColor`; `packages/react-native-bottom-tabs/src/TabView.tsx` maps it to native `barTintColor`; and `TabViewImpl.swift` now consumes it in the iOS 26+ backing path. Passing the existing `UIColor` object to `UIView.backgroundColor` preserves dynamic colors. The block is compiled only for iOS and runs only on iOS 26+, so pre-iOS-26 behavior and tvOS, visionOS, and macOS behavior remain unchanged. The standard appearance object continues to cover both normal and scroll-edge positions.
19+
20+
The final remediated binary was verified through the repository's custom-background example at equivalent top and bottom article positions. Both captures show the configured `#87CEEB` backing: [top](../verification/compatibility/custom-background-top.png) and [bottom](../verification/compatibility/custom-background-bottom.png).
21+
22+
## 2. Minimize, hide, and dark-mode interaction
23+
24+
**Concretely disproven.** The finding described a possible artifact but provided no reproduction or runtime evidence. The implementation uses the public inherited `UIView.backgroundColor` on the real `UITabBar`; it does not add an overlay, mask, appearance proxy, private API, delayed update, or independently positioned view. Existing hiding is also applied to the real system tab bar through SwiftUI's public `toolbar(.hidden, for: .tabBar)` path on supported OS versions. The remediation preserves dynamic `UIColor.systemBackground`, which is the theme-aware value needed for light/dark changes.
25+
26+
No speculative transition workaround was added because UIKit exposes no public minimized-state callback in the installed iOS 27 SDK, and changing or disabling the caller's `minimizeBehavior` would violate that public API.
27+
28+
The primary agent completed the real-app verification on the documented iPhone 17 Pro / iOS 27.0 simulator with the repository Metro bundle and the same public props. Temporary QA-only example wiring was removed before the final diff. The opaque minimize path collapses to the system-managed control without leaving a rectangular band and restores its full backing on scroll-up: [expanded](../verification/compatibility/opaque-minimize-expanded.png), [collapsed](../verification/compatibility/opaque-minimize-collapsed.png), and [restored](../verification/compatibility/opaque-minimize-restored.png).
29+
30+
Hide/show leaves no backing while hidden and restores cleanly: [shown](../verification/compatibility/opaque-hide-shown.png), [hidden](../verification/compatibility/opaque-hide-hidden.png), and [restored](../verification/compatibility/opaque-hide-restored.png). Dynamic `systemBackground` resolves to black in [dark appearance](../verification/compatibility/opaque-dark.png) and back to white after [restoring light appearance](../verification/compatibility/opaque-light-restored.png).
31+
32+
The final binary also rechecked default, transparent, and opaque at matching top/bottom positions; verified [`translucent={false}`](../verification/compatibility/translucent-false.png); and verified the repository's [custom tab-bar example](../verification/compatibility/custom-tabbar.png). The lifecycle captures were made immediately before the custom-color-only remediation was installed; that remediation does not alter the opaque condition or value exercised by those captures. The final installed binary then refreshed all default/transparent/opaque screenshots and captured the custom-color, `translucent={false}`, and custom-tab-bar cases.
33+
34+
## 3. Evidence captured on iOS 27 rather than iOS 26
35+
36+
**Concretely disproven as a requirements violation.** The explicit reproduction requirement is iOS 26 or newer, not exactly iOS 26.x. The captured device reports iOS 27.0, which is within that range and executes the `#available(iOS 26.0, *)` branch. The evidence README records the exact simulator, runtime, Xcode beta, and capture date; it does not label the captures as iOS 26.
37+
38+
The limitation is now explicit in the evidence README: no iOS 26.x runtime is installed, so the branch has no exact-26 capture and none is fabricated. If an iOS 26.x runtime later becomes available, a compatibility run may add confidence about version-specific compositing, but it was not part of the user's `>=26` acceptance requirement.
39+
40+
## 4. Checked-in binary evidence and missing disposition record
41+
42+
**Concretely disproven in part and fixed in part.** Repository-size preference cannot override the user's explicit requirement that clear issue-specific screenshots and videos be committed on this branch. All existing media is therefore preserved. The valid incompleteness portion is fixed by this disposition file, and the evidence README now links both this file and the unchanged successful Fable JSON instead of promising a future record.
43+
44+
## Remediation checks
45+
46+
- `yarn lint` — passed; the three reported warnings predate this remediation and are unrelated to issue #532.
47+
- `yarn typecheck` — passed.
48+
- `yarn build` — passed.
49+
- `RCT_NEW_ARCH_ENABLED=1 yarn build:ios` — passed.
50+
- Core Jest test (`src/__tests__/index.test.tsx`) — passed with the repository's one existing todo.
51+
- The `react-native-bottom-tabs` CocoaPods scheme was built for the generic iOS device SDK with code signing disabled — passed, including compilation of `TabViewImpl.swift`.
52+
- The complete example workspace was built for the iPhone 17 Pro / iOS 27.0 simulator with code signing disabled — passed.
53+
- The final remediated app was installed and the default, transparent, opaque, custom-color, `translucent={false}`, and custom-tab-bar cases were explicitly verified. Minimize, hide/show, and light/dark lifecycle checks also passed as documented above.
54+
- `git diff --check` — passed.
55+
- `fable-review.json` parsed with all four findings and remained unchanged at SHA-256 `6a57310f6a0f8c9885ae6f3962e5ebbe6f7eb9e2d49b6d9308a93b08aac55942`.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"findings": [
3+
{
4+
"file": "packages/react-native-bottom-tabs/ios/TabViewImpl.swift",
5+
"line": 138,
6+
"defect": "The iOS 26 opacity condition (`scrollEdgeAppearance == \"opaque\" || !translucent`) ignores a supplied barTintColor, so the documented `tabBarStyle.backgroundColor` contract (docs/docs/docs/guides/usage-with-react-navigation.mdx:155 and standalone-usage.mdx:209, 'Background color of the tab bar') remains non-functional on iOS 26+ when scrollEdgeAppearance is \"default\" or unset. On iOS <= 18 the same props render the custom color via appearance.backgroundColor (TabViewImpl.swift:193-195); on iOS 26 the new code actively clears tabBar.backgroundColor for that configuration. The fix touches exactly this code path and consumes props.barTintColor only in the opaque branch, leaving the documented API inconsistent across OS versions, and no checked-in evidence exercises a barTintColor route.",
7+
"failure_scenario": "An app (or the repository's own 'Four Tabs - Custom Background Color of Tabs' example, apps/example/src/App.tsx:74-76, which passes backgroundColor '#87CEEB' with default scrollEdgeAppearance) sets only tabBarStyle.backgroundColor — a common React Navigation configuration. On iOS 18 the tab bar shows the custom color; on iOS 26 the color is silently ignored and the plain Liquid Glass bar renders, reproducing the same class of 'appearance prop has no effect on iOS 26' bug this branch claims to fix.",
8+
"severity": "medium"
9+
},
10+
{
11+
"file": "packages/react-native-bottom-tabs/ios/TabViewImpl.swift",
12+
"line": 139,
13+
"defect": "Setting the UITabBar view's own UIView.backgroundColor paints an unmanaged opaque layer under the Liquid Glass bar instead of configuring the bar's background, and its interaction with iOS-26-only features the library itself exposes is unverified. With tabBarMinimizeBehavior (TabViewImpl.swift:47, 528) the system collapses the bar into a floating pill during scroll, and a full-frame view background is not part of the system-managed transition; toolbar(.hidden) tab-bar hiding transitions and dark-mode rendering of the new opaque strip are likewise untested. The checked-in evidence covers only static, light-mode captures of the default/transparent/opaque routes with no minimize behavior, no hide transition, and no dark appearance.",
14+
"failure_scenario": "A user combines scrollEdgeAppearance=\"opaque\" with tabBarMinimizeBehavior=\"onScrollDown\" on iOS 26: while the system animates the bar down to its minimized pill, the UITabBar view's full-frame opaque backgroundColor persists or animates as a solid band that does not match the minimized Liquid Glass presentation, producing a visible rectangle artifact over scrolled content. Nothing in the branch's evidence or tests would catch this before release.",
15+
"severity": "medium"
16+
},
17+
{
18+
"file": "artifacts/issue-532/README.md",
19+
"line": 19,
20+
"defect": "All baseline and verification evidence was captured on an iOS 27.0 beta simulator runtime with Xcode 27.0 Beta 4, but the code gate is #available(iOS 26.0, *), the changeset claims 'Fix scrollEdgeAppearance on iOS 26 and newer', and issue #532 reports released iOS 26 behavior. No capture on any iOS 26.x runtime exists, so the checked-in visual evidence does not prove the stated behavior on the OS version the fix targets; Liquid Glass handling of a raw UITabBar view backgroundColor can differ between released iOS 26.x and a later beta.",
21+
"failure_scenario": "The patch ships with the changeset's iOS 26 claim, but on devices running released iOS 26.x the raw view backgroundColor renders differently than on the iOS 27 beta (e.g., clipped by the glass container or not composited as an opaque strip), leaving the originally reported transparent-vs-opaque bug unfixed or introducing a new artifact for the exact user population that filed issue #532.",
22+
"severity": "medium"
23+
},
24+
{
25+
"file": "artifacts/issue-532/README.md",
26+
"line": 65,
27+
"defect": "The branch commits ~32 MB of binary evidence (six .mp4 recordings, ten .png screenshots; 23 MB in verification/videos alone) into the library repository under artifacts/issue-532/, a directory with no precedent in the repo, permanently bloating git history for every consumer clone; this evidence belongs on the PR/issue, not in the package repository. Additionally, this line promises that 'final automated checks and adversarial-review disposition are recorded alongside this evidence', but no such record exists in the tree, leaving the evidence document self-referentially incomplete.",
28+
"failure_scenario": "Every future clone, CI checkout, and fork of react-native-bottom-tabs downloads 32 MB of one-off issue videos forever (git history retains them even if later deleted), and reviewers relying on the README's promised checks/disposition record find it missing, undermining the evidentiary value the artifacts were committed to provide.",
29+
"severity": "low"
30+
}
31+
]
32+
}
192 KB
Loading
239 KB
Loading
22.3 KB
Loading
240 KB
Loading
20.3 KB
Loading
24.1 KB
Loading
24.1 KB
Loading

0 commit comments

Comments
 (0)