[iOS][Fabric] Dynamic borderColor/outlineColor (PlatformColor/DynamicColorIOS) resolve against the system appearance, ignoring overrideUserInterfaceStyle
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 72/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- ios, objective-c, react-native
- Lĩnh vực
- mobile-dev
Hướng nghiên cứu
Bắt đầu tại packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm và kiểm tra invalidateLayer cùng các đường dẫn chuyển đổi border-image và outline. Chạy trình tái hiện RNTester Playground trên iOS với giao diện hệ thống sáng và ghi đè tối bắt buộc. Được xem là hoàn tất khi màu viền và outline động tuân theo trait collection hiệu lực của view đối với các view đã mount, mới mount và được tái sử dụng, trong khi màu nền vẫn chính xác.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.
- Ngôn ngữ chính
- C++
- Star
- 127k
- Fork
- 25.3k
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của react/react-native
-
Needs: Author Feedback Needs: Repro
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 92/100
react/react-native#58621 · 1 bình luận ·
-
Needs: Author Feedback Needs: Repro
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
react/react-native#58610 · 1 bình luận ·
-
Needs: Triage :mag:
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
react/react-native#58565 · 1 bình luận · 2 reaction ·
-
Needs: Author Feedback Needs: Repro
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
react/react-native#58555 · 5 bình luận · 2 reaction ·
-
Needs: Attention Needs: Repro
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
react/react-native#58526 · 2 bình luận ·
Tất cả issue của react/react-native
Issue tương tự
-
ai_reviewed
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
ydb-platform/ydb#53869 · 3 bình luận ·
-
bug cert blocker needs triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
project-chip/connectedhomeip#74373 ·
-
[request] tracy/0.14.1 Đang mởupstream update
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
conan-io/conan-center-index#31035 ·
-
Bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
-
documentation
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
vllm-project/vllm-ascend#17329 ·