Compare commits

..

86 Commits

Author SHA1 Message Date
Duraid Abdul c2cdc1c822 Split Strings 2021-07-20 13:24:34 -07:00
Duraid Abdul 61ed2d92db Performance improvements 2021-07-20 13:02:41 -07:00
Duraid Abdul a522748e3c Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-06-22 10:56:58 -07:00
Duraid Abdul 58aecf3a9d Update LCManager.swift 2021-06-22 10:56:56 -07:00
Duraid Abdul 971de252bf Delete Additional Files directory 2021-06-15 12:14:04 -07:00
Duraid Abdul ef0bd6cd8a Offload Demo files 2021-06-15 12:13:45 -07:00
Duraid Abdul 5f8c210c62 Update LCManager.swift
Improved debug border implementation.
2021-06-12 14:36:27 -07:00
Duraid Abdul 0bbfabc568 Reverse text in console, brand new debug. 2021-06-10 11:48:37 -07:00
Duraid Abdul 157ab2153f Delete Demo_Test.gif 2021-06-04 13:09:21 -07:00
Duraid Abdul 51ddd6c2c2 Delete Demo_test2.gif 2021-06-04 13:09:14 -07:00
Duraid Abdul c5a5641906 Update README.md 2021-06-04 13:08:59 -07:00
Duraid Abdul cbbbf2c6db Update README.md 2021-06-04 13:08:25 -07:00
Duraid Abdul a75c5763a4 Update README.md 2021-06-04 13:06:53 -07:00
Duraid Abdul a66afaef04 Update README.md 2021-06-04 13:04:54 -07:00
Duraid Abdul 7ca52868ff Add files via upload 2021-06-04 13:04:38 -07:00
Duraid Abdul 417bdf2fb3 Add files via upload 2021-06-04 13:02:41 -07:00
Duraid Abdul 92299c62de Delete Demo_Test.gif 2021-06-04 13:01:30 -07:00
Duraid Abdul 22cd8d12ff Update README.md 2021-06-04 12:59:44 -07:00
Duraid Abdul de62ed79af Add files via upload 2021-06-04 12:58:32 -07:00
Duraid Abdul 261ba2a83b Update README.md 2021-06-04 12:43:30 -07:00
Duraid Abdul 697d636cbc Update README.md 2021-06-04 12:42:59 -07:00
Duraid Abdul cdbb4fef4d Update README.md 2021-06-04 12:42:32 -07:00
Duraid Abdul dea5e6e4e0 Update README.md 2021-06-04 12:40:55 -07:00
Duraid Abdul b28605e52e Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-06-03 16:27:47 -07:00
Duraid Abdul 35a5350f17 Update LCManager.swift 2021-06-03 16:27:43 -07:00
Duraid Abdul b319499056 Merge pull request #3 from hxperl/add-uicolor-context
🎨 Add UIColor context to fix CI Build compile
2021-06-02 22:12:51 -07:00
hxperl 17d30ad554 🎨 Add UIColor context to fix CI Build compile 2021-06-03 13:09:15 +09:00
Duraid Abdul b56d27ff26 Add OS Compile Date to System Report 2021-06-01 14:46:05 -07:00
Duraid Abdul c7fe1daf26 Update System Report
Reorganized system report code for readability. Added some more valued to the system report.
2021-06-01 14:17:25 -07:00
Duraid Abdul 6db60e25b2 Asynchronous System Report 2021-05-31 21:24:23 -07:00
Duraid Abdul 5094ddc710 Added System Report 2021-05-31 21:15:22 -07:00
Duraid Abdul 9b5e0d84ac Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-24 01:46:08 -07:00
Duraid Abdul 1620fad461 Update Demo_Resize.gif 2021-05-24 01:44:17 -07:00
Duraid Abdul 86d9e18613 Update README.md 2021-05-24 01:40:04 -07:00
Duraid Abdul 832f507ab1 Update README.md 2021-05-24 01:38:59 -07:00
Duraid Abdul 2406568789 Update README.md 2021-05-24 01:35:52 -07:00
Duraid Abdul f57800e8a4 Update Demos 2021-05-24 01:27:11 -07:00
Duraid Abdul e7bc5d221b Update LCManager.swift 2021-05-24 01:01:40 -07:00
Duraid Abdul 6e121fb39b Update LCManager.swift
Fix for early initialization.
2021-05-24 00:56:07 -07:00
Duraid Abdul 4c8a519c4f Update README.md 2021-05-23 16:58:53 -07:00
Duraid Abdul d3d12e2450 Update README.md 2021-05-23 16:44:38 -07:00
Duraid Abdul 062f74a429 Update README.md 2021-05-23 16:35:52 -07:00
Duraid Abdul c8468823c0 Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-23 16:12:40 -07:00
Duraid Abdul c19df26edc Update README.md 2021-05-23 16:11:54 -07:00
Duraid Abdul d1227ee522 Update Demo.gif 2021-05-23 16:11:12 -07:00
Duraid Abdul 97a6ae0899 Update Demo.gif 2021-05-23 15:53:20 -07:00
Duraid Abdul 4b0f9e149a Update Demo.gif 2021-05-23 15:35:04 -07:00
Duraid Abdul 20f88c739f Delete Demo_1.gif 2021-05-23 01:22:21 -07:00
Duraid Abdul 98a66af9b2 Add files via upload 2021-05-23 01:20:59 -07:00
Duraid Abdul 19f2302f13 Add files via upload 2021-05-23 01:18:22 -07:00
Duraid Abdul 308a25f6db Delete Demo.gif 2021-05-23 01:17:48 -07:00
Duraid Abdul 40614dffec Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-23 01:15:05 -07:00
Duraid Abdul 245d6f0310 Update Demo.gif 2021-05-23 01:14:22 -07:00
Duraid Abdul 349d7359eb Update README.md 2021-05-23 01:07:57 -07:00
Duraid Abdul 37e0ab6323 Update Demo.gif 2021-05-22 23:43:37 -07:00
Duraid Abdul 1ea0dc7026 Update Demo.gif 2021-05-22 23:36:08 -07:00
Duraid Abdul a9db186136 Update README.md 2021-05-22 14:51:35 -07:00
Duraid Abdul 67e2ff5ce5 Update LCManager.swift 2021-05-22 14:47:17 -07:00
Duraid Abdul 1c12c101b6 Update LCManager.swift 2021-05-22 14:13:37 -07:00
Duraid Abdul cd31f0d55d Update LCManager.swift 2021-05-22 13:42:43 -07:00
Duraid Abdul 7a4a020c9c Merge pull request #2 from NoahFetz/main
Add ability to copy the console content to the clipboard.
2021-05-22 13:41:07 -07:00
Duraid Abdul 7208c56820 Merge branch 'main' into main 2021-05-22 13:39:29 -07:00
Duraid Abdul 8d7f902344 Update ResizeController.swift 2021-05-22 02:44:05 -07:00
Duraid Abdul eca9258206 Update ResizeController.swift 2021-05-22 02:40:00 -07:00
Duraid Abdul 87824eb760 Update ResizeController.swift 2021-05-22 02:32:44 -07:00
Duraid Abdul 5bcd63c6b9 Update ResizeController.swift 2021-05-22 02:27:21 -07:00
Duraid Abdul ec233369aa Update ResizeController.swift 2021-05-22 02:27:09 -07:00
Duraid Abdul 98ff7089cd Update ResizeController.swift 2021-05-22 02:19:57 -07:00
Duraid Abdul 1d41bbd85e Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-22 02:08:24 -07:00
Duraid Abdul c2bc901427 Resize mode 2021-05-22 02:08:18 -07:00
Noah Fetz 6025ea6cc2 Add ability to copy the console content to the clipboard 2021-05-20 16:00:24 +02:00
Duraid Abdul 1bf0e61732 Update README.md 2021-05-19 22:25:42 -07:00
Duraid Abdul 82c5086787 Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-19 22:11:11 -07:00
Duraid Abdul a8b7f702b4 Update LCManager.swift 2021-05-19 22:11:05 -07:00
Duraid Abdul a7a5cba7d9 Update README.md 2021-05-13 22:35:29 -07:00
Duraid Abdul 20e44ee90a Update LCManager.swift 2021-05-13 09:57:58 -07:00
Duraid Abdul 0d364aa66b Update README.md 2021-05-12 11:23:01 -07:00
Duraid Abdul 3a18df11ea Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-12 11:19:50 -07:00
Duraid Abdul 5f8cba0388 Update LCManager.swift 2021-05-12 11:19:48 -07:00
Duraid Abdul 930bc42cef Update README.md 2021-05-12 11:09:49 -07:00
Duraid Abdul c004ca0c2c Update LCManager.swift 2021-05-12 00:59:52 -07:00
Duraid Abdul da55c724e4 Update LCManager.swift 2021-05-12 00:35:34 -07:00
Duraid Abdul 9487460c56 Update LCManager.swift 2021-05-12 00:32:41 -07:00
Duraid Abdul 3b0d66211e Update LCManager.swift 2021-05-12 00:17:28 -07:00
Duraid Abdul b6e987d1d5 Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-11 23:54:14 -07:00
Duraid Abdul 95297acf18 Update LCManager.swift 2021-05-11 23:54:10 -07:00
8 changed files with 1213 additions and 133 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 MiB

