Created
April 5, 2024 16:56
-
-
Save bfahey/b822f1227c173b272f1958741b19373a to your computer and use it in GitHub Desktop.
Debug your app with Apple's private UIDebuggingInformationOverlay. Supports iOS 13-17+.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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