Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6846048d0 | |||
| d6d9aad082 | |||
| 6723947fe6 | |||
| 24a7a197b0 | |||
| ab79c1200f | |||
| 0ae97b8162 | |||
| 4b79e2744d | |||
| 641e20bb01 | |||
| bc6c4a91ba | |||
| 82605fcfbb | |||
| 27876dfba9 | |||
| 1a2da892ac | |||
| c9bfed3373 | |||
| 3f732f5054 | |||
| 33de7eb54a | |||
| bad02ce90b | |||
| c2cdc1c822 | |||
| 61ed2d92db | |||
| a522748e3c | |||
| 58aecf3a9d | |||
| 971de252bf | |||
| ef0bd6cd8a |
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**
|
||||
@@ -17,7 +17,7 @@ Welcome to LocalConsole! This Swift Package makes on-device debugging easy with
|
||||
```swift
|
||||
import LocalConsole
|
||||
|
||||
let localConsoleManager = LCManager.shared
|
||||
let consoleManager = LCManager.shared
|
||||
```
|
||||
|
||||
## **Usage**
|
||||
@@ -25,30 +25,29 @@ Once prepared, the localConsole can be used throughout your project.
|
||||
```swift
|
||||
|
||||
// Show the console view.
|
||||
localConsoleManager.isVisible = true
|
||||
consoleManager.isVisible = true
|
||||
|
||||
// Hide the console view.
|
||||
localConsoleManager.isVisible = false
|
||||
consoleManager.isVisible = false
|
||||
```
|
||||
|
||||
```swift
|
||||
// Print items to the console view.
|
||||
localConsoleManager.print("Hello, world!")
|
||||
consoleManager.print("Hello, world!")
|
||||
|
||||
// Clear console text.
|
||||
localConsoleManager.clear()
|
||||
consoleManager.clear()
|
||||
|
||||
// Copy console text.
|
||||
localConsoleManager.copy()
|
||||
consoleManager.copy()
|
||||
```
|
||||
|
||||
```swift
|
||||
// Change the console view font size.
|
||||
localConsoleManager.fontSize = 5
|
||||
consoleManager.fontSize = 5
|
||||
```
|
||||
|
||||
|
||||
## **To-Do**
|
||||
* Support for iOS 13
|
||||
* Screen edge console hiding
|
||||
* Make console view reactive to landscape/portrait switch
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ import SwiftUI
|
||||
|
||||
var GLOBAL_BORDER_TRACKERS: [BorderManager] = []
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
public static let shared = LCManager()
|
||||
@@ -26,8 +27,53 @@ 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)
|
||||
|
||||
lazy var borderView = UIView()
|
||||
|
||||
var lumaWidthAnchor: NSLayoutConstraint!
|
||||
var lumaHeightAnchor: NSLayoutConstraint!
|
||||
|
||||
lazy var lumaView: LumaView = {
|
||||
let lumaView = LumaView()
|
||||
lumaView.foregroundView.backgroundColor = .black
|
||||
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
||||
|
||||
consoleView.addSubview(lumaView)
|
||||
|
||||
lumaView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
lumaHeightAnchor = lumaView.heightAnchor.constraint(equalToConstant: consoleView.frame.size.height)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
lumaView.widthAnchor.constraint(equalTo: consoleView.widthAnchor),
|
||||
lumaHeightAnchor,
|
||||
lumaView.centerXAnchor.constraint(equalTo: consoleView.centerXAnchor),
|
||||
lumaView.centerYAnchor.constraint(equalTo: consoleView.centerYAnchor)
|
||||
])
|
||||
|
||||
return lumaView
|
||||
}()
|
||||
|
||||
/// The fixed size of the console view.
|
||||
lazy var consoleSize = defaultConsoleSize {
|
||||
didSet {
|
||||
@@ -66,11 +112,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
var consoleWindow: ConsoleWindow?
|
||||
|
||||
// The console needs a parent view controller in order to display context menus.
|
||||
let viewController = UIViewController()
|
||||
lazy var viewController = UIViewController()
|
||||
lazy var consoleView = viewController.view!
|
||||
|
||||
/// Text view that displays printed items.
|
||||
let consoleTextView = InvertedTextView()
|
||||
lazy var consoleTextView = InvertedTextView()
|
||||
|
||||
/// Button that reveals menu.
|
||||
lazy var menuButton = UIButton()
|
||||
@@ -79,41 +125,94 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
var scrollLocked = true
|
||||
|
||||
/// Feedback generator for the long press action.
|
||||
let feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
lazy var feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
|
||||
lazy var panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(consolePiPPanner(recognizer:)))
|
||||
lazy var longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(recognizer:)))
|
||||
|
||||
/// Gesture endpoints. Each point represents a corner of the screen. TODO: Handle screen rotation.
|
||||
var possibleEndpoints: [CGPoint] {
|
||||
|
||||
if consoleSize.width < UIScreen.portraitSize.width - 112 {
|
||||
return [CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
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 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
|
||||
// Four endpoints, one for each corner.
|
||||
var endpoints = [CGPoint(x: 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 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
|
||||
if consoleView.frame.minX <= 0 {
|
||||
|
||||
endpoints = [endpoints[0], endpoints[1]]
|
||||
|
||||
// Left edge hiding endpoints.
|
||||
if consoleView.center.y < UIScreen.portraitSize.height / 2 {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 10,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 10,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
} else if consoleView.frame.maxX >= UIScreen.portraitSize.width {
|
||||
|
||||
endpoints = [endpoints[2], endpoints[3]]
|
||||
|
||||
// Right edge hiding endpoints.
|
||||
if consoleView.center.y < UIScreen.portraitSize.height / 2 {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 10,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 10,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
|
||||
} 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 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
|
||||
// Two endpoints, one for the top, one for the bottom..
|
||||
var endpoints = [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 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
|
||||
if consoleView.frame.minX <= 0 {
|
||||
|
||||
// Left edge hiding endpoints.
|
||||
if consoleView.center.y < UIScreen.portraitSize.height / 2 {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 10,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 10,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
} else if consoleView.frame.maxX >= UIScreen.portraitSize.width {
|
||||
|
||||
// Right edge hiding endpoints.
|
||||
if consoleView.center.y < UIScreen.portraitSize.height / 2 {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 10,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 10,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
consoleView.backgroundColor = .black
|
||||
|
||||
|
||||
consoleView.layer.shadowRadius = 16
|
||||
consoleView.layer.shadowOpacity = 0.5
|
||||
@@ -124,7 +223,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.layer.cornerRadius = 22
|
||||
consoleView.layer.cornerCurve = .continuous
|
||||
|
||||
let borderView = UIView()
|
||||
let _ = lumaView
|
||||
|
||||
borderView.frame = CGRect(x: -1, y: -1,
|
||||
width: consoleSize.width + 2,
|
||||
height: consoleSize.height + 2)
|
||||
@@ -183,7 +283,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
circle.isUserInteractionEnabled = false
|
||||
menuButton.addSubview(circle)
|
||||
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17)))
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .medium)))
|
||||
ellipsisImage.frame.size = circle.bounds.size
|
||||
ellipsisImage.contentMode = .center
|
||||
circle.addSubview(ellipsisImage)
|
||||
@@ -247,11 +348,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)
|
||||
@@ -279,38 +388,71 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private var _hasRelayedOffsetChange = false
|
||||
var grabberMode: Bool = false {
|
||||
|
||||
didSet {
|
||||
guard oldValue != grabberMode else { return }
|
||||
|
||||
if grabberMode {
|
||||
|
||||
if oldValue == false {
|
||||
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
||||
lumaHeightAnchor.constant = consoleView.frame.size.height
|
||||
consoleView.layoutIfNeeded()
|
||||
}
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 0
|
||||
menuButton.alpha = 0
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) { [self] in
|
||||
lumaView.foregroundView.alpha = 0
|
||||
borderView.alpha = 0
|
||||
}.startAnimation()
|
||||
|
||||
lumaHeightAnchor.constant = 96
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
lumaView.layer.cornerRadius = 8
|
||||
consoleView.layoutIfNeeded()
|
||||
}.startAnimation(afterDelay: 0.06)
|
||||
|
||||
consoleTextView.isUserInteractionEnabled = false
|
||||
|
||||
} else {
|
||||
lumaHeightAnchor.constant = consoleView.frame.size.height
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.layoutIfNeeded()
|
||||
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
menuButton.alpha = 1
|
||||
borderView.alpha = 1
|
||||
}.startAnimation(afterDelay: 0.2)
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 1) { [self] in
|
||||
lumaView.foregroundView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
consoleTextView.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -376,12 +518,46 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var dynamicReportTimer: Timer? {
|
||||
willSet { dynamicReportTimer?.invalidate() }
|
||||
}
|
||||
|
||||
func systemReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
if currentText != "" { print("\n") }
|
||||
|
||||
dynamicReportTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
|
||||
var _currentText = currentText
|
||||
|
||||
// To optimize performance, only scan the last 2500 characters of text for system report changes.
|
||||
let range: NSRange = {
|
||||
if _currentText.count <= 2500 {
|
||||
return NSMakeRange(0, _currentText.count)
|
||||
}
|
||||
return NSMakeRange(_currentText.count - 2500, 2500)
|
||||
}()
|
||||
|
||||
let regex0 = try! NSRegularExpression(pattern: "Thermal State: .*", options: NSRegularExpression.Options.caseInsensitive)
|
||||
_currentText = regex0.stringByReplacingMatches(in: _currentText, options: [], range: range, withTemplate: "Thermal State: \(SystemReport.shared.thermalState)")
|
||||
|
||||
let regex1 = try! NSRegularExpression(pattern: "System Uptime: .*", options: NSRegularExpression.Options.caseInsensitive)
|
||||
_currentText = regex1.stringByReplacingMatches(in: _currentText, options: [], range: range, withTemplate: "System Uptime: \(ProcessInfo.processInfo.systemUptime.formattedString!)")
|
||||
|
||||
let regex2 = try! NSRegularExpression(pattern: "Low Power Mode: .*", options: NSRegularExpression.Options.caseInsensitive)
|
||||
_currentText = regex2.stringByReplacingMatches(in: _currentText, options: [], range: range, withTemplate: "Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)")
|
||||
|
||||
if currentText != _currentText {
|
||||
currentText = _currentText
|
||||
} else {
|
||||
|
||||
// Invalidate the timer if there is no longer anything to update.
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
print(
|
||||
"""
|
||||
\n
|
||||
Model Name: \(SystemReport.shared.gestaltMarketingName)
|
||||
Model Identifier: \(SystemReport.shared.gestaltModelIdentifier)
|
||||
Architecture: \(SystemReport.shared.gestaltArchitecture)
|
||||
@@ -392,25 +568,25 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
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
|
||||
System Uptime: \(ProcessInfo.processInfo.systemUptime.formattedString!)
|
||||
Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)
|
||||
"""
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func displayReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
if currentText != "" { print("\n") }
|
||||
|
||||
print(
|
||||
"""
|
||||
\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))
|
||||
"""
|
||||
)
|
||||
}
|
||||
@@ -420,6 +596,25 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
scrollLocked.toggle()
|
||||
}
|
||||
|
||||
func commitTextChanges(requestMenuUpdate menuUpdateRequested: Bool) {
|
||||
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - consoleTextView.bounds.size.height - 20 {
|
||||
|
||||
// Weird, weird fix that makes the scroll view bottom pinning system work.
|
||||
consoleTextView.isScrollEnabled.toggle()
|
||||
consoleTextView.isScrollEnabled.toggle()
|
||||
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
}
|
||||
|
||||
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
|
||||
@@ -474,7 +669,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 {
|
||||
@@ -498,11 +693,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) {
|
||||
@@ -535,6 +730,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
@objc func longPressAction(recognizer: UILongPressGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
|
||||
guard !grabberMode else { return }
|
||||
|
||||
feedbackGenerator.selectionChanged()
|
||||
|
||||
scrollLocked = false
|
||||
@@ -542,6 +740,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1.04, y: 1.04)
|
||||
consoleTextView.alpha = 0.5
|
||||
menuButton.alpha = 0.5
|
||||
}.startAnimation()
|
||||
case .cancelled, .ended:
|
||||
|
||||
@@ -552,7 +751,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
if !grabberMode {
|
||||
consoleTextView.alpha = 1
|
||||
menuButton.alpha = 1
|
||||
}
|
||||
}.startAnimation()
|
||||
default: break
|
||||
}
|
||||
@@ -564,7 +766,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
initialViewLocation = consoleView.center
|
||||
}
|
||||
|
||||
guard !scrollLocked else { return }
|
||||
guard !scrollLocked || grabberMode else { return }
|
||||
|
||||
let translation = recognizer.translation(in: consoleView.superview)
|
||||
let velocity = recognizer.velocity(in: consoleView.superview)
|
||||
@@ -599,20 +801,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
self.grabberMode = nearestTargetPosition.x < 0 || nearestTargetPosition.x > UIScreen.portraitSize.width
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// Animate touch down.
|
||||
func consolePiPTouchDown() {
|
||||
UIViewPropertyAnimator(duration: 1, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.96, y: 0.96)
|
||||
}.startAnimation()
|
||||
guard !grabberMode else { return }
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
if !scrollLocked {
|
||||
consoleView.backgroundColor = #colorLiteral(red: 0.1331297589, green: 0.1331297589, blue: 0.1331297589, alpha: 1)
|
||||
}
|
||||
UIViewPropertyAnimator(duration: 1.25, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.95, y: 0.95)
|
||||
}.startAnimation()
|
||||
}
|
||||
|
||||
@@ -623,11 +825,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) { [self] in
|
||||
consoleView.backgroundColor = .black
|
||||
if !grabberMode {
|
||||
consoleTextView.alpha = 1
|
||||
menuButton.alpha = 1
|
||||
}
|
||||
}.startAnimation()
|
||||
}
|
||||
|
||||
@@ -705,7 +906,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)
|
||||
@@ -718,6 +919,78 @@ extension UIWindow {
|
||||
|
||||
//#endif
|
||||
|
||||
class LumaView: UIView {
|
||||
lazy var visualEffectView: UIView = {
|
||||
Bundle(path: "/Sys" + "tem/Lib" + "rary/Private" + "Frameworks/Material" + "Kit." + "framework")!.load()
|
||||
|
||||
let Pill = NSClassFromString("MT" + "Luma" + "Dodge" + "Pill" + "View") as! UIView.Type
|
||||
|
||||
let pillView = Pill.init()
|
||||
|
||||
enum Style: Int {
|
||||
case none = 0
|
||||
case thin = 1
|
||||
case gray = 2
|
||||
case black = 3
|
||||
case white = 4
|
||||
}
|
||||
|
||||
enum BackgroundLuminance: Int {
|
||||
case unknown = 0
|
||||
case dark = 1
|
||||
case light = 2
|
||||
}
|
||||
|
||||
pillView.setValue(2, forKey: "style")
|
||||
pillView.setValue(1, forKey: "background" + "Luminance")
|
||||
pillView.perform(NSSelectorFromString("_" + "update" + "Style"))
|
||||
|
||||
addSubview(pillView)
|
||||
|
||||
pillView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
pillView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
pillView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
pillView.topAnchor.constraint(equalTo: topAnchor),
|
||||
pillView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
return pillView
|
||||
}()
|
||||
|
||||
lazy var foregroundView: UIView = {
|
||||
let view = UIView()
|
||||
|
||||
addSubview(view)
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
view.topAnchor.constraint(equalTo: topAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let _ = visualEffectView
|
||||
let _ = foregroundView
|
||||
|
||||
layer.cornerCurve = .continuous
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class InvertedTextView: UITextView {
|
||||
|
||||
var pendingOffsetChange = false
|
||||
@@ -747,3 +1020,15 @@ class InvertedTextView: UITextView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
var formattedString: String? {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.hour, .minute, .second]
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func _debugPrint(_ items: Any) {
|
||||
print(items)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class ResizeController {
|
||||
|
||||
public static let shared = ResizeController()
|
||||
@@ -235,6 +236,7 @@ class ResizeController {
|
||||
}
|
||||
}()
|
||||
|
||||
LCManager.shared.lumaHeightAnchor.constant = resolvedHeight
|
||||
LCManager.shared.consoleSize.height = resolvedHeight
|
||||
LCManager.shared.consoleView.center.y = consoleCenterPoint.y
|
||||
|
||||
@@ -242,9 +244,11 @@ class ResizeController {
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
|
||||
if LCManager.shared.consoleSize.height > maxHeight {
|
||||
LCManager.shared.consoleSize.height = maxHeight
|
||||
LCManager.shared.lumaHeightAnchor.constant = maxHeight
|
||||
}
|
||||
if LCManager.shared.consoleSize.height < minHeight {
|
||||
LCManager.shared.consoleSize.height = minHeight
|
||||
LCManager.shared.lumaHeightAnchor.constant = minHeight
|
||||
}
|
||||
|
||||
LCManager.shared.consoleView.center.y = self.consoleCenterPoint.y
|
||||
@@ -329,6 +333,7 @@ class ResizeController {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class PlatterView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
@@ -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