Skip to content

Commit 7eae675

Browse files
committed
fix: prefer writable appearance proxy members
1 parent 1b12a71 commit 7eae675

4 files changed

Lines changed: 156 additions & 28 deletions

File tree

HANDOFF.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,44 @@ During this thread `agent-device` was version `0.18.0`; the npm package is
111111

112112
## Current Verified State
113113

114+
Update from 2026-06-29 16:05 EDT:
115+
116+
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.
117+
- GitHub Actions run `28396473566` on `1b12a71e` kept setup, dependency
118+
install, FFI boundary check, V8 download, libffi build, metadata generation,
119+
NativeScript build, CLI build, and macOS tests green.
120+
- iOS simulator still failed in `ApiTests.js Appearance` at the same
121+
`UILabel.appearance().textColor` readback:
122+
`Expected undefined to be { }`.
123+
- Root cause refinement:
124+
- The previous host-set fallback still did not help if the returned
125+
`UIAppearance` proxy got a getter-only own accessor.
126+
- The accessor installer de-duplicated target-class metadata by the first
127+
property name it saw. If duplicate members exist and the first member is
128+
readonly or less capable than a later UIKit property member, assignment is
129+
ignored before the host-object fallback can run.
130+
- Current runtime fix:
131+
- UIAppearance accessor installation now gathers candidate members by
132+
property name and prefers writable members, then explicit setter selectors,
133+
before defining the cache-backed own accessor.
134+
- Tagged UIAppearance host-object assignment now runs before generic
135+
native-object assignment and uses the same writable-preferred proxy member
136+
selection.
137+
- Source coverage guards both the writable-preferred accessor selection and
138+
the UIAppearance-before-generic host-set order.
139+
- Local verification after this correction:
140+
- `node packages/react-native/test/runtime-objc-property-setter.test.js`
141+
passed.
142+
- Runtime RN JS tests passed:
143+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
144+
- `npm run check:ffi-boundaries` passed.
145+
- `git diff --check` passed.
146+
- `npm run build:macos-cli` passed.
147+
- Not done:
148+
- Commit/push this writable-member selection correction and watch fresh PR
149+
CI.
150+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
151+
114152
Update from 2026-06-29 15:13 EDT:
115153

116154
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.

NativeScript/ffi/shared/bridge/HostObjects.mm

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,41 @@ void cacheAppearanceProxyPropertyValue(
644644
}
645645
}
646646

