Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bbfabc568 | |||
| 157ab2153f | |||
| 51ddd6c2c2 | |||
| c5a5641906 | |||
| cbbbf2c6db | |||
| a75c5763a4 | |||
| a66afaef04 | |||
| 7ca52868ff | |||
| 417bdf2fb3 | |||
| 92299c62de | |||
| 22cd8d12ff | |||
| de62ed79af | |||
| 261ba2a83b | |||
| 697d636cbc | |||
| cdbb4fef4d | |||
| dea5e6e4e0 | |||
| b28605e52e | |||
| 35a5350f17 | |||
| b319499056 | |||
| 17d30ad554 | |||
| b56d27ff26 | |||
| c7fe1daf26 | |||
| 6db60e25b2 | |||
| 5094ddc710 | |||
| 9b5e0d84ac | |||
| 1620fad461 | |||
| 86d9e18613 | |||
| 832f507ab1 | |||
| 2406568789 | |||
| f57800e8a4 | |||
| e7bc5d221b | |||
| 6e121fb39b | |||
| 4c8a519c4f | |||
| d3d12e2450 | |||
| 062f74a429 | |||
| c8468823c0 | |||
| c19df26edc | |||
| d1227ee522 | |||
| 97a6ae0899 | |||
| 4b0f9e149a | |||
| 20f88c739f | |||
| 98a66af9b2 | |||
| 19f2302f13 | |||
| 308a25f6db | |||
| 40614dffec | |||
| 245d6f0310 | |||
| 349d7359eb | |||
| 37e0ab6323 | |||
| 1ea0dc7026 | |||
| a9db186136 |
Binary file not shown.
|
After Width: | Height: | Size: 10 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 MiB After Width: | Height: | Size: 11 MiB |
@@ -2,7 +2,10 @@
|
||||
|
||||
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.
|
||||
|
||||
<img src="https://github.com/duraidabdul/LocalConsole/blob/main/Demo.gif?raw=true" width="250">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
## **Setup**
|
||||
|
||||
@@ -32,8 +35,11 @@ localConsoleManager.isVisible = false
|
||||
// Print items to the console view.
|
||||
localConsoleManager.print("Hello, world!")
|
||||
|
||||
// Clear text in the console view.
|
||||
// Clear console text.
|
||||
localConsoleManager.clear()
|
||||
|
||||
// Copy console text.
|
||||
localConsoleManager.copy()
|
||||
```
|
||||
|
||||
```swift
|
||||
@@ -43,8 +49,6 @@ localConsoleManager.fontSize = 5
|
||||
|
||||
|
||||
## **To-Do**
|
||||
* Custom console view size
|
||||
* Support for iOS 13
|
||||
* Screen edge console hiding
|
||||
* Custom pinch to resize gesture
|
||||
* Make console view reactive to landscape/portrait switch
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
/// This class handles enabling and disabling debug borders on a specified view.
|
||||
@@ -64,5 +62,3 @@ class BorderManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIScreen {
|
||||
@@ -59,5 +57,3 @@ extension UIView {
|
||||
frame.origin.y = (round(frame.origin.y * UIScreen.main.scale)) / UIScreen.main.scale
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension CGPoint {
|
||||
@@ -80,5 +78,3 @@ func nearestTargetTo(_ point: CGPoint, possibleTargets: [CGPoint]) -> CGPoint {
|
||||
}
|
||||
return nearestEndpoint
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
//#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
var GLOBAL_DEBUG_BORDERS = false
|
||||
var GLOBAL_BORDER_TRACKERS: [BorderManager] = []
|
||||
@@ -26,17 +27,35 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let defaultConsoleSize = CGSize(width: 212, height: 124)
|
||||
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,12 +71,12 @@ 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.
|
||||
var menuButton: UIButton!
|
||||
lazy var menuButton = UIButton()
|
||||
|
||||
/// Tracks whether the PiP console is in text view scroll mode or pan mode.
|
||||
/// Tracks whether the PiP console is in text view scroll mode or pan mode.
|
||||
var scrollLocked = true
|
||||
|
||||
/// Feedback generator for the long press action.
|
||||
@@ -74,14 +93,14 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
} else {
|
||||
return [CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,21 +109,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// Configure console window.
|
||||
let windowScene = UIApplication.shared
|
||||
.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.first
|
||||
|
||||
if let windowScene = windowScene as? UIWindowScene {
|
||||
consoleWindow = ConsoleWindow(windowScene: windowScene)
|
||||
consoleWindow?.frame = UIScreen.main.bounds
|
||||
consoleWindow?.windowLevel = UIWindow.Level.statusBar
|
||||
consoleWindow?.isHidden = false
|
||||
consoleWindow?.addSubview(consoleView)
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride
|
||||
}
|
||||
configureWindow()
|
||||
|
||||
consoleSize = CGSize(width: UserDefaults.standard.object(forKey: "LocalConsole_Width") as? CGFloat ?? consoleSize.width,
|
||||
height: UserDefaults.standard.object(forKey: "LocalConsole_Height") as? CGFloat ?? consoleSize.height)
|
||||
@@ -117,7 +122,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()
|
||||
@@ -132,17 +137,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
|
||||
@@ -157,7 +164,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,
|
||||
@@ -177,7 +184,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)
|
||||
@@ -185,9 +192,59 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
menuButton.tintColor = UIColor(white: 1, alpha: 0.75)
|
||||
menuButton.menu = makeMenu()
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
consoleView.addSubview(menuButton!)
|
||||
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)
|
||||
}
|
||||
|
||||
/// Adds a LocalConsole window to the app's main scene.
|
||||
func configureWindow() {
|
||||
var windowSceneFound = false
|
||||
|
||||
// Configure console window.
|
||||
func fetchWindowScene() {
|
||||
let windowScene = UIApplication.shared
|
||||
.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.first
|
||||
|
||||
if let windowScene = windowScene as? UIWindowScene {
|
||||
|
||||
windowSceneFound = true
|
||||
|
||||
consoleWindow = ConsoleWindow(windowScene: windowScene)
|
||||
consoleWindow?.frame = UIScreen.main.bounds
|
||||
consoleWindow?.windowLevel = UIWindow.Level.statusBar
|
||||
consoleWindow?.isHidden = false
|
||||
consoleWindow?.addSubview(consoleView)
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride
|
||||
}
|
||||
}
|
||||
|
||||
fetchWindowScene()
|
||||
|
||||
/// Ensures the window is configured (i.e. scene has been found). If not, delay and wait for a scene to prepare itself, then try again.
|
||||
for i in 1...10 {
|
||||
|
||||
let delay = Double(i) / 10
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [self] in
|
||||
|
||||
guard !windowSceneFound else { return }
|
||||
|
||||
fetchWindowScene()
|
||||
|
||||
if isVisible {
|
||||
isVisible = false
|
||||
consoleView.layer.removeAllAnimations()
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -202,25 +259,46 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
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) {
|
||||
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - 20 - consoleTextView.bounds.size.height ||
|
||||
_hasRelayedOffsetChange == false {
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
|
||||
_hasRelayedOffsetChange = true
|
||||
}
|
||||
|
||||
let string: String = {
|
||||
if consoleTextView.text == "" {
|
||||
return "\(items)"
|
||||
} else {
|
||||
return "\(items)\n" + consoleTextView.text
|
||||
return consoleTextView.text + "\n\(items)"
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -245,6 +323,32 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
// MARK: Handle keyboard show/hide.
|
||||
private var keyboardHeight: CGFloat? = nil {
|
||||
didSet {
|
||||
|
||||
if consoleView.center != possibleEndpoints[0] && consoleView.center != possibleEndpoints[1] {
|
||||
let nearestTargetPosition = nearestTargetTo(consoleView.center, possibleTargets: possibleEndpoints.suffix(2))
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.55, dampingRatio: 1) {
|
||||
self.consoleView.center = nearestTargetPosition
|
||||
}.startAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func keyboardWillShow(_ notification: Notification) {
|
||||
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
|
||||
let keyboardRectangle = keyboardFrame.cgRectValue
|
||||
self.keyboardHeight = keyboardRectangle.height
|
||||
}
|
||||
}
|
||||
|
||||
@objc func keyboardWillHide() {
|
||||
keyboardHeight = nil
|
||||
}
|
||||
|
||||
private var debugBordersEnabled = false {
|
||||
didSet {
|
||||
GLOBAL_DEBUG_BORDERS = debugBordersEnabled
|
||||
@@ -273,29 +377,48 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleLock() {
|
||||
scrollLocked.toggle()
|
||||
func systemReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
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 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 displayReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
isVisible = false
|
||||
} else {
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
consoleView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
isVisible = true
|
||||
print(
|
||||
"""
|
||||
\n
|
||||
Screen Size: \(UIScreen.main.bounds.size)
|
||||
Screen Corner Radius: \(UIScreen.main.value(forKey: "_displayCornerRadius") as! CGFloat)
|
||||
Screen Scale: \(UIScreen.main.scale)
|
||||
Max Frame Rate: \(UIScreen.main.maximumFramesPerSecond) Hz
|
||||
Brightness: \(UIScreen.main.brightness)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Renders color properly (for dark appearance).
|
||||
consoleView.backgroundColor = .black
|
||||
}
|
||||
|
||||
@objc func toggleLock() {
|
||||
scrollLocked.toggle()
|
||||
}
|
||||
|
||||
func setAttributedText(_ string: String) {
|
||||
@@ -333,10 +456,47 @@ 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()
|
||||
self.menuButton.menu = self.makeMenu()
|
||||
})
|
||||
|
||||
let systemReport = UIAction(title: "System Report",
|
||||
image: UIImage(systemName: "cpu"), handler: { _ in
|
||||
self.systemReport()
|
||||
})
|
||||
|
||||
// Show the right glyph for the current device being used.
|
||||
let deviceSymbol: String = {
|
||||
|
||||
let hasHomeButton = UIScreen.main.value(forKey: "_displayCornerRadius") 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 SpringBoard",
|
||||
@@ -357,7 +517,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
animator.startAnimation()
|
||||
})
|
||||
let debugActions = UIMenu(title: "", options: .displayInline, children: [viewFrames, respring])
|
||||
let debugActions = UIMenu(title: "", options: .displayInline,
|
||||
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
|
||||
children: [viewFrames, systemReport, displayReport, respring])])
|
||||
|
||||
var menuContent: [UIMenuElement] = []
|
||||
|
||||
@@ -557,4 +719,34 @@ extension UIWindow {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
//#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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,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 +197,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 +263,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:
|
||||
@@ -377,6 +389,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
|
||||
@@ -408,7 +421,7 @@ class PlatterView: UIView {
|
||||
|
||||
lazy var doneButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = .systemBlue.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
|
||||
button.backgroundColor = UIColor.systemBlue.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
|
||||
button.setTitle("Done", for: .normal)
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
|
||||
@@ -455,7 +468,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) {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// SystemReport.swift
|
||||
// LocalConsole
|
||||
//
|
||||
// Created by Duraid Abdul on 2021-06-01.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MachO
|
||||
|
||||
class SystemReport {
|
||||
static let shared = SystemReport()
|
||||
|
||||
var versionString: String {
|
||||
ProcessInfo.processInfo.operatingSystemVersionString
|
||||
.replacingOccurrences(of: "Build ", with: "")
|
||||
.replacingOccurrences(of: "Version ", with: "")
|
||||
}
|
||||
|
||||
// Current device thermal state.
|
||||
var thermalState: String {
|
||||
let state = ProcessInfo.processInfo.thermalState
|
||||
|
||||
switch state {
|
||||
case .nominal: return "Nominal"
|
||||
case .fair : return "Fair"
|
||||
case .serious : return "Serious"
|
||||
case .critical : return "Critical"
|
||||
default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// 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 dictionary = NSDictionary(contentsOf: url)
|
||||
return dictionary?.value(forKey: "CacheExtra") as? NSDictionary
|
||||
}()
|
||||
|
||||
// Device marketing name.
|
||||
lazy var gestaltMarketingName: Any = gestaltCacheExtra?.value(forKey: "Z/dqyWS6OZTRy10UcmUAhw") ?? "Unknown"
|
||||
|
||||
// iBoot (second-stage loader) version.
|
||||
lazy var gestaltFirmwareVersion: Any = gestaltCacheExtra?.value(forKey: "LeSRsiLoJCMhjn6nd6GWbQ") ?? "Unknown"
|
||||
|
||||
// CPU architecture.
|
||||
lazy var gestaltArchitecture: Any = gestaltCacheExtra?.value(forKey: "k7QIBwZJJOVw+Sej/8h8VA") ?? deviceArchitecture
|
||||
|
||||
// Fallback in case gestaltArchitecture doesn't return a value.
|
||||
var deviceArchitecture: String {
|
||||
let info = NXGetLocalArchInfo()
|
||||
return String(utf8String: (info?.pointee.description)!) ?? "Unknown"
|
||||
}
|
||||
|
||||
lazy var gestaltModelIdentifier: Any = gestaltCacheExtra?.value(forKey: "h9jDsbgj7xIVeIQ8S3/X3Q") ?? modelIdentifier
|
||||
|
||||
// Fallback in case gestaltModelIdentifier doesn't return a value.
|
||||
var modelIdentifier: String {
|
||||
if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MODEL_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"
|
||||
}
|
||||
|
||||
var kernel: String {
|
||||
var size = 0
|
||||
sysctlbyname("kern.ostype", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("kern.ostype", &string, &size, nil, 0)
|
||||
return String(cString: string)
|
||||
}
|
||||
|
||||
var kernelVersion: String {
|
||||
var size = 0
|
||||
sysctlbyname("kern.osrelease", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("kern.osrelease", &string, &size, nil, 0)
|
||||
return String(cString: string)
|
||||
}
|
||||
|
||||
var compileDate: String {
|
||||
var size = 0
|
||||
sysctlbyname("kern.version", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("kern.version", &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)
|
||||
if let matches = detector?.matches(in: fullString, options: [], range: NSRange(location: 0, length: fullString.utf16.count)) {
|
||||
for match in matches {
|
||||
|
||||
if let date = match.date {
|
||||
|
||||
let dateformatter = DateFormatter()
|
||||
dateformatter.dateStyle = .medium
|
||||
|
||||
return dateformatter.string(from: date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user