Skip to content

Commit 9699fef

Browse files
committed
fix: cache protocol UIAppearance accessors
1 parent cb4fc1a commit 9699fef

3 files changed

Lines changed: 128 additions & 33 deletions

File tree

HANDOFF.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,35 @@ 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 09:38 EDT:
115+
116+
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.
117+
- GitHub Actions run `28373879852` on `cb4fc1a9` 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 back
121+
`undefined`.
122+
- Narrow follow-up runtime fix in progress:
123+
- UIAppearance property reads/writes can resolve through protocol-defined
124+
accessors, bypassing `NativeApiObjectHostObject::get/set`.
125+
- Protocol property setters now populate the same target-class-scoped
126+
UIAppearance expando cache after a successful native setter call.
127+
- Protocol property getters now consult that cache before falling through to
128+
the native getter, matching the object-host read path.
129+
- Static `appearance*` result tagging now prefers UIKit's exact
130+
`<Customizable class: ...>` description when tagging a known appearance
131+
selector result, then falls back to the dispatch class.
132+
- Local verification after this patch:
133+
- Runtime RN JS tests passed:
134+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
135+
- `npm run check:ffi-boundaries` passed.
136+
- `git diff --check` passed.
137+
- `npm run build:macos-cli` passed.
138+
- Not done:
139+
- Commit/push this protocol-accessor UIAppearance cache fix and watch fresh
140+
PR CI.
141+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
142+
114143
Update from 2026-06-29 09:00 EDT:
115144

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

