[iOS][Fabric] Dynamic borderColor/outlineColor (PlatformColor/DynamicColorIOS) resolve against the system appearance, ignoring overrideUserInterfaceStyle
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 72/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 冷清
- 技术栈
- ios, objective-c, react-native
- 领域
- mobile-dev
调研方向
从 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm 开始,检查 invalidateLayer 以及 border-image 和 outline 的转换路径。在 iOS 上运行 RNTester Playground 复现步骤,分别使用浅色系统外观和强制深色覆盖。对于已挂载、新挂载和回收复用的 View,如果动态边框和 outline 颜色都遵循 View 的有效 trait collection,同时背景色保持正确,即表示完成。
由索引模型根据 Issue 内容生成。
描述
Reproducer
Single-file RNTester reproducer — edits only RNTesterPlayground.js:
- Branch: https://github.com/JacquesLeupin/react-native/tree/repro/fabric-border-trait-collection-57836
- Diff vs
main: https://github.com/facebook/react-native/compare/main...JacquesLeupin:react-native:repro/fabric-border-trait-collection-57836
Run RNTester on iOS with the simulator's System Appearance set to Light and open the Playground example: it forces the app dark via Appearance.setColorScheme('dark') at 2s and mounts a second row of boxes at 4s (a button resets to the system scheme and re-runs). The same reproducer as a standalone stock-CLI app is inline under Steps to reproduce below.
Description
On the New Architecture, borderColor (all edges) and outlineColor set from a dynamic color (PlatformColor or DynamicColorIOS) resolve against the system appearance instead of the view's effective appearance. When an app forces an appearance that differs from the system — Appearance.setColorScheme('dark') (which sets overrideUserInterfaceStyle on every window) or a native overrideUserInterfaceStyle assignment — borders render the wrong appearance variant while backgroundColor and text render correctly.
Worse, the wrong border persists. Two shapes (both observed in the reproducer / a production app):
- Views already mounted when the override flips keep their stale borders:
traitCollectionDidChange:→invalidateLayerexists for exactly this, but the border conversion inside it still reads the ambient trait state rather than the view's own traits, and the borders demonstrably stay on the pre-override variant whilebackgroundColor(a UIView-managed dynamic color) corrects itself. - Recycled component views reattach under unchanged traits, so no trait-change callback ever fires for them; whatever
invalidateLayerresolved during a mounting pass (ambient = system appearance) sticks. In a production app with list recycling this makes bordered surfaces render system-variant borders indefinitely under a launch-time override.
The old architecture renders this correctly: Paper resolves border colors against the view's trait collection in RCTView (displayLayer: via borderColorsWithTraitCollection:), so this is a Fabric regression relative to Paper.
Root cause
In React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm, invalidateLayer resolves backgroundColor explicitly against the view's trait collection:
UIColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection];
…but the border and outline paths convert dynamic UIColors straight to CGColor with no explicit resolve:
UIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left);
layer.borderColor = borderColor.CGColor; // resolves via UITraitCollection.currentTraitCollection
-[UIColor CGColor] on a dynamic color resolves against UITraitCollection.currentTraitCollection, which tracks the system appearance — Fabric mounting runs outside UIKit's trait-context callbacks, so it never matches a window-level overrideUserInterfaceStyle. The same flattening affects RCTCreateRCTBorderColorsFromBorderColors (the border-image path) and both outlineColor paths. (layer.shadowColor in updateProps has the same latent issue.)
Steps to reproduce
- Set the iOS Simulator/device System Appearance to Light.
- Create a stock app (
npx @react-native-community/cli init— New Architecture default) and use theApp.tsxbelow. - Launch. The app forces dark via
Appearance.setColorScheme('dark')after 2s, then mounts a second pair of boxes at 4s.
import React, {useEffect, useState} from 'react';
import {Appearance, DynamicColorIOS, StyleSheet, Text, useColorScheme, View} from 'react-native';
// ONE dynamic color for both fills and borders: red in light, green in dark.
const dynamicColor = DynamicColorIOS({light: '#ff3b30', dark: '#34c759'});
function Boxes({label}: {label: string}) {
return (
<View style={styles.section}>
<Text style={styles.sectionLabel}>{label}</Text>
<View style={styles.row}>
<View style={[styles.box, {backgroundColor: dynamicColor}]} />
<View style={[styles.box, styles.bordered, {borderColor: dynamicColor}]} />
<View style={[styles.box, styles.bordered, styles.clipped, {borderColor: dynamicColor}]} />
</View>
</View>
);
}
export default function App() {
const scheme = useColorScheme();
const [afterOverride, setAfterOverride] = useState(false);
useEffect(() => {
const t1 = setTimeout(() => Appearance.setColorScheme('dark'), 2000);
const t2 = setTimeout(() => setAfterOverride(true), 4000);
return () => { clearTimeout(t1); clearTimeout(t2); };
}, []);
return (
<View style={styles.screen}>
<Text style={styles.title}>useColorScheme(): {String(scheme)}</Text>
<Boxes label="A: mounted BEFORE the dark override" />
{afterOverride && <Boxes label="B: mounted AFTER the dark override" />}
</View>
);
}
const styles = StyleSheet.create({
screen: {flex: 1, paddingTop: 90, paddingHorizontal: 24, backgroundColor: '#202124'},
title: {fontSize: 18, fontWeight: '600', color: '#fff'},
section: {marginTop: 16},
sectionLabel: {fontSize: 14, color: '#fff', marginBottom: 8},
row: {flexDirection: 'row', gap: 12},
clipped: {overflow: 'hidden'},
box: {width: 104, height: 80, borderRadius: 8},
bordered: {borderWidth: 6},
});
The three boxes per row exercise backgroundColor, the border-image path (default overflow), and the CoreAnimation layer.borderColor path (overflow: 'hidden').
Expected: once the app is dark (useColorScheme() reports dark), every fill and border is green.
Actual: every fill is green, but section A's borders — on both border code paths — remain red (the light variant) permanently after the override flips. Freshly-created views in section B can render correctly via the attach-time trait change; recycled views (the common case in real list-heavy apps) reattach with unchanged traits and stay wrong, which is how this presents across whole production screens.
Version and platforms
- Reproduced on 0.81.5 and 0.86.2 (New Architecture); the affected code is unchanged on
main(f7a8360696). - iOS only. Old architecture renders correctly.
Related
- #42006 (closed) — PlatformColor/DynamicColorIOS regressions on the New Architecture; the "only the initial scheme applies" half of that report matches this mechanism for borders.
- #30377 (closed, 2020) — the Paper-era version of dynamic border colors not updating, fixed for Paper via trait-collection resolution in
RCTView.
A fix PR that mirrors the existing backgroundColor handling (resolve against self.traitCollection at all four border/outline conversion sites) accompanies this issue.
- 主要语言
- C++
- 星标
- 127k
- 派生
- 25.3k
- PR 合并指标
- 30 天内没有已合并 PR
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
react/react-native 的其他 Issue
-
Needs: Author Feedback Needs: Repro
难度 1/5 1 小时以内 新手友好度 92/100
react/react-native#58621 · 1 条评论 ·
-
Needs: Author Feedback Needs: Repro
难度 2/5 1-3 小时 新手友好度 85/100
react/react-native#58610 · 1 条评论 ·
-
Needs: Triage :mag:
难度 2/5 1-3 小时 新手友好度 82/100
react/react-native#58565 · 1 条评论 · 2 个 reaction ·
-
Needs: Author Feedback Needs: Repro
难度 2/5 1-3 小时 新手友好度 88/100
react/react-native#58555 · 5 条评论 · 2 个 reaction ·
-
Needs: Attention Needs: Repro
难度 2/5 1-3 小时 新手友好度 85/100
react/react-native#58526 · 2 条评论 ·
查看 react/react-native 的全部 Issue
相似的 Issue
-
ai_reviewed
难度 2/5 1-3 小时 新手友好度 68/100
ydb-platform/ydb#53869 · 3 条评论 ·
-
bug cert blocker needs triage
难度 2/5 1-3 小时 新手友好度 82/100
project-chip/connectedhomeip#74373 ·
-
upstream update
难度 2/5 1-3 小时 新手友好度 75/100
conan-io/conan-center-index#31035 ·
-
Bug
难度 2/5 1-3 小时 新手友好度 68/100
-
documentation
难度 1/5 1 小时以内 新手友好度 85/100
vllm-project/vllm-ascend#17329 ·