Skip to main content

Apple Private API Reference

How to discover and call Apple's private APIs for debugging and tooling — what they are, how to find them, and why you must never ship them in a production app.

Do not ship private API calls in App Store builds. Apple’s static analyzer scans submitted binaries for private symbol references and will reject the app. Use these techniques in debug builds, internal tooling, and during investigation only.


What Are Private APIs?

Every Apple framework ships with two layers: the public API documented at developer.apple.com, and a much larger private layer that Apple uses internally. Private APIs are real, working code — they power SpringBoard, Accessibility, the simulator, Instruments, and most of the system UI you use every day. Apple just doesn’t guarantee their stability, safety, or continued existence across OS versions.

From an iOS engineering perspective, private APIs are most useful for:

  • Understanding how a system framework actually works so you can work with it, not against it
  • Building internal debugging overlays and tooling
  • Reverse-engineering a system behavior to file a meaningful radar
  • Unblocking yourself when a public API is broken or missing

Discovery Tools

Before you can call a private API, you need to find it. These are the standard tools.

class-dump

Detail
WhatReconstructs Objective-C class and method headers from a compiled Mach-O binary
WhyGives you a browseable .h file for any framework — private methods, properties, and ivars included
HowRun against the framework binary inside the simulator runtime or a decrypted device binary
# Dump the SpringBoard binary (simulator path)
class-dump \
  /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/SpringBoard.app/SpringBoard \
  -o ~/dumps/SpringBoard/

# Dump a specific framework
class-dump \
  /Applications/Xcode.app/.../RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit \
  -o ~/dumps/UIKit/

The output is a folder of .h files. Search them like any code:

grep -r "screenshot" ~/dumps/UIKit/ -l
grep -r "_statusBar" ~/dumps/SpringBoard/ | head -20

Hopper Disassembler / IDA Pro

Detail
WhatDisassemblers that turn compiled ARM64 machine code back into readable pseudocode
WhyWhen class-dump gives you the signature but not the behaviour — Hopper shows you the implementation
HowDrag the binary into Hopper, let it analyse, then search by symbol name in the labels panel

Useful workflow: find the method in class-dump, search it in Hopper, read the pseudocode to understand what parameters it actually validates and what it returns. This is how you find out whether a private initialiser will crash if you pass nil for an undocumented parameter.


LLDB at Runtime

Detail
WhatThe debugger attached to a running process — can inspect live objects, call methods, and print the full class hierarchy
WhyThe fastest way to explore a live object you already have a pointer to
HowPause in a breakpoint or use lldb to attach to a running simulator process
# Print all methods on a class (including private)
expression -l objc -O -- [UIWindow _methods]

# Print the Objective-C class hierarchy of an object
expression -l objc -O -- [(id)0x600003a12340 class]

# List all subviews recursively
expression -l objc -O -- [[[UIWindow keyWindow] rootViewController] view]

# Call a private method directly
expression -l objc -- (void)[[UIApplication sharedApplication] _reloadKeyboardForInputView:nil]

# Print all registered notification names
expression -l objc -O -- [[NSNotificationCenter defaultCenter] _addObserver:nil selector:nil name:nil object:nil]

Runtime Header Dumps (online)

Several open-source projects continuously dump and publish the private headers for every iOS/macOS release. Search for “iOS private headers GitHub” — the most complete sets cover UIKit, Foundation, SpringBoard, BackBoardServices, and more. These save you from running class-dump yourself.


Calling Private APIs

Once you’ve found the method, there are several ways to call it depending on the language boundary.

Objective-C performSelector:

The simplest approach for Objective-C methods. Bypasses the compiler’s type checking entirely.

Detail
WhatSends a message to an object using a runtime selector string
WhyNo import needed — works on any object if the method exists at runtime
RiskNo type safety; wrong argument type or count crashes immediately
// Call a private UIApplication method
let app = UIApplication.shared
let selector = NSSelectorFromString("_reloadKeyboardForInputView:")
if app.responds(to: selector) {
    app.perform(selector, with: nil)
}

// Call a private UIView method with a return value
let window = UIApplication.shared.keyWindow
let sel = NSSelectorFromString("_accessibilityIdentifier")
if let result = window?.perform(sel)?.takeUnretainedValue() as? String {
    print(result)
}

Always guard with responds(to:) — private APIs can disappear in any OS update.


Objective-C Runtime Direct Call (objc_msgSend)

For methods with non-id return types or multiple arguments, perform won’t work. Cast objc_msgSend to the correct function signature.

Detail
WhatDirect call to the Obj-C message dispatch function with full type control
WhyHandles CGRect, struct, BOOL, and multi-argument private methods that perform can’t express
RiskYou must get the type signature exactly right; a mismatch corrupts the stack
import ObjectiveC

// Example: call -[UIView _frameInWindowCoordinates] which returns CGRect
typealias CGRectIMP = @convention(c) (AnyObject, Selector) -> CGRect
let sel = NSSelectorFromString("_frameInWindowCoordinates")
let imp = unsafeBitCast(
    class_getMethodImplementation(UIView.self, sel),
    to: CGRectIMP.self
)
let rect = imp(someView, sel)
print(rect)