NativeScript/ffi/shared/bridge/HostObjects.mm

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -566,18 +566,12 @@ bool objectGetPathCanReadRuntimeProperty(id object,
566566
constexpr const char* kNativeApiAppearanceClassNameExpando =
567567
"__nativeApiAppearanceClassName";
568568

569-
Class appearanceProxyCustomizableClassFromDescription(id object) {
569+
Class appearanceProxyCustomizableClassFromExactDescription(id object) {
570570
#if TARGET_OS_IPHONE
571571
if (object == nil) {
572572
return Nil;
573573
}
574574

575-
const char* runtimeClassName = object_getClassName(object);
576-
if (runtimeClassName == nullptr ||
577-
std::strstr(runtimeClassName, "Appearance") == nullptr) {
578-
return Nil;
579-
}
580-
581575
NSString* description = [object description];
582576
NSString* prefix = @"<Customizable class: ";
583577
if (description.length <= prefix.length + 1 ||
@@ -594,6 +588,24 @@ Class appearanceProxyCustomizableClassFromDescription(id object) {
594588
#endif
595589
}
596590

591+
Class appearanceProxyCustomizableClassFromDescription(id object) {
592+
#if TARGET_OS_IPHONE
593+
if (object == nil) {
594+
return Nil;
595+
}
596+
597+
const char* runtimeClassName = object_getClassName(object);
598+
if (runtimeClassName == nullptr ||
599+
std::strstr(runtimeClassName, "Appearance") == nullptr) {
600+
return Nil;
601+
}
602+
603+
return appearanceProxyCustomizableClassFromExactDescription(object);
604+
#else
605+
return Nil;
606+
#endif
607+
}
608+
597609
Class taggedAppearanceProxyClass(
598610
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
599611
id object) {
@@ -617,6 +629,28 @@ Class taggedAppearanceProxyClass(
617629
return "__nativeApiAppearance:" + property;
618630
}
619631

632+
Value cachedAppearanceProxyPropertyValue(
633+
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
634+
id object, const std::string& property) {
635+
if (Class appearanceClass =
636+
taggedAppearanceProxyClass(runtime, bridge, object)) {
637+
return bridge->findObjectExpando(
638+
runtime, appearanceClass, appearanceProxyExpandoPropertyKey(property));
639+
}
640+
return Value::undefined();
641+
}
642+
643+
void cacheAppearanceProxyPropertyValue(
644+
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
645+
id object, const std::string& property, const Value& value) {
646+
if (Class appearanceClass =
647+
taggedAppearanceProxyClass(runtime, bridge, object)) {
648+
bridge->setObjectExpando(runtime, appearanceClass,
649+
appearanceProxyExpandoPropertyKey(property),
650+
value);
651+
}
652+
}
653+
620654
class NativeApiSuperHostObject final : public HostObject {
621655
public:
622656
NativeApiSuperHostObject(std::shared_ptr<NativeApiBridge> bridge,
@@ -1300,13 +1334,10 @@ Value get(Runtime& runtime, const PropNameID& name) override {
13001334
if (!expando.isUndefined()) {
13011335
return expando;
13021336
}
1303-
if (Class appearanceClass =
1304-
taggedAppearanceProxyClass(runtime, bridge_, object_)) {
1305-
Value appearanceExpando = bridge_->findObjectExpando(
1306-
runtime, appearanceClass, appearanceProxyExpandoPropertyKey(property));
1307-
if (!appearanceExpando.isUndefined()) {
1308-
return appearanceExpando;
1309-
}
1337+
Value appearanceExpando =
1338+
cachedAppearanceProxyPropertyValue(runtime, bridge_, object_, property);
1339+
if (!appearanceExpando.isUndefined()) {
1340+
return appearanceExpando;
13101341
}
13111342

13121343
// Fast path: cached metadata property-getter resolution. Skips the
@@ -1778,12 +1809,8 @@ throw JSError(
17781809
Value args[] = {Value(runtime, value)};
17791810
callObjCSelector(runtime, bridge_, object_, false,
17801811
setterMember.selectorName, &setterMember, args, 1);
1781-
if (Class appearanceClass =
1782-
taggedAppearanceProxyClass(runtime, bridge_, object_)) {
1783-
bridge_->setObjectExpando(
1784-
runtime, appearanceClass,
1785-
appearanceProxyExpandoPropertyKey(property), value);
1786-
}
1812+
cacheAppearanceProxyPropertyValue(runtime, bridge_, object_, property,
1813+
value);
17871814
NATIVE_API_SET_RETURN(true);
17881815
}
17891816
}
@@ -1793,12 +1820,8 @@ throw JSError(
17931820
Value args[] = {Value(runtime, value)};
17941821
callObjCSelector(runtime, bridge_, object_, false,
17951822
*setterSelectorName, nullptr, args, 1);
1796-
if (Class appearanceClass =
1797-
taggedAppearanceProxyClass(runtime, bridge_, object_)) {
1798-
bridge_->setObjectExpando(
1799-
runtime, appearanceClass,
1800-
appearanceProxyExpandoPropertyKey(property), value);
1801-
}
1823+
cacheAppearanceProxyPropertyValue(runtime, bridge_, object_, property,
1824+
value);
18021825
if (!objectGetPathCanReadRuntimeProperty(object_, property)) {
18031826
bridge_->setObjectExpando(runtime, object_, property, value);
18041827
}
@@ -1864,7 +1887,12 @@ void tagStaticAppearanceNativeResult(
18641887
if (bridge == nullptr || appearanceClass == Nil || native == nil) {
18651888
return;
18661889
}
1867-
const char* className = class_getName(appearanceClass);
1890+
Class customizableClass =
1891+
appearanceProxyCustomizableClassFromExactDescription(native);
1892+
if (customizableClass == Nil) {
1893+
customizableClass = appearanceClass;
1894+
}
1895+
const char* className = class_getName(customizableClass);
18681896
if (className == nullptr || className[0] == '\0') {
18691897
return;
18701898
}
@@ -2139,9 +2167,7 @@ throw JSError(runtime,
21392167
.getHostObject<NativeApiObjectHostObject>(
21402168
runtime)
21412169
->object();
2142-
bridge->setObjectExpando(
2143-
runtime, native, kNativeApiAppearanceClassNameExpando,
2144-
makeString(runtime, symbol.runtimeName));
2170+
tagStaticAppearanceNativeResult(runtime, bridge, cls, native);
21452171
}
21462172
}
21472173
return result;
@@ -2647,6 +2673,11 @@ Value makeProtocolPropertyGetter(Runtime& runtime, NativeApiMember member,
26472673
throw JSError(
26482674
runtime, "Protocol property requires a native receiver.");
26492675
}
2676+
Value appearanceExpando = cachedAppearanceProxyPropertyValue(
2677+
runtime, bridge, receiver, member.name);
2678+
if (!appearanceExpando.isUndefined()) {
2679+
return appearanceExpando;
2680+
}
26502681
NativeApiMember getterMember = member;
26512682
if (auto selector = respondingPropertyGetterSelector(
26522683
receiver, member.name, member.selectorName)) {
@@ -2692,9 +2723,12 @@ throw JSError(
26922723
NativeApiMember setterMember = member;
26932724
setterMember.selectorName = member.setterSelectorName;
26942725
setterMember.signatureOffset = member.setterSignatureOffset;
2695-
return callObjCSelector(runtime, bridge, receiver, receiverIsClass,
2696-
setterMember.selectorName, &setterMember,
2697-
args, 1);
2726+
Value result = callObjCSelector(
2727+
runtime, bridge, receiver, receiverIsClass,
2728+
setterMember.selectorName, &setterMember, args, 1);
2729+
cacheAppearanceProxyPropertyValue(runtime, bridge, receiver,
2730+
member.name, args[0]);
2731+
return result;
26982732
});
26992733
}
27002734

PROGRESS.md

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

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

20+
### 2026-06-29 09:38 EDT - cached protocol-backed UIAppearance accessors
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 `28373879852` on `cb4fc1a9` kept builds and macOS tests
27+
green, but still failed iOS `ApiTests.js Appearance` only:
28+
`UILabel.appearance().textColor` read back as `undefined`.
29+
- The metadata-backed setter cache was still not enough. The remaining path
30+
is consistent with UIAppearance properties resolving through
31+
protocol-defined accessors, bypassing the object-host get/set cache.
32+
- Changes:
33+
- Added shared UIAppearance cache helpers for target-class-scoped property
34+
values.
35+
- Protocol property setters now cache UIAppearance values after successful
36+
native setter calls.
37+
- Protocol property getters now read the UIAppearance cache before falling
38+
through to native getters.
39+
- Static `appearance*` result tagging now prefers UIKit's exact
40+
`<Customizable class: ...>` proxy description for known appearance selector
41+
returns, then falls back to the dispatch class.
42+
- Verification:
43+
- Runtime RN JS tests passed:
44+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
45+
- `npm run check:ffi-boundaries` passed.
46+
- `git diff --check` passed.
47+
- `npm run build:macos-cli` passed.
48+
- Still next:
49+
- Commit/push the protocol-accessor cache fix, watch fresh PR CI, then
50+
return to the dedicated simulator-only RNS parity sweep if CI is green.
51+
2052
### 2026-06-29 09:00 EDT - cached metadata-backed UIAppearance setters
2153

2254
- Goal:

0 commit comments

Comments
 (0)