+26 -12
View File
@@ -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" height="500">
<div>
<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**
@@ -21,20 +24,31 @@ let localConsoleManager = LCManager.shared
Once prepared, the localConsole can be used throughout your project.
```swift
// Show local console.
// Show the console view.
localConsoleManager.isVisible = true
// Hide local console.
// Hide the console view.
localConsoleManager.isVisible = false
// Print items to local console.
localConsoleManager.print("Hello, world!")
// Clear local console text.
localConsoleManager.clear()
```
## **Upcoming Features**
* Custom console view size
* Custom console view font size
```swift
// Print items to the console view.
localConsoleManager.print("Hello, world!")
// Clear console text.
localConsoleManager.clear()
// Copy console text.
localConsoleManager.copy()
```
```swift
// Change the console view font size.
localConsoleManager.fontSize = 5
```
## **To-Do**
* Support for iOS 13
* Screen edge console hiding
* Make console view reactive to landscape/portrait switch
-4
View File
@@ -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
+24 -3
View File
@@ -5,8 +5,6 @@
// Copyright © 2021 Duraid Abdul. All rights reserved.
//
#if canImport(UIKit)
import UIKit
extension UIScreen {
@@ -15,6 +13,13 @@ extension UIScreen {
static var size: CGSize {
return UIScreen.main.bounds.size
}
static var portraitSize: CGSize {
return CGSize(width: UIScreen.main.nativeBounds.width / UIScreen.main.nativeScale,
height: UIScreen.main.nativeBounds.height / UIScreen.main.nativeScale)
}
static var hasRoundedCorners = UIScreen.main.value(forKey: "_" + "display" + "Corner" + "Radius") as! CGFloat > 0
}
extension UIApplication {
@@ -35,4 +40,20 @@ extension UIFont {
}
}
#endif
extension UIControl {
func addActions(highlightAction: UIAction, unhighlightAction: UIAction) {
addAction(highlightAction, for: .touchDown)
addAction(highlightAction, for: .touchDragEnter)
addAction(unhighlightAction, for: .touchUpInside)
addAction(unhighlightAction, for: .touchDragExit)
addAction(unhighlightAction, for: .touchCancel)
}
}
extension UIView {
func roundOriginToPixel() {
frame.origin.x = (round(frame.origin.x * UIScreen.main.scale)) / UIScreen.main.scale
frame.origin.y = (round(frame.origin.y * UIScreen.main.scale)) / UIScreen.main.scale
}
}
@@ -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
+434 -110
View File
@@ -5,66 +5,129 @@
// 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] = []
public class LCManager: NSObject, UIGestureRecognizerDelegate {
public static let shared = LCManager()
let consoleSize = CGSize(width: 212, height: 124)
/// Set the font size. The font can be set to a minimum value of 5.0 and a maximum value of 20.0. The default value is 7.5.
public var fontSize: CGFloat = 7.5 {
didSet {
guard fontSize >= 4 else { fontSize = 4; return }
guard fontSize <= 20 else { fontSize = 20; return }
setAttributedText(consoleTextView.text)
}
}
// Strong reference needed to keep the window alive.
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 - 4
} else if consoleView.frame.size.width < ResizeController.kMinConsoleWidth {
consoleTextView.frame.size.width = ResizeController.kMinConsoleWidth - 4
} else {
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")
UserDefaults.standard.set(consoleSize.height, forKey: "LocalConsole_Height")
}
}
/// Strong reference keeps the window alive.
var consoleWindow: ConsoleWindow?
// The console needs a view controller to display context menus.
// The console needs a parent view controller in order to display context menus.
let viewController = UIViewController()
lazy var consoleView = viewController.view!
let consoleTextView = UITextView()
/// Text view that displays printed items.
let consoleTextView = InvertedTextView()
var menuButton: UIButton!
/// Button that reveals menu.
lazy var menuButton = UIButton()
/// Tracks whether the PiP console is in text view scroll mode or pan mode.
var scrollLocked = true
/// Feedback generator for the long press action.
let feedbackGenerator = UISelectionFeedbackGenerator()
lazy var possibleEndpoints = [CGPoint(x: consoleSize.width / 2 + 12,
y: UIApplication.shared.statusBarHeight + consoleSize.height / 2 + 5),
CGPoint(x: UIScreen.size.width - consoleSize.width / 2 - 12,
y: UIApplication.shared.statusBarHeight + consoleSize.height / 2 + 5),
CGPoint(x: consoleSize.width / 2 + 12,
y: UIScreen.size.height - consoleSize.height / 2 - 56),
CGPoint(x: UIScreen.size.width - consoleSize.width / 2 - 12,
y: UIScreen.size.height - consoleSize.height / 2 - 56)]
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)]
} 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)]
}
}
lazy var initialViewLocation: CGPoint = .zero
override init() {
super.init()
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)
// 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.normal
consoleWindow?.isHidden = false
consoleWindow?.addSubview(consoleView)
UIWindow.swizzleStatusBarAppearanceOverride
}
// Configure console view.
consoleView.frame.size = consoleSize
consoleView.backgroundColor = .black
consoleView.layer.shadowRadius = 16
@@ -73,32 +136,41 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
consoleView.center = possibleEndpoints.first!
consoleView.alpha = 0
consoleView.layer.borderWidth = 1
consoleView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
consoleView.layer.cornerRadius = 19
consoleView.layer.cornerRadius = 22
consoleView.layer.cornerCurve = .continuous
let borderView = UIView()
borderView.frame = CGRect(x: -1, y: -1,
width: consoleSize.width + 2,
height: consoleSize.height + 2)
borderView.layer.borderWidth = 1
borderView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
borderView.layer.cornerRadius = consoleView.layer.cornerRadius + 1
borderView.layer.cornerCurve = .continuous
borderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
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
consoleView.addSubview(consoleTextView)
consoleTextView.layer.cornerRadius = consoleView.layer.cornerRadius - 2
consoleTextView.layer.cornerCurve = .continuous
// Configure gesture recognizers.
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(consolePiPPanner(recognizer:)))
panRecognizer.maximumNumberOfTouches = 1
panRecognizer.delegate = self
let tapRecognizer = UITapStartEndGestureRecognizer(target: self, action: #selector(consolePiPTapStartEnd(recognizer:)))
tapRecognizer.delegate = self
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(recognizer:)))
longPressRecognizer.minimumPressDuration = 0.1
consoleView.addGestureRecognizer(panRecognizer)
@@ -106,56 +178,184 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
consoleView.addGestureRecognizer(longPressRecognizer)
// Prepare menu button.
let diameter = CGFloat(25)
let diameter = CGFloat(28)
menuButton = UIButton(frame: CGRect(x: consoleView.bounds.width - diameter - (consoleView.layer.cornerRadius - diameter / 2),
y: consoleView.bounds.height - diameter - (consoleView.layer.cornerRadius - diameter / 2),
width: diameter, height: diameter))
menuButton.layer.cornerRadius = diameter / 2
menuButton.backgroundColor = UIColor(white: 1, alpha: 0.20)
// This tuned button frame is used to adjust where the menu appears.
menuButton = UIButton(frame: CGRect(x: consoleView.bounds.width - 44,
y: consoleView.bounds.height - 36,
width: 44,
height: 36 + 4 /*Offests the context menu by the desired amount*/))
menuButton.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin]
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16)))
ellipsisImage.frame.size = menuButton!.bounds.size
let circleFrame = CGRect(
x: menuButton.bounds.width - diameter - (consoleView.layer.cornerRadius - diameter / 2),
y: menuButton.bounds.height - diameter - (consoleView.layer.cornerRadius - diameter / 2) - 4,
width: diameter, height: diameter)
let circle = UIView(frame: circleFrame)
circle.backgroundColor = UIColor(white: 0.2, alpha: 0.95)
circle.layer.cornerRadius = diameter / 2
circle.isUserInteractionEnabled = false
menuButton.addSubview(circle)
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17)))
ellipsisImage.frame.size = circle.bounds.size
ellipsisImage.contentMode = .center
menuButton.addSubview(ellipsisImage)
circle.addSubview(ellipsisImage)
menuButton.tintColor = UIColor(white: 1, alpha: 0.75)
menuButton.menu = makeMenu()
menuButton.showsMenuAsPrimaryAction = true
consoleView.addSubview(menuButton!)
consoleView.addSubview(menuButton)
UIView.swizzleDebugBehaviour
print(consoleView)
print(menuButton)
print("Hello, world!")
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
public var isVisible = false {
/// 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
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) {
if currentText == "" {
currentText = "\(items)"
} else {
currentText = currentText + "\n\(items)"
}
}
/// Clear text in the console view.
public func clear() {
currentText = ""
}
/// Copy the console view text to the device's clipboard.
public func copy() {
UIPasteboard.general.string = consoleTextView.text
}
// 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
UIView.swizzleDebugBehaviour_UNTRACKABLE_TOGGLE()
guard debugBordersEnabled else {
GLOBAL_BORDER_TRACKERS.forEach {
$0.deactivate()
@@ -181,69 +381,152 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
}
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 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()
}
}
public func print(_ items: Any) {
func setAttributedText(_ string: String) {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 7
let attributes: [NSAttributedString.Key: Any] = [
.paragraphStyle: paragraphStyle,
.foregroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 7, weight: .semibold, design: .monospaced)
.font: UIFont.systemFont(ofSize: fontSize, weight: .semibold, design: .monospaced)
]
let string: String = {
if consoleTextView.attributedText.string == "" {
return "\(items)"
} else {
return "\(items)\n" + consoleTextView.text
}
}()
consoleTextView.attributedText = NSAttributedString(string: string, attributes: attributes)
}
public func clear() {
consoleTextView.text = ""
}
func makeMenu() -> UIMenu {
let copy = UIAction(title: "Copy",
image: UIImage(systemName: "doc.on.doc"), handler: { _ in
self.copy()
})
let resize = UIAction(title: "Resize Console",
image: UIImage(systemName: "arrow.left.and.right.square"), handler: { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
ResizeController.shared.isActive.toggle()
ResizeController.shared.platterView.reveal()
}
})
let clear = UIAction(title: "Clear Console",
image: UIImage(systemName: "xmark.square"), handler: { _ in
self.clear()
})
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 respring = UIAction(title: "Restart SpringBoard",
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: "_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) {
@@ -257,8 +540,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
animator.startAnimation()
})
let debugActions = UIMenu(title: "", options: .displayInline,
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
children: [viewFrames, systemReport, displayReport, respring])])
return UIMenu(title: "", children: [viewFrames, respring])
var menuContent: [UIMenuElement] = []
if consoleTextView.text != "" {
menuContent.append(contentsOf: [copy, consoleActions])
} else {
menuContent.append(resize)
}
menuContent.append(debugActions)
return UIMenu(title: "", children: menuContent)
}
@objc func longPressAction(recognizer: UILongPressGestureRecognizer) {
@@ -390,9 +685,10 @@ class ConsoleWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)!
return hitView.isKind(of: ConsoleWindow.self) ? nil : hitView
if let hitView = super.hitTest(point, with: event) {
return hitView.isKind(of: ConsoleWindow.self) ? nil : hitView
}
return super.hitTest(point, with: event)
}
}
@@ -414,20 +710,18 @@ public class UITapStartEndGestureRecognizer: UITapGestureRecognizer {
// MARK: Fun hacks!
extension UIView {
/// Swizzle UIView to use custom frame system when needed.
static let swizzleDebugBehaviour: Void = {
static func swizzleDebugBehaviour_UNTRACKABLE_TOGGLE() {
guard let originalMethod = class_getInstanceMethod(UIView.self, #selector(layoutSubviews)),
let swizzledMethod = class_getInstanceMethod(UIView.self, #selector(swizzled_layoutSubviews)) else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}()
}
@objc func swizzled_layoutSubviews() {
swizzled_layoutSubviews()
if GLOBAL_DEBUG_BORDERS {
let tracker = BorderManager(view: self)
GLOBAL_BORDER_TRACKERS.append(tracker)
tracker.activate()
}
let tracker = BorderManager(view: self)
GLOBAL_BORDER_TRACKERS.append(tracker)
tracker.activate()
}
}
@@ -435,7 +729,7 @@ extension UIWindow {
/// Make sure this window does not have control over the status bar appearance.
static let swizzleStatusBarAppearanceOverride: Void = {
guard let originalMethod = class_getInstanceMethod(UIWindow.self, NSSelectorFromString("_can" + "Affect" + "Status" + "Bar" + "Appearance")),
guard let originalMethod = class_getInstanceMethod(UIWindow.self, NSSelectorFromString("_can" + "Affect" + "Sta" + "tus" + "Bar" + "Appe" + "arance")),
let swizzledMethod = class_getInstanceMethod(UIWindow.self, #selector(swizzled_statusBarAppearance))
else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
@@ -446,4 +740,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
}
}
}
}
+621
View File
@@ -0,0 +1,621 @@
//
// ResizeController.swift
//
// Created by Duraid Abdul.
// Copyright © 2021 Duraid Abdul. All rights reserved.
//
import UIKit
class ResizeController {
public static let shared = ResizeController()
lazy var platterView = PlatterView(frame: .zero)
lazy var consoleCenterPoint = CGPoint(x: (UIScreen.main.nativeBounds.width / 2).rounded() / UIScreen.main.scale,
y: (UIScreen.main.nativeBounds.height / 2).rounded() / UIScreen.main.scale
+ (UIScreen.hasRoundedCorners ? 0 : 24))
lazy var consoleOutlineView: UIView = {
let consoleViewReference = LCManager.shared.consoleView
let view = UIView()
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGreen.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light)).cgColor
view.layer.cornerRadius = consoleViewReference.layer.cornerRadius + 6
view.layer.cornerCurve = .continuous
view.alpha = 0
consoleViewReference.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: consoleViewReference.leadingAnchor, constant: -6),
view.trailingAnchor.constraint(equalTo: consoleViewReference.trailingAnchor, constant: 6),
view.topAnchor.constraint(equalTo: consoleViewReference.topAnchor, constant: -6),
view.bottomAnchor.constraint(equalTo: consoleViewReference.bottomAnchor, constant: 6)
])
return view
}()
lazy var bottomGrabberPillView = UIView()
lazy var bottomGrabber: UIView = {
let view = UIView()
LCManager.shared.consoleWindow?.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: 116),
view.heightAnchor.constraint(equalToConstant: 46),
view.centerXAnchor.constraint(equalTo: consoleOutlineView.centerXAnchor),
view.topAnchor.constraint(equalTo: consoleOutlineView.bottomAnchor, constant: -18)
])
bottomGrabberPillView.frame = CGRect(x: 58 - 18, y: 25, width: 36, height: 5)
bottomGrabberPillView.backgroundColor = UIColor.label
bottomGrabberPillView.alpha = 0.3
bottomGrabberPillView.layer.cornerRadius = 2.5
bottomGrabberPillView.layer.cornerCurve = .continuous
view.addSubview(bottomGrabberPillView)
let verticalPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(verticalPanner(recognizer:)))
verticalPanGestureRecognizer.maximumNumberOfTouches = 1
view.addGestureRecognizer(verticalPanGestureRecognizer)
view.alpha = 0
return view
}()
lazy var rightGrabberPillView = UIView()
lazy var rightGrabber: UIView = {
let view = UIView()
LCManager.shared.consoleWindow?.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: 46),
view.heightAnchor.constraint(equalToConstant: 116),
view.centerYAnchor.constraint(equalTo: consoleOutlineView.centerYAnchor),
view.leftAnchor.constraint(equalTo: consoleOutlineView.rightAnchor, constant: -18)
])
rightGrabberPillView.frame = CGRect(x: 25, y: 58 - 18, width: 5, height: 36)
rightGrabberPillView.backgroundColor = UIColor.label
rightGrabberPillView.alpha = 0.3
rightGrabberPillView.layer.cornerRadius = 2.5
rightGrabberPillView.layer.cornerCurve = .continuous
view.addSubview(rightGrabberPillView)
let horizontalPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(horizontalPanner(recognizer:)))
horizontalPanGestureRecognizer.maximumNumberOfTouches = 1
view.addGestureRecognizer(horizontalPanGestureRecognizer)
view.alpha = 0
return view
}()
var isActive: Bool = false {
didSet {
guard isActive != oldValue else { return }
// Initialize views outside of animation.
_ = platterView
_ = consoleOutlineView
_ = bottomGrabber
_ = rightGrabber
// Ensure initial autolayout is performed unanimated.
LCManager.shared.consoleWindow?.layoutIfNeeded()
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
}
// Ensure background color animates in right the first time.
LCManager.shared.consoleWindow?.backgroundColor = .clear
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
LCManager.shared.consoleView.center = self.consoleCenterPoint
// Update grabbers (layout constraints)
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.menuButton.alpha = 0
LCManager.shared.consoleWindow?.backgroundColor = UIColor(dynamicProvider: { traitCollection in
UIColor(white: 0, alpha: traitCollection.userInterfaceStyle == .light ? 0.1 : 0.3)
})
}.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
consoleOutlineView.alpha = 1
}.startAnimation(afterDelay: 0.3)
bottomGrabber.transform = .init(translationX: 0, y: -5)
rightGrabber.transform = .init(translationX: -5, y: 0)
UIViewPropertyAnimator(duration: 1, dampingRatio: 1) { [self] in
bottomGrabber.alpha = 1
rightGrabber.alpha = 1
bottomGrabber.transform = .identity
rightGrabber.transform = .identity
}.startAnimation(afterDelay: 0.3)
LCManager.shared.panRecognizer.isEnabled = false
LCManager.shared.longPressRecognizer.isEnabled = false
// Activate full screen button.
consoleOutlineView.isUserInteractionEnabled = true
} else {
LCManager.shared.consoleView.layer.shadowOpacity = 0.5
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
LCManager.shared.consoleView.center = LCManager.shared.possibleEndpoints.first!
// Update grabbers (layout constraints)
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.menuButton.alpha = 1
LCManager.shared.consoleWindow?.backgroundColor = .clear
}.startAnimation()
UIViewPropertyAnimator(duration: 0.2, dampingRatio: 1) { [self] in
consoleOutlineView.alpha = 0
bottomGrabber.alpha = 0
rightGrabber.alpha = 0
}.startAnimation()
LCManager.shared.panRecognizer.isEnabled = true
LCManager.shared.longPressRecognizer.isEnabled = true
// Deactivate full screen button.
consoleOutlineView.isUserInteractionEnabled = false
}
}
}
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 minHeight = Self.kMinConsoleHeight
let maxHeight = Self.kMaxConsoleHeight
switch recognizer.state {
case .began:
initialHeight = LCManager.shared.consoleSize.height
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
bottomGrabberPillView.alpha = 0.6
}.startAnimation()
case .changed:
let resolvedHeight: CGFloat = {
let initialEstimate = initialHeight + 2 * translation.y
if initialEstimate <= maxHeight && initialEstimate > minHeight {
return initialEstimate
} else if initialEstimate > maxHeight {
var excess = initialEstimate - maxHeight
excess = 25 * log(1/25 * excess + 1)
return maxHeight + excess
} else {
var excess = minHeight - initialEstimate
excess = 7 * log(1/7 * excess + 1)
return minHeight - excess
}
}()
LCManager.shared.consoleSize.height = resolvedHeight
LCManager.shared.consoleView.center.y = consoleCenterPoint.y
case .ended, .cancelled:
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
if LCManager.shared.consoleSize.height > maxHeight {
LCManager.shared.consoleSize.height = maxHeight
}
if LCManager.shared.consoleSize.height < minHeight {
LCManager.shared.consoleSize.height = minHeight
}
LCManager.shared.consoleView.center.y = self.consoleCenterPoint.y
// Animate autolayout updates.
LCManager.shared.consoleWindow?.layoutIfNeeded()
}.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
bottomGrabberPillView.alpha = 0.3
}.startAnimation()
default: break
}
}
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 minWidth = Self.kMinConsoleWidth
let maxWidth = Self.kMaxConsoleWidth
switch recognizer.state {
case .began:
initialWidth = LCManager.shared.consoleSize.width
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
rightGrabberPillView.alpha = 0.6
}.startAnimation()
case .changed:
let resolvedWidth: CGFloat = {
let initialEstimate = initialWidth + 2 * translation.x
if initialEstimate <= maxWidth && initialEstimate > minWidth {
return initialEstimate
} else if initialEstimate > maxWidth {
var excess = initialEstimate - maxWidth
excess = 25 * log(1/25 * excess + 1)
return maxWidth + excess
} else {
var excess = minWidth - initialEstimate
excess = 7 * log(1/7 * excess + 1)
return minWidth - excess
}
}()
LCManager.shared.consoleSize.width = resolvedWidth
LCManager.shared.consoleView.center.x = (UIScreen.main.nativeBounds.width * 1/2).rounded() / UIScreen.main.scale
case .ended, .cancelled:
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
if LCManager.shared.consoleSize.width > maxWidth {
LCManager.shared.consoleSize.width = maxWidth
}
if LCManager.shared.consoleSize.width < minWidth {
LCManager.shared.consoleSize.width = minWidth
}
LCManager.shared.consoleView.center.x = (UIScreen.main.nativeBounds.width * 1/2).rounded() / UIScreen.main.scale
// Animate autolayout updates.
LCManager.shared.consoleWindow?.layoutIfNeeded()
}.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
rightGrabberPillView.alpha = 0.3
}.startAnimation()
default: break
}
}
}
class PlatterView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.frame.size = UIScreen.portraitSize
// Make sure bottom doesn't show on upwards pan.
self.frame.size.height += 50
self.frame.origin = possibleEndpoints[1]
autoresizingMask = [.flexibleWidth, .flexibleHeight]
layer.shadowRadius = 10
layer.shadowOpacity = 0.125
layer.shadowOffset = CGSize(width: 0, height: 0)
layer.borderColor = dynamicBorderColor.cgColor
layer.borderWidth = 1 / UIScreen.main.scale
layer.cornerRadius = 30
layer.cornerCurve = .continuous
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
blurView.layer.cornerRadius = 30
blurView.layer.cornerCurve = .continuous
blurView.clipsToBounds = true
blurView.frame = bounds
addSubview(blurView)
LCManager.shared.consoleWindow?.addSubview(self)
LCManager.shared.consoleWindow?.sendSubviewToBack(self)
_ = backgroundButton
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(platterPanner(recognizer:)))
panRecognizer.maximumNumberOfTouches = 1
addGestureRecognizer(panRecognizer)
let grabber = UIView()
grabber.frame.size = CGSize(width: 36, height: 5)
grabber.frame.origin.y = 10
grabber.center.x = bounds.width / 2
grabber.backgroundColor = .label
grabber.alpha = 0.1
grabber.layer.cornerRadius = 2.5
grabber.layer.cornerCurve = .continuous
addSubview(grabber)
let titleLabel = UILabel()
titleLabel.text = "Resize Console"
titleLabel.font = .systemFont(ofSize: 30, weight: .bold)
titleLabel.sizeToFit()
titleLabel.center.x = bounds.width / 2
titleLabel.frame.origin.y = 28
titleLabel.roundOriginToPixel()
addSubview(titleLabel)
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
subtitleLabel.frame.origin.y = titleLabel.frame.maxY + 8
subtitleLabel.roundOriginToPixel()
addSubview(subtitleLabel)
addSubview(resetButton)
resetButton.center = CGPoint(x: UIScreen.portraitSize.width / 2 - 74,
y: UIScreen.portraitSize.height - possibleEndpoints[0].y * 2)
resetButton.roundOriginToPixel()
addSubview(doneButton)
doneButton.center = CGPoint(x: UIScreen.portraitSize.width / 2 + 74,
y: UIScreen.portraitSize.height - possibleEndpoints[0].y * 2)
doneButton.roundOriginToPixel()
}
lazy var backgroundButton: UIButton = {
let backgroundButton = UIButton(primaryAction: UIAction(handler: { _ in
ResizeController.shared.isActive = false
self.dismiss()
}))
backgroundButton.frame.size = CGSize(width: self.frame.size.width, height: possibleEndpoints[0].y + 30)
LCManager.shared.consoleWindow?.addSubview(backgroundButton)
LCManager.shared.consoleWindow?.sendSubviewToBack(backgroundButton)
return backgroundButton
}()
lazy var doneButton: UIButton = {
let button = UIButton(type: .custom)
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)
button.frame.size = CGSize(width: 116, height: 52)
button.layer.cornerRadius = 20
button.layer.cornerCurve = .continuous
button.addAction(UIAction(handler: { _ in
ResizeController.shared.isActive = false
self.dismiss()
}), for: .touchUpInside)
button.addActions(highlightAction: UIAction(handler: { _ in
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) {
button.alpha = 0.6
}.startAnimation()
}), unhighlightAction: UIAction(handler: { _ in
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
button.alpha = 1
}.startAnimation()
}))
return button
}()
lazy var resetButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = UIColor(dynamicProvider: { traitCollection in
if traitCollection.userInterfaceStyle == .dark {
return UIColor(white: 1, alpha: 0.125)
} else {
return UIColor(white: 0, alpha: 0.1)
}
})
button.setTitle("Reset", for: .normal)
button.setTitleColor(.label, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
button.frame.size = CGSize(width: 116, height: 52)
button.layer.cornerRadius = 20
button.layer.cornerCurve = .continuous
button.addAction(UIAction(handler: { _ in
// 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 - 4
}
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
LCManager.shared.consoleSize = LCManager.shared.defaultConsoleSize
LCManager.shared.consoleView.center = ResizeController.shared.consoleCenterPoint
LCManager.shared.consoleWindow?.layoutIfNeeded()
}.startAnimation()
}), for: .touchUpInside)
button.addActions(highlightAction: UIAction(handler: { _ in
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) {
button.alpha = 0.6
}.startAnimation()
}), unhighlightAction: UIAction(handler: { _ in
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
button.alpha = 1
}.startAnimation()
}))
return button
}()
func reveal() {
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
self.frame.origin = self.possibleEndpoints[0]
}.startAnimation()
backgroundButton.isHidden = false
}
func dismiss() {
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
self.frame.origin = self.possibleEndpoints[1]
}.startAnimation()
backgroundButton.isHidden = true
}
let dynamicBorderColor = UIColor(dynamicProvider: { traitCollection in
if traitCollection.userInterfaceStyle == .dark {
return UIColor(white: 1, alpha: 0.075)
} else {
return UIColor(white: 0, alpha: 0.125)
}
})
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
layer.borderColor = dynamicBorderColor.cgColor
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var possibleEndpoints = [CGPoint(x: 0, y: (UIScreen.hasRoundedCorners ? 44 : -8) + 63), CGPoint(x: 0, y: UIScreen.portraitSize.height + 5)]
var initialPlatterOriginY = CGFloat.zero
@objc func platterPanner(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: superview)
let velocity = recognizer.velocity(in: superview)
switch recognizer.state {
case .began:
initialPlatterOriginY = frame.origin.y
case .changed:
let resolvedOriginY: CGFloat = {
let initialEstimate = initialPlatterOriginY + translation.y
if initialEstimate >= possibleEndpoints[0].y {
// Stick buttons to bottom.
[doneButton, resetButton,
ResizeController.shared.bottomGrabber, ResizeController.shared.rightGrabber,
LCManager.shared.consoleView
].forEach {
$0.transform = .identity
}
return initialEstimate
} else {
var excess = possibleEndpoints[0].y - initialEstimate
excess = 10 * log(1/10 * excess + 1)
// Stick buttons to bottom.
doneButton.transform = .init(translationX: 0, y: excess)
resetButton.transform = .init(translationX: 0, y: excess)
ResizeController.shared.bottomGrabber.transform = .init(translationX: 0, y: -excess / 2.5)
ResizeController.shared.rightGrabber.transform = .init(translationX: 0, y: -excess / 2)
LCManager.shared.consoleView.transform = .init(translationX: 0, y: -excess / 2)
return possibleEndpoints[0].y - excess
}
}()
if frame.origin.y > possibleEndpoints[0].y + 40 {
ResizeController.shared.isActive = false
} else {
ResizeController.shared.isActive = true
}
frame.origin.y = resolvedOriginY
case .ended, .cancelled:
// After the PiP is thrown, determine the best corner and re-target it there.
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let projectedPosition = CGPoint(
x: 0,
y: frame.origin.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
)
let nearestTargetPosition = nearestTargetTo(projectedPosition, possibleTargets: possibleEndpoints)
let relativeInitialVelocity = CGVector(
dx: 0,
dy: frame.origin.y >= possibleEndpoints[0].y
? relativeVelocity(forVelocity: velocity.y, from: frame.origin.y, to: nearestTargetPosition.y)
: 0
)
let timingParameters = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)
let positionAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
positionAnimator.addAnimations { [self] in
frame.origin = nearestTargetPosition
[doneButton, resetButton,
ResizeController.shared.bottomGrabber, ResizeController.shared.rightGrabber,
LCManager.shared.consoleView
].forEach {
$0.transform = .identity
}
}
positionAnimator.startAnimation()
if nearestTargetPosition == possibleEndpoints[1] {
ResizeController.shared.isActive = false
backgroundButton.isHidden = true
} else {
ResizeController.shared.isActive = true
}
default: break
}
}
}
+108
View File
@@ -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: "/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: "CacheE" + "xtra") as? NSDictionary
}()
// Device marketing name.
lazy var gestaltMarketingName: Any = gestaltCacheExtra?.value(forKey: "Z/dqyWS6OZ" + "TRy10UcmUAhw") ?? "Unknown"
// iBoot (second-stage loader) version.
lazy var gestaltFirmwareVersion: Any = gestaltCacheExtra?.value(forKey: "LeSRsiLoJC" + "Mhjn6nd6GWbQ") ?? "Unknown"
// CPU architecture.
lazy var gestaltArchitecture: Any = gestaltCacheExtra?.value(forKey: "k7QIBwZJJO" + "Vw+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: "h9jDsbgj7xI" + "VeIQ8S3/X3Q") ?? modelIdentifier
// Fallback in case gestaltModelIdentifier doesn't return a value.
var modelIdentifier: String {
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"
}
var kernel: String {
var size = 0
sysctlbyname("ker" + "n.os" + "type", nil, &size, nil, 0)
var string = [CChar](repeating: 0, count: Int(size))
sysctlbyname("ker" + "n.os" + "type", &string, &size, nil, 0)
return String(cString: string)
}
var kernelVersion: String {
var size = 0
sysctlbyname("ker" + "n.os" + "release", nil, &size, nil, 0)
var string = [CChar](repeating: 0, count: Int(size))
sysctlbyname("ker" + "n.os" + "release", &string, &size, nil, 0)
return String(cString: string)
}
var compileDate: String {
var size = 0
sysctlbyname("ker" + "n.ve" + "rsion", nil, &size, nil, 0)
var string = [CChar](repeating: 0, count: Int(size))
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)
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"
}
}