ITMS-90338: rezolvă „Non-public API usage” cu Expo
De Mehdi Azizi
Publicat pe , 9 min.
ITMS-90338: build-ul tău conține o metodă, o funcție sau o clasă pe care Apple o consideră privată. Găsește dependența, actualizeaz-o și refă build-ul.
ITMS-90338 este o verificare automată pe care Apple o face pe build-ul tău. Îți spune că binarul tău face referire la API-uri pe care Apple le consideră private: metode Objective-C, funcții sau clase. Soluția: identifici fișierul menționat în e-mail, urmărești numele până la dependența care îl conține, o actualizezi sau îți redenumești metoda, apoi refaci build-ul.
Cu Expo, numele vine aproape mereu dintr-o bibliotecă nativă, rar din JavaScript-ul tău. Pe 15 octombrie 2026, am rulat comenzile de analiză din acest ghid pe un build de simulator al Solys, alarma mea cu mini-jocuri (Expo SDK 57), și pe SDK-ul iOS din Xcode 26.2. Uite cum citești e-mailul, cum găsești vinovatul și cum recunoști un fals pozitiv.
Ce înseamnă eroarea ITMS-90338?
Mesajul are titlul „ITMS-90338: Non-public API usage”. Există în trei variante, în funcție de ce a găsit Apple în binarul tău:
- „references non-public selectors”: nume de metode Objective-C, cum ar fi
boldFontOfSize:; - „references non-public symbols”: funcții, scrise cu underscore în față, cum ar fi
__dyld_get_shared_cache_range; - „contains or inherits from non-public classes”: un nume de clasă.
Fiecare variantă dă o cale, apoi lista de nume. Textul adaugă două sfaturi: să-ți redenumești metodele dacă numele lor coincide cu un API privat și să scoți biblioteca statică ce le conține. Un e-mail din septembrie 2026 propune și xcrun swift-demangle ca să faci lizibile numele Swift codificate.
Regula din spatele acestei verificări este guideline 2.5.1: o aplicație poate folosi doar API-uri publice. Contractul de licență al Apple Developer Program repetă asta la articolul 3.3.1: niciun apel la un API privat. Review-ul uman al acestei reguli e detaliat în ghidul despre guideline 2.5.1. Aici ne ocupăm de eroarea de upload.
Ca la celelalte erori ITMS, nimeni nu ți-a deschis aplicația: Apple a analizat binarul în timpul procesării build-ului. Apple nu publică nicio listă cu API-urile private: numele din e-mailul tău sunt singura listă de încredere.
Citește calea: un SDK sau executabilul tău?
Calea scrisă înaintea listei îți spune în ce fișier să cauți.
Frameworks/Nom.framework/Nom: numele se află într-un framework dinamic inclus, deci într-un SDK anume. Pe build-ul Solys, acest folder conține 14 framework-uri: Hermes, React Native, module Expo livrate deja compilate și biblioteca de imagini SDWebImage.- Doar numele aplicației tale: numele se află în executabilul principal. Cu Expo, acesta reunește codul tău nativ și pod-urile compilate din surse, legate static în mod implicit. Atunci orice bibliotecă nativă poate fi de vină.
Arborele: de la numele citat la soluție
Ia rândurile în ordine. Primul care se potrivește cu situația ta îți dă pista.
| Ce citești | Unde cauți | Soluție |
|---|---|---|
Nume cu $s sau _$s | xcrun swift-demangle | Reia arborele |
| Nu apare în IPA-ul tău | grep pe bundle | Retrimite un build |
| Nume public pe iOS | Header-ele SDK-ului iOS | Retrimite un build |
API_UNAVAILABLE(ios) | Header-ele SDK-ului iOS | Actualizează pachetul |
În Frameworks/X | SDK-ul care furnizează X | Actualizează SDK-ul |
| Selector, aplicația ta | Pod-uri și module native | Prefixează sau actualizează |
Simbol _nume, aplicația ta | nm -m, apoi pod-urile | Înlocuiește apelul |
| Clasă, aplicația ta | Clasele tale și pod-urile | Prefixează clasa |
Rândurile „Retrimite un build” semnalează un fals pozitiv, tratat mai jos.
Pasul 1: deschide build-ul trimis
Lucrează pe fișierul trimis efectiv la Apple, nu pe proiectul tău. Descarcă IPA-ul de pe pagina build-ului de pe expo.dev, apoi rulează pe Mac:
unzip -q MyApp.ipa -d ipa
ls ipa/Payload/MyApp.app/Frameworks
otool -L ipa/Payload/MyApp.app/MyApp | grep PrivateFrameworks
A doua linie listează framework-urile incluse. A treia caută o legătură directă către un framework privat Apple: nu trebuie să afișeze nimic. Pe Solys, otool -L nu găsește niciun framework privat, nici în executabil, nici în cele 14 framework-uri.
Pasul 2: găsește fișierul care conține numele
Începe larg. Caută numele citat, fără underscore-urile de la început, în tot bundle-ul:
grep -rl "dyld_get_shared_cache_range" ipa/Payload/MyApp.app
Comanda listează fișierele care conțin textul. Apoi restrânge în funcție de tipul numelui (selector, simbol, clasă), pe fișierul găsit, notat Binaire:
strings -a Binaire | grep -x "boldFontOfSize:"
nm -m Binaire | grep "_dyld_get_shared_cache_range"
nm -m Binaire | grep '_OBJC_CLASS_$_ClassName'
Pentru un selector, folosește strings, nu nm. Pe framework-ul React din Solys, nm afișează metode împreună cu clasa lor, cum ar fi -[RCTRootView initWithBridge:moduleName:initialProperties:], niciodată selectorul initWithBridge: singur. strings îl găsește.
Pentru un simbol, citește finalul liniei. Pe Solys, nm -m afișează de exemplu (undefined) external __dyld_get_image_name (from libSystem): binarul apelează funcția fără s-o conțină, iar libSystem o furnizează. Funcția asta e publică, după cum arată pasul următor.
Pasul 3: verifică dacă numele e cu adevărat privat
Header-ele SDK-ului iOS descriu API-urile publice. O căutare de câteva secunde îți spune dacă numele citat apare acolo și pe ce platforme. Pentru un simbol, scoate întâi underscore-ul adăugat la compilare: _SCDynamicStoreCreate devine SCDynamicStoreCreate.
SDK=$(xcrun --sdk iphoneos --show-sdk-path)
grep -rn "SCDynamicStoreCreate" "$SDK/usr/include" "$SDK"/System/Library/Frameworks/*/Headers
Pe 15 octombrie 2026, cu SDK-ul iOS 26.2, iată ce dau patru nume luate din e-mailuri ITMS-90338 reale:
| Nume citat | În SDK-ul iOS | Interpretare |
|---|---|---|
| _dyld_get_shared_cache_range | Absent | API privat |
| boldFontOfSize: | Absent | Privat sau coliziune |
| SCDynamicStoreCreate | API_UNAVAILABLE(ios) | Doar macOS |
| isMainFrame | Proprietate WebKit | Public |
Un nume prezent și disponibil pe iOS indică un fals pozitiv. Un nume absent indică un API privat sau o coliziune. Două limite: un API public doar în Swift nu apare în aceste header-e, iar pentru un accesor ca setDelegate:, caută proprietatea delegate.
Pasul 4: ajungi până la pachet
Dacă în cale apare un framework, numele lui te duce la pod, apoi la pachetul npm:
grep -n "NomDuFramework" ios/Podfile.lock
npm explain nom-du-paquet
npm explain îți spune ce dependență l-a instalat, dacă nu l-ai adăugat niciodată tu.
Dacă în cale apare executabilul, caută numele în sursele native, apoi în pod-uri:
grep -rl --include='*.m' --include='*.mm' --include='*.swift' --include='*.h' \
"boldFontOfSize" node_modules modules
grep -rl "boldFontOfSize" ios/Pods
Adaptează lista de foldere: modules e cel al modulelor Expo locale. Folderul ios/Pods apare după npx expo prebuild -p ios, care instalează și pod-urile pe Mac. Atenție: comanda poate recrea folderul ios. Dacă l-ai modificat de mână, lucrează pe o copie.
Căutarea în ios/Pods contează la fel de mult ca cea din surse. Pe Solys, acest folder conține 15 framework-uri livrate deja compilate, în format .xcframework: module Expo, Hermes, React Native și SDK-uri terțe. O căutare limitată la fișierele sursă le-ar rata codul.
Cauzele tipice
Un SDK terț, prea vechi sau prea nou. În septembrie 2026, un dezvoltator Flutter a raportat o respingere cauzată de un SDK de securitate: versiunea 8.2.3 apela __dyld_get_shared_cache_range. Versiunea 8.2.2 trecea, iar editorul a recomandat revenirea la ea până la un fix.
Un modul nativ care apelează un API nedocumentat. Propriul tău modul, un plugin sau o bibliotecă mică poate folosi o metodă internă din UIKit găsită pe un forum. Verificarea o detectează după nume.
O coliziune de nume. Metoda sau clasa ta poartă, din întâmplare, numele unui API privat Apple. Mesajul prevede asta și îți cere s-o redenumești. Apple recomandă să prefixezi metodele adăugate la clasele sale, de exemplu cu trei litere mici urmate de un underscore.
Un API public pe macOS, dar nu pe iOS. În 2024, o aplicație .NET MAUI a fost respinsă pentru _SCDynamicStoreCreate. Apple documentează această funcție, dar doar pentru macOS, iar header-ul din SDK-ul iOS o marchează API_UNAVAILABLE(ios, tvos, watchos).
Rezolvă fără să ocolești
- Actualizează dependența. Pentru pachetele urmărite de Expo,
npx expo install --checkle semnalează pe cele care nu se potrivesc cu SDK-ul tău, conform documentației Expo CLI. Pentru celelalte, citește changelog-ul. - Revino la versiunea anterioară dacă cea nouă a introdus numele și anunță editorul.
- Înlocuiește sau elimină o bibliotecă abandonată.
- Redenumește metoda sau clasa ta, cu un prefix.
Nu ascunde numele. Să spargi un string sau să apelezi metoda pe ocolite ascunde utilizarea fără s-o rezolve. Rămâne interzisă de regula 2.5.1 și de contract, și atunci contul tău e cel expus.
Apoi refă build-ul cu un nou număr de build. Un update la distanță nu e suficient: numele e în codul nativ compilat. Înainte de upload, rulează din nou pasul 2 pe noul IPA. Numele nu trebuie să mai apară.
Când e un fals pozitiv
Verificarea Apple mai greșește. Pe 6 iunie 2022, aplicații React Native și Flutter au primit ITMS-90338 pentru nume publice din WebKit, cum ar fi setNavigationDelegate: sau isMainFrame, inclusiv pe update-uri aproape neschimbate. A doua zi, un inginer Apple a scris că problema fusese rezolvată în partea de validare și a invitat lumea să retrimită build-urile. Thread-ul e păstrat în repo-ul GitHub al Expo.
Trei indicii luate împreună indică un fals pozitiv:
- build-ul seamănă cu ultimul build acceptat;
- pasul 2 nu găsește numele, sau pasul 3 îl arată public pe iOS;
- alți dezvoltatori raportează aceeași eroare în aceeași zi pe forumurile Apple.
În cazul ăsta, uită-te la starea serviciilor Apple, apoi retrimite un build cu un număr nou. Dacă eroarea persistă, scrie la suportul pentru dezvoltatori Apple cu e-mailul complet. O coliziune de nume, în schimb, nu e un fals pozitiv în ochii Apple: redenumește.
Înainte de următorul upload
Și celelalte erori ITMS au ghidul lor: ITMS-90725 pentru versiunea de Xcode, ITMS-91053 pentru privacy manifest și ITMS-90034 pentru semnătură. Odată ce build-ul e acceptat, instalează-l cu TestFlight înainte de review.
Respingerile date de un reviewer cer mai multă distanță: pașii sunt pe pagina aplicație respinsă de Apple, iar motivele cele mai frecvente sunt în de ce respinge Apple o aplicație.
Ca să-ți verifici proiectul înainte de upload, Review Kit reunește checklist-ul meu și skill-uri Claude Code care îți recitesc proiectul. Dacă upload-ul ți se blochează și vrei o privire umană, rezervă o oră cu mine: reluăm proiectul tău pe video, pe ecranul tău. Iar dacă îți pregătești prima lansare, fă-ți diagnosticul gratuit: îți pune pașii în ordine și îți trimite checklist-ul meu anti-respingere Apple.
Întrebări frecvente
Este ITMS-90338 o respingere de la App Review?
Nu. E o verificare automată făcută în timpul procesării build-ului, înaintea oricărui review uman. Build-ul respins nu ajunge nici în TestFlight, nici în review. Un API privat descoperit mai târziu de un reviewer ține, în schimb, de guideline 2.5.1.
Pot rezolva ITMS-90338 cu un update la distanță?
Nu. Numele citate sunt în codul nativ compilat al binarului. Un update la distanță schimbă doar JavaScript-ul și resursele. Corectează dependența, refă build-ul, apoi retrimite-l.
De ce citează Apple metode pe care nu le-am scris niciodată?
Pentru că vin dintr-o bibliotecă inclusă sau dintr-o coliziune: o dependență declară o metodă care poartă numele unui API privat. Apple citește numele prezente în binar, nu codul tău sursă. Ajungi până la pachet cu grep și npm explain.
Cum aflu dacă un API e public pe iOS?
Caută-i numele în header-ele SDK-ului iOS, a căror cale ți-o dă xcrun --sdk iphoneos --show-sdk-path, apoi citește-i disponibilitatea. Pagina de documentație Apple o indică și ea. O funcție documentată doar pentru macOS nu e publică pe iPhone.
Trebuie să schimb numărul de build după corectare?
Da. Lasă EAS să-l incrementeze: un număr deja trimis pentru aceeași versiune poate fi respins cu ITMS-90189.

