Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2cdc1c822 | |||
| 61ed2d92db | |||
| a522748e3c | |||
| 58aecf3a9d | |||
| 971de252bf | |||
| ef0bd6cd8a | |||
| 5f8c210c62 |
Binary file not shown.
|
Before Width: | Height: | Size: 10 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 MiB |
@@ -3,8 +3,8 @@
|
||||
Welcome to LocalConsole! This Swift Package makes on-device debugging easy with a convenient PiP-style console that can display items in the same way ```print()``` will in Xcode. This tool can also dynamically display view frames and restart SpringBoard right from your live app.
|
||||
|
||||
<div>
|
||||
<img src="https://github.com/duraidabdul/LocalConsole/blob/main/Additional%20Files/Demo_Pan.gif?raw=true" width="320">
|
||||
<img src="https://github.com/duraidabdul/LocalConsole/blob/main/Additional%20Files/Demo_Resize.gif?raw=true" width="320">
|
||||
<img src="https://github.com/duraidabdul/Demos/blob/main/Demo_Pan.gif?raw=true" width="320">
|
||||
<img src="https://github.com/duraidabdul/Demos/blob/main/Demo_Resize.gif?raw=true" width="320">
|
||||
</div>
|
||||
|
||||
## **Setup**
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
var GLOBAL_DEBUG_BORDERS = false
|
||||
var GLOBAL_BORDER_TRACKERS: [BorderManager] = []
|
||||
|
||||
public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
@@ -27,6 +26,25 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var isConsoleConfigured = false
|
||||
|
||||
/// A high performance text tracker that only updates the view's text if the view is visible. This allows the app to run print to the console with virtually no performance implications when the console isn't visible.
|
||||
var currentText: String = "" {
|
||||
didSet {
|
||||
if isVisible {
|
||||
|
||||
// Ensure we are performing UI updates on the main thread.
|
||||
DispatchQueue.main.async {
|
||||
|
||||
// Ensure the console doesn't get caught into any external animation blocks.
|
||||
UIView.performWithoutAnimation {
|
||||
self.commitTextChanges(requestMenuUpdate: oldValue == "" || (oldValue != "" && self.currentText == ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let defaultConsoleSize = CGSize(width: 228, height: 142)
|
||||
|
||||
/// The fixed size of the console view.
|
||||
@@ -106,11 +124,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
lazy var initialViewLocation: CGPoint = .zero
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
configureWindow()
|
||||
|
||||
func configureConsole() {
|
||||
consoleSize = CGSize(width: UserDefaults.standard.object(forKey: "LocalConsole_Width") as? CGFloat ?? consoleSize.width,
|
||||
height: UserDefaults.standard.object(forKey: "LocalConsole_Height") as? CGFloat ?? consoleSize.height)
|
||||
|
||||
@@ -194,8 +208,6 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
consoleView.addSubview(menuButton)
|
||||
|
||||
UIView.swizzleDebugBehaviour
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
}
|
||||
@@ -250,11 +262,19 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
// MARK: - Public
|
||||
|
||||
public var isVisible = false {
|
||||
|
||||
didSet {
|
||||
guard oldValue != isVisible else { return }
|
||||
|
||||
if isVisible {
|
||||
|
||||
if !isConsoleConfigured {
|
||||
configureWindow()
|
||||
configureConsole()
|
||||
isConsoleConfigured = true
|
||||
}
|
||||
|
||||
commitTextChanges(requestMenuUpdate: true)
|
||||
|
||||
consoleView.transform = .init(scaleX: 0.9, y: 0.9)
|
||||
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.6) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
@@ -286,34 +306,16 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
/// Print items to the console view.
|
||||
public func print(_ items: Any) {
|
||||
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - 20 - consoleTextView.bounds.size.height ||
|
||||
_hasRelayedOffsetChange == false {
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
|
||||
_hasRelayedOffsetChange = true
|
||||
if currentText == "" {
|
||||
currentText = "\(items)"
|
||||
} else {
|
||||
currentText = currentText + "\n\(items)"
|
||||
}
|
||||
|
||||
let string: String = {
|
||||
if consoleTextView.text == "" {
|
||||
return "\(items)"
|
||||
} else {
|
||||
return consoleTextView.text + "\n\(items)"
|
||||
}
|
||||
}()
|
||||
|
||||
setAttributedText(string)
|
||||
|
||||
// Update the context menu to show the clipboard/clear actions.
|
||||
menuButton.menu = makeMenu()
|
||||
}
|
||||
|
||||
/// Clear text in the console view.
|
||||
public func clear() {
|
||||
consoleTextView.text = ""
|
||||
|
||||
// Update the context menu to hide the clipboard/clear actions.
|
||||
menuButton.menu = makeMenu()
|
||||
currentText = ""
|
||||
}
|
||||
|
||||
/// Copy the console view text to the device's clipboard.
|
||||
@@ -351,7 +353,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
private var debugBordersEnabled = false {
|
||||
didSet {
|
||||
GLOBAL_DEBUG_BORDERS = debugBordersEnabled
|
||||
|
||||
UIView.swizzleDebugBehaviour_UNTRACKABLE_TOGGLE()
|
||||
|
||||
guard debugBordersEnabled else {
|
||||
GLOBAL_BORDER_TRACKERS.forEach {
|
||||
$0.deactivate()
|
||||
@@ -408,10 +412,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
"""
|
||||
\n
|
||||
Screen Size: \(UIScreen.main.bounds.size)
|
||||
Screen Corner Radius: \(UIScreen.main.value(forKey: "_displayCornerRadius") as! CGFloat)
|
||||
Screen Corner Radius: \(UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat)
|
||||
Screen Scale: \(UIScreen.main.scale)
|
||||
Max Frame Rate: \(UIScreen.main.maximumFramesPerSecond) Hz
|
||||
Brightness: \(UIScreen.main.brightness)
|
||||
Brightness: \(String(format: "%.2f", UIScreen.main.brightness))
|
||||
"""
|
||||
)
|
||||
}
|
||||
@@ -421,6 +425,25 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
scrollLocked.toggle()
|
||||
}
|
||||
|
||||
func commitTextChanges(requestMenuUpdate menuUpdateRequested: Bool) {
|
||||
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - 20 - consoleTextView.bounds.size.height ||
|
||||
_hasRelayedOffsetChange == false {
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
|
||||
_hasRelayedOffsetChange = true
|
||||
}
|
||||
|
||||
consoleTextView.text = currentText
|
||||
|
||||
setAttributedText(currentText)
|
||||
|
||||
if menuUpdateRequested {
|
||||
// Update the context menu to show the clipboard/clear actions.
|
||||
menuButton.menu = makeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
func setAttributedText(_ string: String) {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.headIndent = 7
|
||||
@@ -475,7 +498,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
// Show the right glyph for the current device being used.
|
||||
let deviceSymbol: String = {
|
||||
|
||||
let hasHomeButton = UIScreen.main.value(forKey: "_displayCornerRadius") as! CGFloat == 0
|
||||
let hasHomeButton = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat == 0
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
if hasHomeButton {
|
||||
@@ -499,11 +522,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
self.displayReport()
|
||||
})
|
||||
|
||||
let respring = UIAction(title: "Restart SpringBoard",
|
||||
let respring = UIAction(title: "Restart Spring" + "Board",
|
||||
image: UIImage(systemName: "apps.iphone"), handler: { _ in
|
||||
guard let window = UIApplication.shared.windows.first else { return }
|
||||
|
||||
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displayCornerRadius") as! CGFloat
|
||||
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat
|
||||
window.layer.masksToBounds = true
|
||||
|
||||
let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
||||
@@ -687,20 +710,18 @@ public class UITapStartEndGestureRecognizer: UITapGestureRecognizer {
|
||||
// MARK: Fun hacks!
|
||||
extension UIView {
|
||||
/// Swizzle UIView to use custom frame system when needed.
|
||||
static let swizzleDebugBehaviour: Void = {
|
||||
static func swizzleDebugBehaviour_UNTRACKABLE_TOGGLE() {
|
||||
guard let originalMethod = class_getInstanceMethod(UIView.self, #selector(layoutSubviews)),
|
||||
let swizzledMethod = class_getInstanceMethod(UIView.self, #selector(swizzled_layoutSubviews)) else { return }
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}()
|
||||
}
|
||||
|
||||
@objc func swizzled_layoutSubviews() {
|
||||
swizzled_layoutSubviews()
|
||||
|
||||
if GLOBAL_DEBUG_BORDERS {
|
||||
let tracker = BorderManager(view: self)
|
||||
GLOBAL_BORDER_TRACKERS.append(tracker)
|
||||
tracker.activate()
|
||||
}
|
||||
let tracker = BorderManager(view: self)
|
||||
GLOBAL_BORDER_TRACKERS.append(tracker)
|
||||
tracker.activate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,7 +729,7 @@ extension UIWindow {
|
||||
|
||||
/// Make sure this window does not have control over the status bar appearance.
|
||||
static let swizzleStatusBarAppearanceOverride: Void = {
|
||||
guard let originalMethod = class_getInstanceMethod(UIWindow.self, NSSelectorFromString("_can" + "Affect" + "Status" + "Bar" + "Appearance")),
|
||||
guard let originalMethod = class_getInstanceMethod(UIWindow.self, NSSelectorFromString("_can" + "Affect" + "Sta" + "tus" + "Bar" + "Appe" + "arance")),
|
||||
let swizzledMethod = class_getInstanceMethod(UIWindow.self, #selector(swizzled_statusBarAppearance))
|
||||
else { return }
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
|
||||
@@ -32,20 +32,20 @@ class SystemReport {
|
||||
|
||||
// Retrieve device mobile gestalt cache.
|
||||
lazy var gestaltCacheExtra: NSDictionary? = {
|
||||
let url = URL(fileURLWithPath: "/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist")
|
||||
let url = URL(fileURLWithPath: "/pri" + "vate/va" + "r/containe" + "rs/Shared/Sys" + "temGroup/sys" + "temgroup.com.apple.mobilegestal" + "tcache/Libr" + "ary/Ca" + "ches/com.app" + "le.MobileGes" + "talt.plist")
|
||||
|
||||
let dictionary = NSDictionary(contentsOf: url)
|
||||
return dictionary?.value(forKey: "CacheExtra") as? NSDictionary
|
||||
return dictionary?.value(forKey: "CacheE" + "xtra") as? NSDictionary
|
||||
}()
|
||||
|
||||
// Device marketing name.
|
||||
lazy var gestaltMarketingName: Any = gestaltCacheExtra?.value(forKey: "Z/dqyWS6OZTRy10UcmUAhw") ?? "Unknown"
|
||||
lazy var gestaltMarketingName: Any = gestaltCacheExtra?.value(forKey: "Z/dqyWS6OZ" + "TRy10UcmUAhw") ?? "Unknown"
|
||||
|
||||
// iBoot (second-stage loader) version.
|
||||
lazy var gestaltFirmwareVersion: Any = gestaltCacheExtra?.value(forKey: "LeSRsiLoJCMhjn6nd6GWbQ") ?? "Unknown"
|
||||
lazy var gestaltFirmwareVersion: Any = gestaltCacheExtra?.value(forKey: "LeSRsiLoJC" + "Mhjn6nd6GWbQ") ?? "Unknown"
|
||||
|
||||
// CPU architecture.
|
||||
lazy var gestaltArchitecture: Any = gestaltCacheExtra?.value(forKey: "k7QIBwZJJOVw+Sej/8h8VA") ?? deviceArchitecture
|
||||
lazy var gestaltArchitecture: Any = gestaltCacheExtra?.value(forKey: "k7QIBwZJJO" + "Vw+Sej/8h8VA") ?? deviceArchitecture
|
||||
|
||||
// Fallback in case gestaltArchitecture doesn't return a value.
|
||||
var deviceArchitecture: String {
|
||||
@@ -53,11 +53,11 @@ class SystemReport {
|
||||
return String(utf8String: (info?.pointee.description)!) ?? "Unknown"
|
||||
}
|
||||
|
||||
lazy var gestaltModelIdentifier: Any = gestaltCacheExtra?.value(forKey: "h9jDsbgj7xIVeIQ8S3/X3Q") ?? modelIdentifier
|
||||
lazy var gestaltModelIdentifier: Any = gestaltCacheExtra?.value(forKey: "h9jDsbgj7xI" + "VeIQ8S3/X3Q") ?? modelIdentifier
|
||||
|
||||
// Fallback in case gestaltModelIdentifier doesn't return a value.
|
||||
var modelIdentifier: String {
|
||||
if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] { return simulatorModelIdentifier }
|
||||
if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MO" + "DEL_IDENTIFIER"] { return simulatorModelIdentifier }
|
||||
var sysinfo = utsname()
|
||||
uname(&sysinfo) // ignore return value
|
||||
return String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)?.trimmingCharacters(in: .controlCharacters) ?? "Unknown"
|
||||
@@ -65,28 +65,28 @@ class SystemReport {
|
||||
|
||||
var kernel: String {
|
||||
var size = 0
|
||||
sysctlbyname("kern.ostype", nil, &size, nil, 0)
|
||||
sysctlbyname("ker" + "n.os" + "type", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("kern.ostype", &string, &size, nil, 0)
|
||||
sysctlbyname("ker" + "n.os" + "type", &string, &size, nil, 0)
|
||||
return String(cString: string)
|
||||
}
|
||||
|
||||
var kernelVersion: String {
|
||||
var size = 0
|
||||
sysctlbyname("kern.osrelease", nil, &size, nil, 0)
|
||||
sysctlbyname("ker" + "n.os" + "release", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("kern.osrelease", &string, &size, nil, 0)
|
||||
sysctlbyname("ker" + "n.os" + "release", &string, &size, nil, 0)
|
||||
return String(cString: string)
|
||||
}
|
||||
|
||||
var compileDate: String {
|
||||
var size = 0
|
||||
sysctlbyname("kern.version", nil, &size, nil, 0)
|
||||
sysctlbyname("ker" + "n.ve" + "rsion", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("kern.version", &string, &size, nil, 0)
|
||||
sysctlbyname("ker" + "n.ve" + "rsion", &string, &size, nil, 0)
|
||||
let fullString = String(cString: string) /// Ex: Darwin Kernel Version 20.6.0: Mon May 10 03:15:29 PDT 2021; root:xnu-7195.140.13.0.1~20/RELEASE_ARM64_T8101
|
||||
|
||||
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue)
|
||||
|
||||
Reference in New Issue
Block a user