Skip to content

Instantly share code, notes, and snippets.

@bfahey
Created April 5, 2024 16:56
Show Gist options
  • Save bfahey/b822f1227c173b272f1958741b19373a to your computer and use it in GitHub Desktop.
Save bfahey/b822f1227c173b272f1958741b19373a to your computer and use it in GitHub Desktop.
Debug your app with Apple's private UIDebuggingInformationOverlay. Supports iOS 13-17+.
import UIKit
import ObjectiveC.runtime
/// An enum that displays UIKitCore's `UIDebuggingInformationOverlay`.
enum DebuggingOverlay {
/// Shows the `UIDebuggingInformationOverlay`.
@available(iOS 13, *)
static func show() {
struct Once {
/// Replace the overlay's init method with UIWindow's.
static let run: Void = {
let initSelector = NSSelectorFromString("init")
let debugOverlayClass = NSClassFromString("UIDebuggingInformationOverlay") as? UIWindow.Type
guard let newInitMethod = class_getInstanceMethod(UIWindow.self, initSelector) else {
fatalError("UIWindow.init was nil")
}
let newImplementation = method_getImplementation(newInitMethod)
class_replaceMethod(debugOverlayClass, initSelector, newImplementation, nil)
}()
}
Once.run
// `UIDebuggingInformationOverlay.prepareDebuggingOverlay()` calls the _UIGetDebuggingOverlayEnabled
// which checks if the device is internal to Apple. Bypass the check by invoking a tap
// gesture.
let tapGesture = UITapGestureRecognizer()
tapGesture.state = .ended
let handlerClass = NSClassFromString("UIDebuggingInformationOverlayInvokeGestureHandler") as! NSObject.Type
let handler = handlerClass.perform(NSSelectorFromString("mainHandler")).takeUnretainedValue()
let _ = handler.perform(NSSelectorFromString("_handleActivationGesture:"), with: tapGesture)
// If the app supports multiple scenes the debug menu needs to be added as a subview.
guard
Bundle.main.object(forInfoDictionaryKey: "UIApplicationSceneManifest") != nil,
let debugOverlayClass = NSClassFromString("UIDebuggingInformationOverlay") as? UIWindow.Type,
let debugOverlayView = debugOverlayClass.perform(NSSelectorFromString("overlay")).takeUnretainedValue() as? UIView,
let keyWindow = getKeyWindow()
else {
return
}
debugOverlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
keyWindow.addSubview(debugOverlayView)
}
/// Returns the app's key window.
private static func getKeyWindow() -> UIWindow? {
UIApplication.shared.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.last { $0.isKeyWindow }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment