ITMS-90338: Non-public API usage, and why it is often a false positive
ITMS-90338 is a name-matching result, not a behaviour finding. Apple's pipeline reads the symbol table and Objective-C selector names in your binary and compares them against a list of private Apple APIs. Anything that matches gets flagged, whether or not it calls anything private.
That distinction is the whole page. It explains why the error names symbols you have never heard of, in frameworks you did not write, and occasionally fires on a binary that uploaded cleanly last week.
The three flavours
The code arrives in three shapes and the wording tells you what was matched:
Symbols. "The app references non-public symbols in Frameworks/libksygpulive.framework/libksygpulive: _CMTimebaseCreateWithMasterClock." A C-level symbol, named with its leading underscore, inside a named binary.
Selectors. "The app references non-public selectors in [YourProject]: callWithArguments:, estimatedProgress, frameInfo, getVersion, initWithFrame:configuration:, isMainFrame, navigationDelegate, setProcessPool:, toDouble, toString." Objective-C method names, and note that this one names your project rather than a framework.
Classes. "The app contains or inherits from non-public classes in Project: ADBannerView."
All three end with the same boilerplate, which is more useful than it looks:
"If method names in your source code match the private Apple APIs listed above, altering your method names will help prevent this app from being flagged in future submissions. In addition, note that one or more of the above APIs may be located in a static library that was included with your app. If so, they must be removed."
Apple is telling you outright that a name collision is enough to trigger it. That is an admission that the check produces false positives.
Read the path first
Before anything else, work out whose code was flagged.
A path under Frameworks/ names a third-party binary. Frameworks/libksygpulive.framework/libksygpulive, Frameworks/Flutter.framework/Flutter, Frameworks/OpenSSL.framework/OpenSSL. This is a dependency problem. Nothing you change in your own source will help.
Your project name, with a list of selectors means the matched names are in code compiled into your app binary. That includes your source, but also any static library or CocoaPod linked statically rather than embedded as a framework.
Skipping this step is how people spend an afternoon renaming their own methods to fix an SDK.
The causes, most common first
A third-party SDK containing private symbols. Video, streaming, analytics and payment SDKs are the usual offenders, particularly ones distributed as prebuilt static libraries. The SDK author called something private, or shipped an old build against an old SDK. You cannot fix this from your side beyond updating or removing the dependency.
Coincidental selector names. The WebKit-shaped list above is a giveaway: a JavaScript bridge that implements its own frameInfo, isMainFrame, navigationDelegate, toString and toDouble will collide with private WebKit selector names. So will a generic getVersion. None of these call anything Apple owns. They just have the same names.
A deprecated Apple framework. ADBannerView came from Apple's own iAd, which Apple later retired. Once a framework is withdrawn, its classes become private by definition, so linking against it produces this error even though the code was legal when you wrote it.
Weak-linking a framework newer than your deployment target. Reports of PencilKit triggering this on projects targeting below iOS 13 exist across several weak-linking approaches. If you support an older iOS than a framework requires, the pipeline can read the reference as private.
Accelerate and BLAS symbols. Libraries such as armadillo link against BLAS routines (ddot, dgemm, dgemv, dsyrk). Those are inside Accelerate.framework and are not public. Compiling and linking succeeds; upload does not.
A beta Xcode. Building with a beta toolchain has produced this error more than once, including a well-reported run of Flutter projects built with Xcode 15 betas. The fix in those cases was to rebuild with the current release Xcode, nothing more.
Apple's side. There have been episodes where builds that were previously accepted, unchanged in any relevant way, started failing this check, and started passing again later. In one widely reported case the answer that resolved it was to upload the same build repeatedly until it went through.
That last cause is uncomfortable to put in writing, but leaving it out would be dishonest. What it does not license is re-uploading forever. Two failures on two builds means read the path.
Finding it yourself
The error names the symbol. Use it.
For a symbol in a named framework, confirm it is really there:
nm -u "MyApp.app/Frameworks/SomeSDK.framework/SomeSDK" | grep _CMTimebaseCreateWithMasterClock
For selectors flagged against your own project, search the whole dependency tree rather than just your source:
grep -rn "setProcessPool\|isMainFrame\|frameInfo" . --include=*.h --include=*.m --include=*.swift
And for prebuilt binaries where a source search finds nothing, grep -rl reads binaries too and reports the file rather than the match:
grep -rl setProcessPool Pods/ Carthage/ node_modules/ 2>/dev/null
Or against the built bundle, which covers the main binary and every embedded framework at once:
grep -rl setProcessPool "MyApp.app"
The point of all of these is the same: turn "Apple says something in my app is private" into "this exact file contains this exact name".
Fixing it, by cause
- Your own method names collided. Rename them. Apple's message explicitly recommends this. Prefix them:
myAppGetVersionrather thangetVersion. Unglamorous and completely effective. - A third-party framework. Update to the latest version first, because SDK authors fix these. If there is no fixed version, ask the vendor and, failing that, remove the SDK. There is no workaround that keeps a static library containing private symbols.
- A retired Apple framework. Remove it. There is no supported version of shipping against a withdrawn framework.
- Weak-linked newer framework. Raise your deployment target to the framework's minimum, or drop the framework.
- BLAS or Accelerate symbols. Link a public alternative, or build the library so it does not reference the private routines directly.
- Beta toolchain. Rebuild with the release Xcode.
- No cause found and the path names nothing of yours. Bump the build number and upload again. Once.
The honest limit
Apple does not publish the private API list. There is no pre-flight check you can run locally that tells you whether your binary will trip this, because the reference list is not available to you.
That means you cannot fully prevent ITMS-90338. You can reduce your exposure by avoiding prebuilt static libraries, keeping dependencies current, prefixing your own method names, and building with a release toolchain. You cannot verify in advance, and any tool claiming to is comparing against a guessed list.
Warning or error?
Both, depending on the case. The email version that says "Please correct the following issues, then upload again" is blocking: the build will not appear. The version that says "Your delivery was successful, but you may wish to correct the following issues" is not.
There is a third outcome worth knowing about. Builds have been reported as appearing in TestFlight and then disappearing after this check ran late. If a build vanished from TestFlight without explanation, check the email account attached to the developer account role that receives delivery notices, because the ITMS text is the only record you will get.