Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a2da892ac | |||
| c9bfed3373 | |||
| 3f732f5054 | |||
| 33de7eb54a | |||
| bad02ce90b | |||
| c2cdc1c822 | |||
| 61ed2d92db | |||
| a522748e3c | |||
| 58aecf3a9d | |||
| 971de252bf | |||
| ef0bd6cd8a | |||
| 5f8c210c62 | |||
| 0bbfabc568 | |||
| 157ab2153f | |||
| 51ddd6c2c2 | |||
| c5a5641906 | |||
| cbbbf2c6db | |||
| a75c5763a4 | |||
| a66afaef04 | |||
| 7ca52868ff | |||
| 417bdf2fb3 | |||
| 92299c62de | |||
| 22cd8d12ff | |||
| de62ed79af | |||
| 261ba2a83b | |||
| 697d636cbc | |||
| cdbb4fef4d | |||
| dea5e6e4e0 |
Binary file not shown.
|
Before Width: | Height: | Size: 10 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 MiB |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Duraid Abdul
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -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**
|
||||
|
||||
@@ -22,6 +22,7 @@ extension UIScreen {
|
||||
static var hasRoundedCorners = UIScreen.main.value(forKey: "_" + "display" + "Corner" + "Radius") as! CGFloat > 0
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
extension UIApplication {
|
||||
var statusBarHeight: CGFloat {
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
//#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
var GLOBAL_DEBUG_BORDERS = false
|
||||
var GLOBAL_BORDER_TRACKERS: [BorderManager] = []
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
public static let shared = LCManager()
|
||||
@@ -26,17 +27,54 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let defaultConsoleSize = CGSize(width: 212, height: 124)
|
||||
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.
|
||||
lazy var consoleSize = defaultConsoleSize {
|
||||
didSet {
|
||||
consoleView.frame.size = consoleSize
|
||||
|
||||
// Update text view width.
|
||||
if consoleView.frame.size.width > ResizeController.kMaxConsoleWidth {
|
||||
consoleTextView.frame.size.width = ResizeController.kMaxConsoleWidth
|
||||
consoleTextView.frame.size.width = ResizeController.kMaxConsoleWidth - 4
|
||||
} else if consoleView.frame.size.width < ResizeController.kMinConsoleWidth {
|
||||
consoleTextView.frame.size.width = ResizeController.kMinConsoleWidth - 4
|
||||
} else {
|
||||
consoleTextView.frame.size.width = consoleSize.width
|
||||
consoleTextView.frame.size.width = consoleSize.width - 4
|
||||
}
|
||||
|
||||
// Update text view height.
|
||||
if consoleView.frame.size.height > ResizeController.kMaxConsoleHeight {
|
||||
consoleTextView.frame.size.height = ResizeController.kMaxConsoleHeight - 4
|
||||
+ (consoleView.frame.size.height - ResizeController.kMaxConsoleHeight) * 2 / 3
|
||||
} else if consoleView.frame.size.height < ResizeController.kMinConsoleHeight {
|
||||
consoleTextView.frame.size.height = ResizeController.kMinConsoleHeight - 4
|
||||
+ (consoleView.frame.size.height - ResizeController.kMinConsoleHeight) * 2 / 3
|
||||
} else {
|
||||
consoleTextView.frame.size.height = consoleSize.height - 4
|
||||
}
|
||||
|
||||
consoleTextView.contentOffset.y = consoleTextView.contentSize.height - consoleTextView.bounds.size.height
|
||||
|
||||
// TODO: Snap to nearest position.
|
||||
|
||||
UserDefaults.standard.set(consoleSize.width, forKey: "LocalConsole_Width")
|
||||
@@ -52,7 +90,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
lazy var consoleView = viewController.view!
|
||||
|
||||
/// Text view that displays printed items.
|
||||
let consoleTextView = UITextView()
|
||||
let consoleTextView = InvertedTextView()
|
||||
|
||||
/// Button that reveals menu.
|
||||
lazy var menuButton = UIButton()
|
||||
@@ -87,11 +125,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)
|
||||
|
||||
@@ -103,7 +137,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.center = possibleEndpoints.first!
|
||||
consoleView.alpha = 0
|
||||
|
||||
consoleView.layer.cornerRadius = 20
|
||||
consoleView.layer.cornerRadius = 22
|
||||
consoleView.layer.cornerCurve = .continuous
|
||||
|
||||
let borderView = UIView()
|
||||
@@ -118,17 +152,19 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.addSubview(borderView)
|
||||
|
||||
// Configure text view.
|
||||
consoleTextView.frame = CGRect(x: 0, y: 2, width: consoleSize.width, height: consoleSize.height - 4)
|
||||
consoleTextView.frame = CGRect(x: 2, y: 2, width: consoleSize.width - 4, height: consoleSize.height - 4)
|
||||
consoleTextView.isEditable = false
|
||||
consoleTextView.backgroundColor = .clear
|
||||
consoleTextView.textContainerInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
|
||||
consoleTextView.textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 10, right: 8)
|
||||
|
||||
consoleTextView.isSelectable = false
|
||||
consoleTextView.showsVerticalScrollIndicator = false
|
||||
consoleTextView.contentInsetAdjustmentBehavior = .never
|
||||
consoleTextView.autoresizingMask = [.flexibleHeight]
|
||||
consoleView.addSubview(consoleTextView)
|
||||
|
||||
consoleTextView.layer.cornerRadius = consoleView.layer.cornerRadius - 2
|
||||
consoleTextView.layer.cornerCurve = .continuous
|
||||
|
||||
// Configure gesture recognizers.
|
||||
panRecognizer.maximumNumberOfTouches = 1
|
||||
panRecognizer.delegate = self
|
||||
@@ -143,7 +179,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
// Prepare menu button.
|
||||
let diameter = CGFloat(26)
|
||||
let diameter = CGFloat(28)
|
||||
|
||||
// This tuned button frame is used to adjust where the menu appears.
|
||||
menuButton = UIButton(frame: CGRect(x: consoleView.bounds.width - 44,
|
||||
@@ -163,7 +199,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
circle.isUserInteractionEnabled = false
|
||||
menuButton.addSubview(circle)
|
||||
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16)))
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17)))
|
||||
ellipsisImage.frame.size = circle.bounds.size
|
||||
ellipsisImage.contentMode = .center
|
||||
circle.addSubview(ellipsisImage)
|
||||
@@ -173,8 +209,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)
|
||||
}
|
||||
@@ -229,49 +263,60 @@ 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)
|
||||
}.startAnimation()
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "shadowOpacity")
|
||||
animation.fromValue = 0
|
||||
animation.toValue = 0.5
|
||||
animation.duration = 0.6
|
||||
consoleView.layer.add(animation, forKey: animation.keyPath)
|
||||
consoleView.layer.shadowOpacity = 0.5
|
||||
|
||||
} else {
|
||||
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) { [self] in
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.9, y: 0.9)
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
consoleView.alpha = 0
|
||||
}.startAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _hasRelayedOffsetChange = false
|
||||
|
||||
/// Print items to the console view.
|
||||
public func print(_ items: Any) {
|
||||
let string: String = {
|
||||
if consoleTextView.text == "" {
|
||||
return "\(items)"
|
||||
} else {
|
||||
return "\(items)\n" + consoleTextView.text
|
||||
}
|
||||
}()
|
||||
|
||||
setAttributedText(string)
|
||||
|
||||
// Update the context menu to show the clipboard/clear actions.
|
||||
menuButton.menu = makeMenu()
|
||||
if currentText == "" {
|
||||
currentText = "\(items)"
|
||||
} else {
|
||||
currentText = currentText + "\n\(items)"
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -309,7 +354,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()
|
||||
@@ -337,49 +384,65 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
func systemReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
print("Screen Scale: \(UIScreen.main.scale)\n")
|
||||
print("Screen Radius: \(UIScreen.main.value(forKey: "_displayCornerRadius") as! CGFloat)")
|
||||
print("Screen Size: \(UIScreen.main.bounds.size)")
|
||||
print("Max Frame Rate: \(UIScreen.main.maximumFramesPerSecond) Hz")
|
||||
print("Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)")
|
||||
print("System Uptime: \(Int(ProcessInfo.processInfo.systemUptime))s")
|
||||
print("Thermal State: \(SystemReport.shared.thermalState)")
|
||||
print("Processor Cores: \(Int(ProcessInfo.processInfo.processorCount))")
|
||||
print("Memory: \(round(100 * Double(ProcessInfo.processInfo.physicalMemory) * pow(10, -9)) / 100) GB")
|
||||
print("OS Compile Date: \(SystemReport.shared.compileDate)")
|
||||
print("System Version: \(SystemReport.shared.versionString)")
|
||||
print("Kernel Version: \(SystemReport.shared.kernel) \(SystemReport.shared.kernelVersion)")
|
||||
print("Firmware: \(SystemReport.shared.gestaltFirmwareVersion)")
|
||||
print("Architecture: \(SystemReport.shared.gestaltArchitecture)")
|
||||
print("Model Identifier: \(SystemReport.shared.gestaltModelIdentifier)")
|
||||
print("Marketing Name: \(SystemReport.shared.gestaltMarketingName)")
|
||||
|
||||
print(
|
||||
"""
|
||||
\n
|
||||
Model Name: \(SystemReport.shared.gestaltMarketingName)
|
||||
Model Identifier: \(SystemReport.shared.gestaltModelIdentifier)
|
||||
Architecture: \(SystemReport.shared.gestaltArchitecture)
|
||||
Firmware: \(SystemReport.shared.gestaltFirmwareVersion)
|
||||
Kernel Version: \(SystemReport.shared.kernel) \(SystemReport.shared.kernelVersion)
|
||||
System Version: \(SystemReport.shared.versionString)
|
||||
OS Compile Date: \(SystemReport.shared.compileDate)
|
||||
Memory: \(round(100 * Double(ProcessInfo.processInfo.physicalMemory) * pow(10, -9)) / 100) GB
|
||||
Processor Cores: \(Int(ProcessInfo.processInfo.processorCount))
|
||||
Thermal State: \(SystemReport.shared.thermalState)
|
||||
System Uptime: \(Int(ProcessInfo.processInfo.systemUptime))s
|
||||
Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)
|
||||
"""
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func displayReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
print(
|
||||
"""
|
||||
\n
|
||||
Screen Size: \(UIScreen.main.bounds.size)
|
||||
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: \(String(format: "%.2f", UIScreen.main.brightness))
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleLock() {
|
||||
scrollLocked.toggle()
|
||||
}
|
||||
|
||||
func toggleVisibility() {
|
||||
if isVisible {
|
||||
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.9, y: 0.9)
|
||||
consoleView.alpha = 0
|
||||
}.startAnimation()
|
||||
func commitTextChanges(requestMenuUpdate menuUpdateRequested: Bool) {
|
||||
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - 20 - consoleTextView.bounds.size.height ||
|
||||
_hasRelayedOffsetChange == false {
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
|
||||
isVisible = false
|
||||
} else {
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
consoleView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
isVisible = true
|
||||
_hasRelayedOffsetChange = true
|
||||
}
|
||||
|
||||
// Renders color properly (for dark appearance).
|
||||
consoleView.backgroundColor = .black
|
||||
consoleTextView.text = currentText
|
||||
|
||||
setAttributedText(currentText)
|
||||
|
||||
if menuUpdateRequested {
|
||||
// Update the context menu to show the clipboard/clear actions.
|
||||
menuButton.menu = makeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
func setAttributedText(_ string: String) {
|
||||
@@ -417,22 +480,54 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
let consoleActions = UIMenu(title: "", options: .displayInline, children: [clear, resize])
|
||||
|
||||
var frameSymbol = "rectangle.3.offgrid"
|
||||
if #available(iOS 15, *) {
|
||||
frameSymbol = "square.inset.filled"
|
||||
}
|
||||
|
||||
let viewFrames = UIAction(title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
|
||||
image: UIImage(systemName: "rectangle.3.offgrid"), handler: { _ in
|
||||
image: UIImage(systemName: frameSymbol), handler: { _ in
|
||||
self.debugBordersEnabled.toggle()
|
||||
self.menuButton.menu = self.makeMenu()
|
||||
})
|
||||
|
||||
let systemReport = UIAction(title: "System Report",
|
||||
image: UIImage(systemName: "doc.badge.gearshape"), handler: { _ in
|
||||
image: UIImage(systemName: "cpu"), handler: { _ in
|
||||
self.systemReport()
|
||||
})
|
||||
|
||||
let respring = UIAction(title: "Restart SpringBoard",
|
||||
// Show the right glyph for the current device being used.
|
||||
let deviceSymbol: String = {
|
||||
|
||||
let hasHomeButton = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat == 0
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
if hasHomeButton {
|
||||
return "ipad.homebutton"
|
||||
} else {
|
||||
return "ipad"
|
||||
}
|
||||
} else if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
if hasHomeButton {
|
||||
return "iphone.homebutton"
|
||||
} else {
|
||||
return "iphone"
|
||||
}
|
||||
} else {
|
||||
return "rectangle"
|
||||
}
|
||||
}()
|
||||
|
||||
let displayReport = UIAction(title: "Display Report",
|
||||
image: UIImage(systemName: deviceSymbol), handler: { _ in
|
||||
self.displayReport()
|
||||
})
|
||||
|
||||
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) {
|
||||
@@ -446,7 +541,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
animator.startAnimation()
|
||||
})
|
||||
let debugActions = UIMenu(title: "", options: .displayInline, children: [viewFrames, systemReport, respring])
|
||||
let debugActions = UIMenu(title: "", options: .displayInline,
|
||||
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
|
||||
children: [viewFrames, systemReport, displayReport, respring])])
|
||||
|
||||
var menuContent: [UIMenuElement] = []
|
||||
|
||||
@@ -614,20 +711,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +730,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)
|
||||
@@ -647,3 +742,33 @@ extension UIWindow {
|
||||
}
|
||||
|
||||
//#endif
|
||||
|
||||
class InvertedTextView: UITextView {
|
||||
|
||||
var pendingOffsetChange = false
|
||||
|
||||
// Thanks to WWDC21 Lab!
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if panGestureRecognizer.numberOfTouches == 0 && pendingOffsetChange {
|
||||
contentOffset.y = contentSize.height - bounds.size.height
|
||||
} else {
|
||||
pendingOffsetChange = false
|
||||
}
|
||||
}
|
||||
|
||||
var cancelNextContentSizeDidSet = false
|
||||
|
||||
override var contentSize: CGSize {
|
||||
didSet {
|
||||
cancelNextContentSizeDidSet = true
|
||||
|
||||
if contentSize.height < bounds.size.height {
|
||||
contentInset.top = bounds.size.height - contentSize.height
|
||||
} else {
|
||||
contentInset.top = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class ResizeController {
|
||||
|
||||
public static let shared = ResizeController()
|
||||
@@ -116,6 +117,14 @@ class ResizeController {
|
||||
|
||||
if isActive {
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) {
|
||||
|
||||
let textView = LCManager.shared.consoleTextView
|
||||
|
||||
textView.contentOffset.y = textView.contentSize.height - textView.bounds.size.height
|
||||
}.startAnimation()
|
||||
|
||||
|
||||
if LCManager.shared.consoleView.traitCollection.userInterfaceStyle == .light {
|
||||
LCManager.shared.consoleView.layer.shadowOpacity = 0.25
|
||||
}
|
||||
@@ -189,12 +198,15 @@ class ResizeController {
|
||||
|
||||
var initialHeight = CGFloat.zero
|
||||
|
||||
static let kMinConsoleHeight: CGFloat = 108
|
||||
static let kMaxConsoleHeight: CGFloat = 346
|
||||
|
||||
@objc func verticalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: bottomGrabber.superview)
|
||||
|
||||
let maxHeight: CGFloat = 346
|
||||
let minHeight: CGFloat = 108
|
||||
let minHeight = Self.kMinConsoleHeight
|
||||
let maxHeight = Self.kMaxConsoleHeight
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
@@ -252,14 +264,15 @@ class ResizeController {
|
||||
|
||||
var initialWidth = CGFloat.zero
|
||||
|
||||
static let kMinConsoleWidth: CGFloat = 112
|
||||
static let kMaxConsoleWidth: CGFloat = UIScreen.portraitSize.width - 56
|
||||
|
||||
@objc func horizontalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: bottomGrabber.superview)
|
||||
|
||||
let maxWidth: CGFloat = Self.kMaxConsoleWidth
|
||||
let minWidth: CGFloat = 112
|
||||
let minWidth = Self.kMinConsoleWidth
|
||||
let maxWidth = Self.kMaxConsoleWidth
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
@@ -317,6 +330,7 @@ class ResizeController {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class PlatterView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@@ -377,6 +391,7 @@ class PlatterView: UIView {
|
||||
|
||||
let subtitleLabel = UILabel()
|
||||
subtitleLabel.text = "Use the grabbers to resize the console."
|
||||
subtitleLabel.font = .systemFont(ofSize: 17, weight: .medium)
|
||||
subtitleLabel.sizeToFit()
|
||||
subtitleLabel.alpha = 0.5
|
||||
subtitleLabel.center.x = bounds.width / 2
|
||||
@@ -455,7 +470,7 @@ class PlatterView: UIView {
|
||||
|
||||
// Resolves a text view frame animation bug that occurs when *decreasing* text view width.
|
||||
if LCManager.shared.consoleSize.width > LCManager.shared.defaultConsoleSize.width {
|
||||
LCManager.shared.consoleTextView.frame.size.width = LCManager.shared.defaultConsoleSize.width
|
||||
LCManager.shared.consoleTextView.frame.size.width = LCManager.shared.defaultConsoleSize.width - 4
|
||||
}
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
|
||||
|
||||
@@ -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