Skip to content

Commit 94df696

Browse files
committed
fix: dispatch UIAppearance proxy setters via target metadata
1 parent c26766e commit 94df696

4 files changed

Lines changed: 107 additions & 0 deletions

File tree

HANDOFF.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,41 @@ 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 11:07 EDT:
115+
116+
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.
117+
- GitHub Actions run `28379145481` on `c26766ef` kept setup, dependency
118+
install, FFI boundary, V8 download, libffi build, metadata generation,
119+
NativeScript build, CLI build, and macOS tests green, but still failed iOS
120+
only in `ApiTests.js Appearance`: `UILabel.appearance().textColor` read
121+
back `undefined`.
122+
- CI disproved the selector-group accessor cache as the final missing path.
123+
The live iOS/JSC assignment is consistent with the host-object set trap
124+
seeing a UIKit appearance proxy whose runtime Objective-C class is not the
125+
customizable target class (`UILabel`), so exact proxy-class metadata lookup
126+
misses and the write falls back to an expando on that proxy instance.
127+
- Runtime fix in progress:
128+
- `NativeApiObjectHostObject::set` now resolves tagged/described
129+
`UIAppearance` proxies back to their customizable target class metadata
130+
before the runtime-discovered setter fallback.
131+
- The target-class metadata setter is invoked on the appearance proxy and the
132+
existing target-class-scoped appearance cache is populated only after the
133+
native call succeeds.
134+
- `callObjCSelector` now narrowly allows metadata-backed instance property
135+
selectors on tagged/described appearance proxies to pass the availability
136+
check even when UIKit handles them through Objective-C forwarding instead
137+
of `respondsToSelector:`.
138+
- Local verification after this patch:
139+
- Runtime RN JS tests passed:
140+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
141+
- `npm run check:ffi-boundaries` passed.
142+
- `git diff --check` passed.
143+
- `npm run build:macos-cli` passed.
144+
- Not done:
145+
- Commit/push the host-object UIAppearance metadata fix and watch fresh PR
146+
CI.
147+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
148+
114149
Update from 2026-06-29 10:22 EDT:
115150

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

NativeScript/ffi/shared/bridge/HostObjects.mm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,31 @@ throw JSError(
18151815
}
18161816
}
18171817

1818+
if (Class appearanceClass =
1819+
taggedAppearanceProxyClass(runtime, bridge_, object_)) {
1820+
if (const NativeApiSymbol* symbol =
1821+
bridge_->findClassForRuntimeClass(appearanceClass)) {
1822+
const auto& members = bridge_->membersForClass(*symbol);
1823+
if (const NativeApiMember* propertyMember =
1824+
selectWritablePropertyMember(members, property, false)) {
1825+
if (propertyMember->readonly ||
1826+
propertyMember->setterSelectorName.empty()) {
1827+
throw JSError(
1828+
runtime, "Attempted to assign to readonly property.");
1829+
}
1830+
NativeApiMember setterMember = *propertyMember;
1831+
setterMember.selectorName = propertyMember->setterSelectorName;
1832+
setterMember.signatureOffset = propertyMember->setterSignatureOffset;
1833+
Value args[] = {Value(runtime, value)};
1834+
callObjCSelector(runtime, bridge_, object_, false,
1835+
setterMember.selectorName, &setterMember, args, 1);
1836+
cacheAppearanceProxyPropertyValue(runtime, bridge_, object_,
1837+
property, value);
1838+
NATIVE_API_SET_RETURN(true);
1839+
}
1840+
}
1841+
}
1842+
18181843
if (auto setterSelectorName =
18191844
runtimeWritablePropertySetter(object_, property)) {
18201845
Value args[] = {Value(runtime, value)};

NativeScript/ffi/shared/bridge/Invocation.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,7 +1673,17 @@ throw JSError(runtime,
16731673
Class lookupClass = dispatchSuperClass != Nil ? dispatchSuperClass : receiverClass;
16741674
Method method = receiverIsClass ? class_getClassMethod(lookupClass, selector)
16751675
: class_getInstanceMethod(lookupClass, selector);
1676+
bool allowForwardedAppearancePropertySelector = false;
1677+
if (method == nullptr && !receiverIsClass && member != nullptr &&
1678+
member->property && bridge != nullptr &&
1679+
taggedAppearanceProxyClass(runtime, bridge, receiver) != Nil) {
1680+
allowForwardedAppearancePropertySelector =
1681+
(count == 0 && selectorName == member->selectorName) ||
1682+
(count == 1 && !member->setterSelectorName.empty() &&
1683+
selectorName == member->setterSelectorName);
1684+
}
16761685
if (method == nullptr &&
1686+
!allowForwardedAppearancePropertySelector &&
16771687
(dispatchSuperClass != Nil || ![receiver respondsToSelector:selector])) {
16781688
throw JSError(runtime,
16791689
"Objective-C selector is not available: " +

PROGRESS.md

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

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

20+
### 2026-06-29 11:07 EDT - host-object UIAppearance target metadata
21+
22+
- Goal:
23+
- Keep PR #46 on the generic runtime primitive path and unblock the final
24+
simulator-only RN Screens parity sweep; no physical devices were used.
25+
- CI finding:
26+
- GitHub Actions run `28379145481` on `c26766ef` kept setup, dependencies,
27+
FFI boundary check, V8 download, libffi build, metadata generation,
28+
NativeScript build, CLI build, and macOS tests green, but still failed iOS
29+
`ApiTests.js Appearance` only: `UILabel.appearance().textColor` read back
30+
as `undefined`.
31+
- That disproved the selector-group accessor cache as the remaining path.
32+
The failing write/readback is consistent with the host-object set trap
33+
receiving the UIKit appearance proxy instance directly. The proxy runtime
34+
class does not carry `UILabel` metadata, so exact-class metadata lookup
35+
misses and the old path could store only a per-proxy expando.
36+
- Changes:
37+
- Host-object property writes on tagged/described `UIAppearance` proxies now
38+
resolve the customizable target class metadata before falling back to
39+
runtime-discovered setters.
40+
- The target-class metadata setter is invoked on the appearance proxy and the
41+
existing class-scoped appearance cache is populated only after that native
42+
call succeeds.
43+
- `callObjCSelector` now narrowly permits metadata-backed instance property
44+
selectors on appearance proxies to reach UIKit's Objective-C forwarding
45+
path when the proxy does not advertise the selector with
46+
`respondsToSelector:`.
47+
- Verification:
48+
- Runtime RN JS tests passed:
49+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
50+
- `npm run check:ffi-boundaries` passed.
51+
- `git diff --check` passed.
52+
- `npm run build:macos-cli` passed.
53+
- Still next:
54+
- Commit/push the host-object metadata fix, watch fresh PR CI, then return
55+
to the dedicated simulator-only RNS parity sweep if CI is green.
56+
2057
### 2026-06-29 10:22 EDT - cached selector-group UIAppearance accessors
2158

2259
- Goal:

0 commit comments

Comments
 (0)