647+
const NativeApiMember* betterAppearanceProxyAccessorMember(
648+
const NativeApiMember* current, const NativeApiMember& candidate) {
649+
if (current == nullptr) {
650+
return &candidate;
651+
}
652+
if (current->readonly != candidate.readonly) {
653+
return candidate.readonly ? current : &candidate;
654+
}
655+
if (current->setterSelectorName.empty() &&
656+
!candidate.setterSelectorName.empty()) {
657+
return &candidate;
658+
}
659+
if (current->signatureOffset == MD_SECTION_OFFSET_NULL &&
660+
candidate.signatureOffset != MD_SECTION_OFFSET_NULL) {
661+
return &candidate;
662+
}
663+
return current;
664+
}
665+
666+
const NativeApiMember* selectAppearanceProxyPropertyMember(
667+
const std::vector<NativeApiMember>& members, const std::string& property) {
668+
const NativeApiMember* selected = nullptr;
669+
for (const auto& member : members) {
670+
if (!member.property || member.name != property) {
671+
continue;
672+
}
673+
bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0;
674+
if (memberIsStatic) {
675+
continue;
676+
}
677+
selected = betterAppearanceProxyAccessorMember(selected, member);
678+
}
679+
return selected;
680+
}
681+
647682
class NativeApiSuperHostObject final : public HostObject {
648683
public:
649684
NativeApiSuperHostObject(std::shared_ptr<NativeApiBridge> bridge,
@@ -1787,34 +1822,13 @@ NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value
17871822
throw JSError(runtime, "Cannot set property on nil object.");
17881823
}
17891824

1790-
if (const NativeApiSymbol* symbol =
1791-
bridge_->findClassForRuntimeClass(object_getClass(object_))) {
1792-
const auto& members = bridge_->membersForClass(*symbol);
1793-
if (const NativeApiMember* propertyMember =
1794-
selectWritablePropertyMember(members, property, false)) {
1795-
if (propertyMember->readonly) {
1796-
throw JSError(
1797-
runtime, "Attempted to assign to readonly property.");
1798-
}
1799-
NativeApiMember setterMember = *propertyMember;
1800-
setterMember.selectorName = propertyMember->setterSelectorName;
1801-
setterMember.signatureOffset = propertyMember->setterSignatureOffset;
1802-
Value args[] = {Value(runtime, value)};
1803-
callObjCSelector(runtime, bridge_, object_, false,
1804-
setterMember.selectorName, &setterMember, args, 1);
1805-
cacheAppearanceProxyPropertyValue(runtime, bridge_, object_, property,
1806-
value);
1807-
NATIVE_API_SET_RETURN(true);
1808-
}
1809-
}
1810-
18111825
if (Class appearanceClass =
18121826
taggedAppearanceProxyClass(runtime, bridge_, object_)) {
18131827
if (const NativeApiSymbol* symbol =
18141828
bridge_->findClassForRuntimeClass(appearanceClass)) {
18151829
const auto& members = bridge_->membersForClass(*symbol);
18161830
if (const NativeApiMember* propertyMember =
1817-
selectWritablePropertyMember(members, property, false)) {
1831+
selectAppearanceProxyPropertyMember(members, property)) {
18181832
if (propertyMember->readonly) {
18191833
throw JSError(
18201834
runtime, "Attempted to assign to readonly property.");
@@ -1841,6 +1855,27 @@ throw JSError(
18411855
}
18421856
}
18431857

1858+
if (const NativeApiSymbol* symbol =
1859+
bridge_->findClassForRuntimeClass(object_getClass(object_))) {
1860+
const auto& members = bridge_->membersForClass(*symbol);
1861+
if (const NativeApiMember* propertyMember =
1862+
selectWritablePropertyMember(members, property, false)) {
1863+
if (propertyMember->readonly) {
1864+
throw JSError(
1865+
runtime, "Attempted to assign to readonly property.");
1866+
}
1867+
NativeApiMember setterMember = *propertyMember;
1868+
setterMember.selectorName = propertyMember->setterSelectorName;
1869+
setterMember.signatureOffset = propertyMember->setterSignatureOffset;
1870+
Value args[] = {Value(runtime, value)};
1871+
callObjCSelector(runtime, bridge_, object_, false,
1872+
setterMember.selectorName, &setterMember, args, 1);
1873+
cacheAppearanceProxyPropertyValue(runtime, bridge_, object_, property,
1874+
value);
1875+
NATIVE_API_SET_RETURN(true);
1876+
}
1877+
}
1878+
18441879
if (auto setterSelectorName =
18451880
runtimeWritablePropertySetter(object_, property)) {
18461881
Value args[] = {Value(runtime, value)};
@@ -2013,15 +2048,20 @@ void installAppearanceProxyPropertyAccessors(
20132048
runtime.global().getPropertyAsObject(runtime, "Object");
20142049
Function defineProperty =
20152050
objectConstructor.getPropertyAsFunction(runtime, "defineProperty");
2016-
std::unordered_set<std::string> installed;
20172051
std::shared_ptr<void> retainedNative =
20182052
retainAppearanceProxyForAccessor(native);
20192053
const auto& members = bridge->membersForClass(*symbol);
2054+
std::unordered_map<std::string, const NativeApiMember*> accessors;
20202055
for (const auto& member : members) {
2021-
if (!shouldInstallAppearanceProxyAccessor(member) ||
2022-
!installed.insert(member.name).second) {
2056+
if (!shouldInstallAppearanceProxyAccessor(member)) {
20232057
continue;
20242058
}
2059+
accessors[member.name] =
2060+
betterAppearanceProxyAccessorMember(accessors[member.name], member);
2061+
}
2062+
2063+
for (const auto& accessor : accessors) {
2064+
const NativeApiMember& member = *accessor.second;
20252065

20262066
try {
20272067
Object descriptor(runtime);

PROGRESS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,45 @@ TypeScript/UI worklets.
1717

1818
## Latest Update - 2026-06-29
1919

20+
### 2026-06-29 16:05 EDT - UIAppearance writable member selection
21+
22+
- Goal:
23+
- Stay on PR #46's generic runtime primitive path for the RN module branch
24+
and remain simulator-only. No physical devices were used.
25+
- CI finding:
26+
- GitHub Actions run `28396473566` on `1b12a71e` kept setup, dependency
27+
install, FFI boundary check, V8 download, libffi build, metadata
28+
generation, NativeScript build, CLI build, and macOS tests green.
29+
- iOS simulator still failed in `ApiTests.js Appearance` at the same
30+
`UILabel.appearance().textColor` readback:
31+
`Expected undefined to be { }`.
32+
- Refinement: if target-class metadata contains duplicate property members,
33+
the appearance accessor installer could de-duplicate on the first member
34+
name and install a getter-only descriptor before the writable UIKit member
35+
was seen. That drops assignment before the host-object fallback can cache
36+
the value.
37+
- Changes:
38+
- UIAppearance accessor installation now gathers members by property name
39+
first and prefers writable members, then members with explicit setter
40+
selectors, before defining the cache-backed own accessor.
41+
- Tagged UIAppearance host-object assignment now runs before generic
42+
native-object assignment and uses the same writable-preferred proxy member
43+
selection.
44+
- Source coverage now guards the writable-preferred accessor selection and
45+
the UIAppearance-before-generic host-set order.
46+
- Verification:
47+
- `node packages/react-native/test/runtime-objc-property-setter.test.js`
48+
passed.
49+
- Runtime RN JS tests passed:
50+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
51+
- `npm run check:ffi-boundaries` passed.
52+
- `git diff --check` passed.
53+
- `npm run build:macos-cli` passed and compiled/linked the edited V8 bridge.
54+
- Still next:
55+
- Commit/push this writable-member selection correction and watch fresh PR
56+
CI for the authoritative iOS simulator result.
57+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
58+
2059
### 2026-06-29 15:13 EDT - UIAppearance host-set fallback
2160

2261
- Goal:

packages/react-native/test/runtime-objc-property-setter.test.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,12 @@ const appearanceAccessorSource = runtimeHostObjects.slice(
5151
appearanceAccessorStart,
5252
appearanceAccessorEnd,
5353
);
54+
const nativeObjectHostObjectStart = runtimeHostObjects.indexOf(
55+
"class NativeApiObjectHostObject final",
56+
);
5457
const appearanceHostSetStart = runtimeHostObjects.indexOf(
55-
"if (Class appearanceClass =",
58+
"NativeApiHostSetResult set(Runtime& runtime, const PropNameID& name, const Value& value) override",
59+
nativeObjectHostObjectStart,
5660
);
5761
const appearanceHostSetEnd = runtimeHostObjects.indexOf(
5862
"\n if (auto setterSelectorName =",
@@ -78,16 +82,23 @@ assert(
7882
runtimeHostObjects.includes("installAppearanceProxyPropertyAccessors") &&
7983
runtimeHostObjects.includes("makeAppearanceProxyPropertySetter") &&
8084
runtimeHostObjects.includes("makeAppearanceProxyPropertyGetter") &&
85+
runtimeHostObjects.includes("betterAppearanceProxyAccessorMember") &&
86+
runtimeHostObjects.includes("std::unordered_map<std::string, const NativeApiMember*> accessors") &&
8187
runtimeHostObjects.includes("cacheAppearanceProxyPropertyValue(") &&
8288
appearanceAccessorSource.includes("runtimeWritablePropertySetter(native, member.name)") &&
8389
appearanceAccessorSource.includes("if (!member.readonly)") &&
90+
appearanceAccessorSource.includes("betterAppearanceProxyAccessorMember(accessors[member.name], member)") &&
91+
!appearanceAccessorSource.includes("std::unordered_set<std::string> installed") &&
8492
!appearanceAccessorSource.includes("!member.readonly && !member.setterSelectorName.empty()"),
85-
"runtime UIAppearance proxies should install safe property accessors backed by the appearance cache",
93+
"runtime UIAppearance proxies should install writable-preferred safe property accessors backed by the appearance cache",
8694
);
8795
assert(
8896
appearanceHostSetSource.includes("runtimeWritablePropertySetter(object_, property)") &&
97+
appearanceHostSetSource.includes("selectAppearanceProxyPropertyMember(members, property)") &&
98+
appearanceHostSetSource.indexOf("taggedAppearanceProxyClass(runtime, bridge_, object_)") <
99+
appearanceHostSetSource.indexOf("findClassForRuntimeClass(object_getClass(object_))") &&
89100
!appearanceHostSetSource.includes("propertyMember->readonly ||\n propertyMember->setterSelectorName.empty()"),
90-
"runtime UIAppearance host-object assignment should use the same runtime setter fallback before caching",
101+
"runtime UIAppearance host-object assignment should select proxy members before generic object setters and use the same runtime setter fallback before caching",
91102
);
92103
assert(
93104
!runtimeHostObjects.includes("SetNativeApiObjectPrototype(runtime, resultObject"),

0 commit comments

Comments
 (0)