// Example: call a two-argument private method
typealias TwoArgIMP = @convention(c) (AnyObject, Selector, AnyObject, Bool) -> Void
let sel2 = NSSelectorFromString("_setNeedsDisplayForceUpdate:")
let imp2 = unsafeBitCast(
    class_getMethodImplementation(UIView.self, sel2),
    to: TwoArgIMP.self
)
imp2(someView, sel2, NSNumber(value: 1), true)

dlopen / dlsym — Loading Private Frameworks

Some private frameworks aren’t linked at all — you have to load them at runtime.

Detail
WhatOpens a dynamic library at runtime and resolves a function pointer by symbol name
WhyLets you call C functions and Objective-C classes from frameworks Apple doesn’t expose at link time
RiskApp Store rejection is near-certain if the framework is private; fine for internal tools
import Darwin

// Load a private framework
guard let handle = dlopen(
    "/System/Library/PrivateFrameworks/MobileGestalt.framework/MobileGestalt",
    RTLD_NOW
) else {
    print("Failed to load: \(String(cString: dlerror()))")
    return
}
defer { dlclose(handle) }

// Resolve a C function symbol
typealias MGCopyAnswerFn = @convention(c) (CFString) -> CFTypeRef?
guard let sym = dlsym(handle, "MGCopyAnswer") else { return }
let MGCopyAnswer = unsafeBitCast(sym, to: MGCopyAnswerFn.self)

// Call it — MobileGestalt answers device capability questions
let answer = MGCopyAnswer("DeviceColor" as CFString)
print(answer ?? "nil")

Declaring a Private Header Stub

The cleanest compile-time approach: declare the private interface yourself so the compiler accepts the call without any runtime gymnastics.

Detail
WhatAn @interface category that re-declares private methods on an existing class
WhyFull type safety, autocompletion, and no perform ceremony — reads like normal code
RiskRequires a separate file never imported in release builds; easy to accidentally leave in
// PrivateUIKit.h  — NEVER include in a target that ships to the App Store
@import UIKit;

@interface UIApplication (Private)
- (void)_reloadKeyboardForInputView:(UIView *)view;
- (UIView *)_accessibilityView;
@end

@interface UIWindow (Private)
- (UIView *)_hitTest:(CGPoint)point withEvent:(UIEvent *)event;
@end
// Use it from Swift in a debug-only file
#if DEBUG
import UIKit
// include the .h via bridging header, then:
UIApplication.shared._reloadKeyboardForInputView(nil)
#endif

Wrap every import and call site in #if DEBUG so the symbols never reach a release binary.


Guarding Against OS Version Changes

Private APIs break silently across OS updates. Never call one unconditionally.

func callPrivateMethodIfAvailable(on view: UIView) {
    let sel = NSSelectorFromString("_privateLayoutPass")
    guard view.responds(to: sel) else {
        // Method removed in this OS version — skip or use fallback
        return
    }
    view.perform(sel)
}

For C symbols via dlsym, check for nil before calling:

guard let sym = dlsym(handle, "MGCopyAnswer") else {
    print("Symbol not found — API removed or renamed")
    return
}

Avoiding App Store Rejection

Apple’s binary submission pipeline runs static analysis and symbol scanning. These are the common trip wires.

RiskHow Apple Detects ItMitigation
Direct symbol referenceSymbol table scan of your Mach-OWrap in dlsym with a string — but this is still a policy violation
NSSelectorFromString with a private nameStatic string pattern matchingNo safe mitigation — don’t ship it
@interface category stub left in release buildLinker leaves symbol references#if DEBUG guard on all stubs and imports
Private framework linked at build timeLink map / load command inspectionUse dlopen at runtime — only for internal/enterprise builds

The only safe rule: strip all private API code from your App Store target at the preprocessor level.

#if DEBUG
// All private API calls live here
#endif

Use a separate scheme, a separate Xcode target, or a build flag (-D INTERNAL_BUILD) for tools that need private APIs in production distribution (e.g. an enterprise debug app).


Practical Debugging Use Cases

TaskPrivate API Approach
Print the full view hierarchy as a tree[view recursiveDescription] via perform
Find which view is first responder[UIApplication.shared _firstResponderForView:window]
Force a layout pass and inspect frame changes_layoutSubviewsIfNeeded + _frameInWindowCoordinates
Read the current GPU frame timeCADisplayLink + private CALayer timing properties
Inspect which auto-layout constraint is ambiguous[view _autolayoutTrace] via perform
Check device hardware capabilitiesMGCopyAnswer from MobileGestalt
Trigger a screenshot programmatically (internal tools)UIScreen private capture methods
// Print view hierarchy (the most useful one-liner in debugging)
#if DEBUG
extension UIView {
    func debugHierarchy() {
        let sel = NSSelectorFromString("recursiveDescription")
        if self.responds(to: sel),
           let desc = self.perform(sel)?.takeUnretainedValue() as? String {
            print(desc)
        }
    }
}
#endif

Quick Reference

TechniqueLanguageUse When
performs(selector:) + performSwift/ObjCSimple id-returning ObjC methods
objc_msgSend castSwift/ObjCStruct returns or multi-argument ObjC methods
Private header stubObjCYou want compiler type-checking; debug target only
dlopen + dlsymSwift/ObjCPrivate C functions or unlinked frameworks
LLDB expressionLLDBOne-off runtime exploration; nothing ships
class-dumpShellDiscovery — finding what methods exist
HopperGUIDiscovery — understanding what a method does