Compare commits

...

17 Commits

Author SHA1 Message Date
Duraid Abdul 2c5d5e018a Fix share sheet crash 2022-12-04 16:19:33 -07:00
Duraid Abdul 6413eb8a81 Add a custom menu to the console’s menu 2022-11-21 23:44:08 -07:00
Duraid Abdul 1dcb6e3a57 Fix build failure 2022-10-16 19:21:53 -06:00
Duraid Abdul bfc3bc4fd2 Updated action icons, removed ConsoleWindow. 2022-10-16 19:09:36 -06:00
Duraid Abdul 74556927be Fix crash when invoking keyboard early 2022-10-15 12:49:16 -06:00
Duraid Abdul fee9e30df9 Fix simulator crash 2022-10-15 12:48:52 -06:00
Duraid Abdul 5e2b5feca4 Fix blocked touches on iPad 2022-10-15 12:48:23 -06:00
Duraid Abdul 3ffdde8904 Fix Xcode 13 build failure 2022-09-20 10:39:18 -06:00
Duraid Abdul 63ebc8ed31 Fix border view frame 2022-09-18 23:39:48 -06:00
Duraid Abdul 64e18b18fc Fix keybaord avoidance 2022-09-18 23:31:00 -06:00
Duraid Abdul 9dcfa5accd iPhone 14 Pro support 2022-09-18 21:05:11 -06:00
Duraid Abdul adbb2d763f Update README.md 2022-05-14 19:15:47 -07:00
Duraid Abdul c08c26f4a7 Update README.md 2022-05-14 19:15:18 -07:00
Duraid Abdul d995119198 Update LCManager.swift 2022-02-01 12:26:52 -08:00
Duraid Abdul b4d7c06432 Update LCManager.swift 2022-01-26 21:02:54 -08:00
Duraid Abdul a7b95a4379 Update LCManager.swift 2022-01-26 02:18:26 -08:00
Duraid Abdul 3efe25f804 Update LCManager.swift 2022-01-25 17:15:30 -08:00
3 changed files with 425 additions and 286 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ Welcome to LocalConsole! This Swift Package makes on-device debugging easy with
2. Paste the following into the URL field: https://github.com/duraidabdul/LocalConsole/
3. Once the package dependancy has been added, import LocalConsole and create an easily accessible global instance of ```Console.shared```.
3. Once the package dependancy has been added, import LocalConsole and create an easily accessible global instance of ```LCManager.shared```.
```swift
import LocalConsole
+406 -267
View File
@@ -136,11 +136,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
}
/// Strong reference keeps the window alive.
var consoleWindow: ConsoleWindow?
/// Enables rotation.
lazy var viewController = ConsoleViewController()
lazy var consoleViewController = ConsoleViewController()
/// Note: The console always needs a parent view controller in order to display context menus. In this case, the parent controller will be the viewController.
lazy var consoleView = UIView()
@@ -149,52 +145,52 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
lazy var consoleTextView = InvertedTextView()
/// Button that reveals menu.
lazy var menuButton = UIButton()
lazy var menuButton = ConsoleMenuButton()
/// Tracks whether the PiP console is in text view scroll mode or pan mode.
var scrollLocked = true
/// Feedback generator for the long press action.
lazy var feedbackGenerator = UISelectionFeedbackGenerator()
lazy var feedbackGenerator = UIImpactFeedbackGenerator(style: .soft)
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] {
let screenSize = viewController.view.frame.size
let screenSize = consoleViewController.view.frame.size
// Must check for portrait mode manually here. UIDevice was reporting orientation incorrectly before.
let isPortraitNotchedPhone = UIDevice.current.hasNotch && viewController.view.frame.size.width < viewController.view.frame.size.height
let isPortraitNotchedPhone = UIDevice.current.hasNotch && consoleViewController.view.frame.size.width < consoleViewController.view.frame.size.height
// Fix incorrect reported orientation on phone.
let isLandscapePhone = (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) && UIDevice.current.userInterfaceIdiom == .phone
let isLandscapePhone = UIDevice.current.userInterfaceIdiom == .phone && consoleViewController.view.frame.width > consoleViewController.view.frame.height
let isLandscapeLeftNotchedPhone = UIDevice.current.orientation == .landscapeLeft
&& UIDevice.current.userInterfaceIdiom == .phone
&& UIDevice.current.hasNotch
&& isLandscapePhone
let isLandscapeRightNotchedPhone = UIDevice.current.orientation == .landscapeRight
&& UIDevice.current.userInterfaceIdiom == .phone
&& UIDevice.current.hasNotch
&& isLandscapePhone
let safeAreaInsets = consoleViewController.view.safeAreaInsets
let leftEndpointX = consoleSize.width / 2 + safeAreaInsets.left + (isLandscapePhone ? 4 : 12) + (isLandscapeRightNotchedPhone ? -16 : 0)
let rightEndpointX = screenSize.width - (consoleSize.width / 2 + safeAreaInsets.right) - (isLandscapePhone ? 4 : 12) + (isLandscapeLeftNotchedPhone ? 16 : 0)
let topEndpointY = consoleSize.height / 2 + safeAreaInsets.top + 12 + (isPortraitNotchedPhone ? -10 : 0)
let bottomEndpointY = screenSize.height - consoleSize.height / 2 - (keyboardHeight ?? safeAreaInsets.bottom) - 12 + (isLandscapePhone ? 10 : 0)
if consoleSize.width < screenSize.width - 112 {
// Four endpoints, one for each corner.
var endpoints = [
// Top endpoints.
CGPoint(x: consoleSize.width / 2 + 12 + (isLandscapeLeftNotchedPhone ? 40 : isLandscapePhone ? 12 : 0),
y: (isPortraitNotchedPhone ? 38 : isLandscapePhone ? 0 : 16) + consoleSize.height / 2 + 12),
CGPoint(x: screenSize.width - consoleSize.width / 2 - 12 - (isLandscapeRightNotchedPhone ? 40 : isLandscapePhone ? 12 : 0),
y: (isPortraitNotchedPhone ? 38 : isLandscapePhone ? 0 : 16) + consoleSize.height / 2 + 12),
// Bottom endpoints.
CGPoint(x: consoleSize.width / 2 + 12 + (isLandscapeLeftNotchedPhone ? 40 : isLandscapePhone ? 12 : 0),
y: screenSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
CGPoint(x: screenSize.width - consoleSize.width / 2 - 12 - (isLandscapeRightNotchedPhone ? 40 : isLandscapePhone ? 12 : 02),
y: screenSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
CGPoint(x: leftEndpointX, y: topEndpointY),
CGPoint(x: rightEndpointX, y: topEndpointY),
CGPoint(x: leftEndpointX, y: bottomEndpointY),
CGPoint(x: rightEndpointX, y: bottomEndpointY),
]
if consoleView.frame.minX <= 0 {
@@ -231,12 +227,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
return endpoints
} else {
// Two endpoints, one for the top, one for the bottom..
var endpoints = [CGPoint(x: screenSize.width / 2,
y: (UIScreen.hasRoundedCorners ? 38 : 16) + consoleSize.height / 2 + 12),
CGPoint(x: screenSize.width / 2,
y: screenSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
var endpoints = [
CGPoint(x: screenSize.width / 2, y: topEndpointY),
CGPoint(x: screenSize.width / 2, y: bottomEndpointY)
]
if consoleView.frame.minX <= 0 {
@@ -281,10 +276,14 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
let _ = lumaView
borderView.frame = CGRect(x: -1, y: -1,
width: consoleSize.width + 2,
height: consoleSize.height + 2)
borderView.layer.borderWidth = 1
let borderWidth = 2 - 1 / consoleView.traitCollection.displayScale
borderView.frame = CGRect(
x: -borderWidth, y: -borderWidth,
width: consoleSize.width + 2 * borderWidth,
height: consoleSize.height + 2 * borderWidth
)
borderView.layer.borderWidth = borderWidth
borderView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
borderView.layer.cornerRadius = consoleView.layer.cornerRadius + 1
borderView.layer.cornerCurve = .continuous
@@ -312,7 +311,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
let tapRecognizer = UITapStartEndGestureRecognizer(target: self, action: #selector(consolePiPTapStartEnd(recognizer:)))
tapRecognizer.delegate = self
longPressRecognizer.minimumPressDuration = 0.1
longPressRecognizer.minimumPressDuration = 0.3
consoleView.addGestureRecognizer(panRecognizer)
consoleView.addGestureRecognizer(tapRecognizer)
@@ -322,10 +321,12 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
let diameter = CGFloat(30)
// 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.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 circleFrame = CGRect(
@@ -339,13 +340,17 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
circle.isUserInteractionEnabled = false
menuButton.addSubview(circle)
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)))
let ellipsisImage = UIImageView(
image: UIImage(
systemName: "ellipsis",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
)
)
ellipsisImage.frame.size = circle.bounds.size
ellipsisImage.contentMode = .center
circle.addSubview(ellipsisImage)
menuButton.tintColor = UIColor(white: 1, alpha: 0.75)
menuButton.tintColor = UIColor(white: 1, alpha: 0.8)
menuButton.menu = makeMenu()
menuButton.showsMenuAsPrimaryAction = true
consoleView.addSubview(menuButton)
@@ -356,15 +361,15 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
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
/// Adds a consoleViewController to the app's main window.
func configureConsoleViewController() {
var windowFound = false
// Update console cached based on last-cached origin.
func updateConsoleOrigin() {
snapToCachedEndpoint()
if consoleView.center.x < 0 || consoleView.center.x > viewController.view.frame.size.width {
if consoleView.center.x < 0 || consoleView.center.x > consoleViewController.view.frame.size.width {
grabberMode = true
scrollLocked = !grabberMode
@@ -376,43 +381,56 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
// Configure console window.
func fetchWindowScene() {
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
func fetchWindow() -> UIWindow? {
if #available(iOS 15.0, *) {
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
if let windowScene = windowScene as? UIWindowScene, let keyWindow = windowScene.keyWindow {
return keyWindow
}
return nil
} else {
return UIApplication.shared.windows.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
viewController.view = PassthroughView()
consoleWindow?.rootViewController = viewController
viewController.view.addSubview(consoleView)
UIWindow.swizzleStatusBarAppearanceOverride
updateConsoleOrigin()
}
func addConsoleToWindow(window: UIWindow) {
window.addSubview(consoleViewController.view)
window.rootViewController?.addChild(consoleViewController)
consoleViewController.view = PassthroughView()
consoleViewController.view.addSubview(consoleView)
consoleViewController.view.frame = window.bounds
consoleViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
updateConsoleOrigin()
SwizzleTool().swizzleContextMenuReverseOrder()
// Ensure console view always stays above other views.
SwizzleTool().swizzleDidAddSubview {
window.bringSubviewToFront(self.consoleViewController.view)
}
}
/// 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 }
guard !windowFound else { return }
fetchWindowScene()
if let window = fetchWindow() {
windowFound = true
addConsoleToWindow(window: window)
}
if isVisible {
isVisible = false
@@ -424,8 +442,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
func snapToCachedEndpoint() {
let cachedConsolePosition = CGPoint(x: UserDefaults.standard.object(forKey: "LocalConsole.X") as? CGFloat ?? possibleEndpoints.first!.x,
y: UserDefaults.standard.object(forKey: "LocalConsole.Y") as? CGFloat ?? possibleEndpoints.first!.y)
let cachedConsolePosition = CGPoint(
x: UserDefaults.standard.object(forKey: "LocalConsole.X") as? CGFloat ?? possibleEndpoints.first!.x,
y: UserDefaults.standard.object(forKey: "LocalConsole.Y") as? CGFloat ?? possibleEndpoints.first!.y
)
consoleView.center = cachedConsolePosition // Update console center so possibleEndpoints are calculated correctly.
consoleView.center = nearestTargetTo(cachedConsolePosition, possibleTargets: possibleEndpoints)
@@ -441,7 +461,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
if !isConsoleConfigured {
DispatchQueue.main.async { [self] in
configureWindow()
configureConsoleViewController()
configureConsole()
isConsoleConfigured = true
}
@@ -476,13 +496,18 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
}
// Specify a UIMenu or UIAction to be included in the console's main menu.
public var menu: UIMenuElement? = nil {
didSet {
menuButton.menu = makeMenu()
}
}
var grabberMode: Bool = false {
didSet {
guard oldValue != grabberMode else { return }
if grabberMode {
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
lumaHeightAnchor.constant = consoleView.frame.size.height
consoleView.layoutIfNeeded()
@@ -500,7 +525,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
lumaWidthAnchor.constant = -34
lumaHeightAnchor.constant = 96
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
lumaView.layer.cornerRadius = 8
lumaView.layer.cornerRadius = 10
consoleView.layoutIfNeeded()
}.startAnimation(afterDelay: 0.06)
@@ -532,6 +557,60 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
}
@objc func handleDeviceOrientationChange(previousSize: CGSize) {
// Cancel the panner console is being panned to allow for location manipulation.
[LCManager.shared.panRecognizer, LCManager.shared.longPressRecognizer].forEach {
$0.isEnabled.toggle(); $0.isEnabled.toggle()
}
if UIDevice.current.userInterfaceIdiom != .pad && ResizeController.shared.isActive {
ResizeController.shared.isActive = false
ResizeController.shared.platterView.dismiss()
}
if UIDevice.current.userInterfaceIdiom == .pad && ResizeController.shared.isActive {
DispatchQueue.main.async {
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
LCManager.shared.consoleView.center = ResizeController.shared.consoleCenterPoint
}.startAnimation(afterDelay: 0.05)
}
} else {
let consoleView = LCManager.shared.consoleView
let targetLocationEstimate: CGPoint = {
var xPosition = consoleView.center.x
var yPosition = consoleView.center.y
if consoleView.center.x > previousSize.width / 2 {
xPosition += consoleViewController.view.frame.width - previousSize.width
}
if consoleView.center.y > previousSize.height / 2 {
yPosition += consoleViewController.view.frame.height - previousSize.height
}
return CGPoint(x: xPosition, y: yPosition)
}()
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
consoleView.center = targetLocationEstimate
}.startAnimation(afterDelay: 0.05)
DispatchQueue.main.async {
// Update portrait orientation menu option for resize controller.
LCManager.shared.menuButton.menu = LCManager.shared.makeMenu()
// Reassess center of console based on target location estimate.
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
consoleView.center = nearestTargetTo(consoleView.center, possibleTargets: LCManager.shared.possibleEndpoints)
}.startAnimation(afterDelay: 0.05)
LCManager.shared.reassessGrabberMode()
}
}
}
var hasShortened = false
public var isCharacterLimitDisabled = false
@@ -580,10 +659,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
// MARK: Handle keyboard show/hide.
private var keyboardHeight: CGFloat? = nil {
didSet {
temporaryKeyboardHeightValueTracker = oldValue
if consoleView.center != possibleEndpoints[0] && consoleView.center != possibleEndpoints[1] {
if possibleEndpoints.count > 2, consoleView.center != possibleEndpoints[0] && consoleView.center != possibleEndpoints[1] {
let nearestTargetPosition = nearestTargetTo(consoleView.center, possibleTargets: possibleEndpoints.suffix(2))
UIViewPropertyAnimator(duration: 0.55, dampingRatio: 1) {
@@ -637,15 +715,23 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
var dynamicReportTimer: Timer? {
willSet { dynamicReportTimer?.invalidate() }
willSet {
timerInvalidationCounter = 0
dynamicReportTimer?.invalidate()
}
}
var timerInvalidationCounter = 0
func systemReport() {
DispatchQueue.main.async { [self] in
if currentText != "" { print("\n") }
dynamicReportTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
dynamicReportTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] timer in
guard consoleTextView.panGestureRecognizer.numberOfTouches == 0 else { return }
var _currentText = currentText
// To optimize performance, only scan the last 2500 characters of text for system report changes.
@@ -667,10 +753,19 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
if currentText != _currentText {
currentText = _currentText
timerInvalidationCounter = 0
} else {
// Invalidate the timer if there is no longer anything to update.
timer.invalidate()
timerInvalidationCounter += 1
// It has been 2 seconds and values have not changed.
if timerInvalidationCounter == 2 {
// Invalidate the timer if there is no longer anything to update.
dynamicReportTimer = nil
}
}
}
@@ -698,13 +793,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
if currentText != "" { print("\n") }
let safeAreaInsets = consoleViewController.view.safeAreaInsets ?? .zero
print(
"""
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))
Screen Size: \(UIScreen.main.bounds.size)
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))
Safe Area Insets: top: \(String(describing: safeAreaInsets.top))
left: \(String(describing: safeAreaInsets.left))
bottom: \(String(describing: safeAreaInsets.bottom))
right: \(String(describing: safeAreaInsets.right))
"""
)
}
@@ -746,34 +848,41 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
public var showAllUserDefaultsKeys = false
func makeMenu() -> UIMenu {
let share: UIAction = {
// Something here causes a crash < iOS 15. Fall back to copy text for iOS 15 and below.
if #available(iOS 16, *) {
return UIAction(title: "Share Text...", image: UIImage(systemName: "square.and.arrow.up")) { _ in
let activityViewController = UIActivityViewController(
activityItems: [self.consoleTextView.text ?? ""],
applicationActivities: nil
)
self.consoleViewController.present(activityViewController, animated: true)
}
} else {
return UIAction(title: "Copy Text", image: UIImage(systemName: "doc.on.doc")) { _ in
UIPasteboard.general.string = self.consoleTextView.text
}
}
}()
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
let resize = UIAction(title: "Resize Console", image: UIImage(systemName: "arrow.up.backward.and.arrow.down.forward")) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
ResizeController.shared.isActive.toggle()
ResizeController.shared.platterView.reveal()
}
})
}
// If device is phone in landscape, disable resize controller.
if UIDevice.current.userInterfaceIdiom == .phone && viewController.view.frame.width > viewController.view.frame.height {
if UIDevice.current.userInterfaceIdiom == .phone && consoleViewController.view.frame.width > consoleViewController.view.frame.height {
resize.attributes = .disabled
if #available(iOS 15, *) {
resize.subtitle = "Portrait Orientation Only"
}
}
let clear = UIAction(title: "Clear Console",
image: UIImage(systemName: "xmark.square"), handler: { _ in
let clear = UIAction(title: "Clear Console", image: UIImage(systemName: "delete.backward"), attributes: .destructive) { _ in
self.clear()
})
let consoleActions = UIMenu(title: "", options: .displayInline, children: [clear, resize])
}
var frameSymbol = "rectangle.3.offgrid"
@@ -806,11 +915,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}()
if keys.isEmpty {
actions.append(UIAction(title: "No Entries",
image: nil, attributes: .disabled, handler: { _ in }
))
actions.append(
UIAction(title: "No Entries", attributes: .disabled, handler: { _ in })
)
} else {
for key in keys.sorted(by: { $0 < $1 }) {
for key in keys.sorted(by: { $0.lowercased() < $1.lowercased() }) {
// Old LocalConsole Key Cleanup
guard !key.contains("LocalConsole_") else {
@@ -868,7 +977,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in
}))
self.viewController.present(alertController,
self.consoleViewController.present(alertController,
animated: true)
}
action.subtitle = "\(value)"
@@ -897,16 +1006,17 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
let viewFrames = UIAction(title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
image: UIImage(systemName: frameSymbol), handler: { _ in
let viewFrames = UIAction(
title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
image: UIImage(systemName: frameSymbol)
) { _ in
self.debugBordersEnabled.toggle()
self.menuButton.menu = self.makeMenu()
})
}
let systemReport = UIAction(title: "System Report",
image: UIImage(systemName: "cpu"), handler: { _ in
let systemReport = UIAction(title: "System Report", image: UIImage(systemName: "cpu")) { _ in
self.systemReport()
})
}
// Show the right glyph for the current device being used.
let deviceSymbol: String = {
@@ -930,13 +1040,15 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
}()
let displayReport = UIAction(title: "Display Report",
image: UIImage(systemName: deviceSymbol), handler: { _ in
let displayReport = UIAction(title: "Display Report", image: UIImage(systemName: deviceSymbol)) { _ in
self.displayReport()
})
}
let respring = UIAction(title: "Restart Spring" + "Board",
image: UIImage(systemName: "apps.iphone"), handler: { _ in
let terminateApplication = UIAction(title: "Terminate App", image: UIImage(systemName: "xmark"), attributes: .destructive) { _ in
UIApplication.shared.perform(NSSelectorFromString("terminateWithSuccess"))
}
let respring = UIAction(title: "Restart Spring" + "Board", image: UIImage(systemName: "arrowtriangle.backward"), attributes: .destructive) { _ in
guard let window = UIApplication.shared.windows.first else { return }
@@ -952,44 +1064,63 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
for _ in 0...1000 {
DispatchQueue.global(qos: .default).async {
// This will cause jetsam to terminate SpringBoard.
// This will cause jetsam to terminate backboardd.
while true {
window.snapshotView(afterScreenUpdates: false)
}
}
}
})
}
debugActions.append(contentsOf: [viewFrames, systemReport, displayReport, respring])
debugActions.append(contentsOf: [viewFrames, systemReport, displayReport])
let destructActions = [terminateApplication , respring]
let debugMenu = UIMenu(title: "", options: .displayInline,
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
children: debugActions)])
let debugMenu = UIMenu(
title: "Debug", image: UIImage(systemName: "ant"),
children: [
UIMenu(title: "", options: .displayInline, children: debugActions),
UIMenu(title: "", options: .displayInline, children: destructActions),
]
)
var menuContent: [UIMenuElement] = []
if consoleTextView.text != "" {
menuContent.append(contentsOf: [copy, consoleActions])
menuContent.append(contentsOf: [UIMenu(title: "", options: .displayInline, children: [share, resize])])
} else {
menuContent.append(resize)
menuContent.append(UIMenu(title: "", options: .displayInline, children: [resize]))
}
menuContent.append(debugMenu)
if let customMenu = menu {
menuContent.append(customMenu)
}
if consoleTextView.text != "" {
menuContent.append(UIMenu(title: "", options: .displayInline, children: [clear]))
}
return UIMenu(title: "", children: menuContent)
}
var consolePiPPopAnimator: UIViewPropertyAnimator?
@objc func longPressAction(recognizer: UILongPressGestureRecognizer) {
switch recognizer.state {
case .began:
guard !grabberMode else { return }
feedbackGenerator.selectionChanged()
feedbackGenerator.impactOccurred(intensity: 1)
scrollLocked = false
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
consolePiPPopAnimator = UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
consoleView.transform = .init(scaleX: 1.04, y: 1.04)
}
consolePiPPopAnimator?.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
consoleTextView.alpha = 0.5
menuButton.alpha = 0.5
}.startAnimation()
@@ -1024,7 +1155,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
initialViewLocation = consoleView.center
}
guard !scrollLocked else { return }
guard !scrollLocked else {
isPressed = false
return
}
let translation = recognizer.translation(in: consoleView.superview)
let velocity = recognizer.velocity(in: consoleView.superview)
@@ -1083,37 +1217,44 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
func reassessGrabberMode() {
if consoleView.frame.maxX > 30 && consoleView.frame.minX < viewController.view.frame.size.width - 30 {
if consoleView.frame.maxX > 30 && consoleView.frame.minX < consoleViewController.view.frame.size.width - 30 {
grabberMode = false
} else {
grabberMode = true
}
}
// Animate touch down.
func consolePiPTouchDown() {
guard !grabberMode else { return }
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) { [self] in
consoleView.transform = .init(scaleX: 0.97, y: 0.97)
}.startAnimation()
}
var consolePiPTouchDownAnimator: UIViewPropertyAnimator?
// Animate touch up.
func consolePiPTouchUp() {
UIViewPropertyAnimator(duration: scrollLocked ? 0.4 : 0.7, dampingRatio: scrollLocked ? 1 : 0.45) { [self] in
consoleView.transform = .identity
}.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
if !grabberMode {
consoleTextView.alpha = 1
if !ResizeController.shared.isActive {
menuButton.alpha = 1
var isPressed: Bool = false {
didSet {
guard oldValue != isPressed else { return }
if isPressed {
guard !grabberMode else { return }
consolePiPTouchDownAnimator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) { [self] in
consoleView.transform = .init(scaleX: 0.96, y: 0.96)
}
consolePiPTouchDownAnimator?.startAnimation(afterDelay: 0.1)
} else {
consolePiPTouchDownAnimator?.stopAnimation(true)
consolePiPPopAnimator?.stopAnimation(true)
UIViewPropertyAnimator(duration: scrollLocked ? 0.4 : 0.7, dampingRatio: scrollLocked ? 1 : 0.45) { [self] in
consoleView.transform = .identity
}.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
if !grabberMode {
consoleTextView.alpha = 1
if !ResizeController.shared.isActive {
menuButton.alpha = 1
}
}
}.startAnimation()
}
}.startAnimation()
}
}
// Simulataneously listen to all gesture recognizers.
@@ -1124,42 +1265,39 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
@objc func consolePiPTapStartEnd(recognizer: UITapStartEndGestureRecognizer) {
switch recognizer.state {
case .began:
consolePiPTouchDown()
isPressed = true
case .changed:
break
case .ended, .cancelled, .possible, .failed:
consolePiPTouchUp()
isPressed = false
@unknown default:
break
}
}
}
// Custom window for the console to appear above other windows while passing touches down.
class ConsoleWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
/// Custom button that pauses console window swizzling to allow the console menu's presenting view controller to remain the top view controller.
class ConsoleMenuButton: UIButton {
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
super.contextMenuInteraction(interaction, willDisplayMenuFor: configuration, animator: animator)
if let hitView = super.hitTest(point, with: event) {
return hitView.isKind(of: ConsoleWindow.self) ? nil : hitView
}
return super.hitTest(point, with: event)
SwizzleTool.pauseDidAddSubviewSwizzledClosure = true
}
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
SwizzleTool.pauseDidAddSubviewSwizzledClosure = false
}
}
// Custom view for the console to appear above other windows while passing touches down.
// Custom view that is passes touches .
class PassthroughView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let hitView = super.hitTest(point, with: event) {
return hitView.isKind(of: PassthroughView.self) ? nil : hitView
}
return super.hitTest(point, with: event)
let hitView = super.hitTest(point, with: event)
return hitView == self ? nil : hitView
}
}
import UIKit.UIGestureRecognizerSubclass
public class UITapStartEndGestureRecognizer: UITapGestureRecognizer {
@@ -1192,59 +1330,105 @@ extension UIView {
}
}
extension UIWindow {
class SwizzleTool: NSObject {
/// 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" + "Sta" + "tus" + "Bar" + "Appe" + "arance")),
let swizzledMethod = class_getInstanceMethod(UIWindow.self, #selector(swizzled_statusBarAppearance))
else { return }
/// Ensure context menus always show in a non reversed order.
func swizzleContextMenuReverseOrder() {
guard let originalMethod = class_getInstanceMethod(NSClassFromString("_" + "UI" + "Context" + "Menu" + "List" + "View").self, NSSelectorFromString("reverses" + "Action" + "Order")),
let swizzledMethod = class_getInstanceMethod(SwizzleTool.self, #selector(swizzled_reverses_Action_Order))
else { Swift.print("Swizzle Error Occurred"); return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}()
}
@objc func swizzled_reverses_Action_Order() -> Bool {
if let menu = self.value(forKey: "displayed" + "Menu") as? UIMenu,
menu.title == "Debug" || menu.title == "User" + "Defaults" {
return false
}
if let orig = self.value(forKey: "_" + "reverses" + "Action" + "Order") as? Bool {
return orig
}
return false
}
@objc func swizzled_statusBarAppearance() -> Bool {
return isKeyWindow
static var swizzledDidAddSubviewClosure: (() -> Void)?
static var pauseDidAddSubviewSwizzledClosure: Bool = false
func swizzleDidAddSubview(_ closure: @escaping () -> Void) {
guard let originalMethod = class_getInstanceMethod(UIWindow.self, #selector(UIWindow.didAddSubview(_:))),
let swizzledMethod = class_getInstanceMethod(SwizzleTool.self, #selector(swizzled_did_add_subview(_:)))
else { Swift.print("Swizzle Error Occurred"); return }
method_exchangeImplementations(originalMethod, swizzledMethod)
Self.swizzledDidAddSubviewClosure = closure
}
@objc func swizzled_did_add_subview(_ subview: UIView) {
guard !Self.pauseDidAddSubviewSwizzledClosure else { return }
if let closure = Self.swizzledDidAddSubviewClosure {
closure()
}
}
}
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
Bundle(path: "/Sys" + "tem/Lib" + "rary/Private" + "Framework" + "s/Material" + "Kit." + "framework")!.load()
if 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
} else {
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialDark))
addSubview(visualEffectView)
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
visualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
visualEffectView.topAnchor.constraint(equalTo: topAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
return visualEffectView
}
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 = {
@@ -1340,61 +1524,16 @@ fileprivate func _debugPrint(_ items: Any) {
// Support for auto-rotate.
class ConsoleViewController: UIViewController {
var previousSize: CGSize?
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// Cancel the panner console is being panned to allow for location manipulation.
[LCManager.shared.panRecognizer, LCManager.shared.longPressRecognizer].forEach {
$0.isEnabled.toggle(); $0.isEnabled.toggle()
}
if UIDevice.current.userInterfaceIdiom != .pad && ResizeController.shared.isActive {
ResizeController.shared.isActive = false
ResizeController.shared.platterView.dismiss()
}
if UIDevice.current.userInterfaceIdiom == .pad && ResizeController.shared.isActive {
DispatchQueue.main.async {
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
LCManager.shared.consoleView.center = ResizeController.shared.consoleCenterPoint
}.startAnimation(afterDelay: 0.05)
}
} else {
let consoleView = LCManager.shared.consoleView
let oldSize = LCManager.shared.viewController.view.frame.size
let targetLocationEstimate: CGPoint = {
var xPosition = consoleView.center.x
var yPosition = consoleView.center.y
if consoleView.center.x > oldSize.width / 2 {
xPosition += size.width - oldSize.width
}
if consoleView.center.y > oldSize.height / 2 {
yPosition += size.height - oldSize.height
}
return CGPoint(x: xPosition, y: yPosition)
}()
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
consoleView.center = targetLocationEstimate
}.startAnimation(afterDelay: 0.05)
DispatchQueue.main.async {
// Update portrait orientation menu option for resize controller.
LCManager.shared.menuButton.menu = LCManager.shared.makeMenu()
// Reassess center of console based on target location estimate.
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
consoleView.center = nearestTargetTo(consoleView.center, possibleTargets: LCManager.shared.possibleEndpoints)
}.startAnimation(afterDelay: 0.05)
LCManager.shared.reassessGrabberMode()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let previousSize = previousSize, previousSize != view.bounds.size {
LCManager.shared.handleDeviceOrientationChange(previousSize: previousSize)
self.previousSize = view.bounds.size
} else if previousSize == nil {
previousSize = view.bounds.size
}
}
}
+18 -18
View File
@@ -15,7 +15,7 @@ class ResizeController {
lazy var platterView = PlatterView(frame: .zero)
var consoleCenterPoint: CGPoint {
let containerViewSize = LCManager.shared.viewController.view.frame.size
let containerViewSize = LCManager.shared.consoleViewController.view.frame.size
return CGPoint(x: (containerViewSize.width * UIScreen.main.scale / 2).rounded() / UIScreen.main.scale,
y: (containerViewSize.height * UIScreen.main.scale / 2).rounded() / UIScreen.main.scale
@@ -50,7 +50,7 @@ class ResizeController {
lazy var bottomGrabber: UIView = {
let view = UIView()
LCManager.shared.consoleWindow?.addSubview(view)
LCManager.shared.consoleViewController.view.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
@@ -80,7 +80,7 @@ class ResizeController {
lazy var rightGrabber: UIView = {
let view = UIView()
LCManager.shared.consoleWindow?.addSubview(view)
LCManager.shared.consoleViewController.view.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
@@ -117,7 +117,7 @@ class ResizeController {
_ = rightGrabber
// Ensure initial autolayout is performed unanimated.
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.consoleViewController.view.layoutIfNeeded()
if #available(iOS 15, *) {
FrameRateRequest.shared.perform(duration: 1.5)
@@ -138,17 +138,17 @@ class ResizeController {
}
// Ensure background color animates in right the first time.
LCManager.shared.consoleWindow?.backgroundColor = .clear
LCManager.shared.consoleViewController.view.backgroundColor = .clear
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
LCManager.shared.consoleView.center = self.consoleCenterPoint
// Update grabbers (layout constraints)
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.consoleViewController.view.layoutIfNeeded()
LCManager.shared.menuButton.alpha = 0
LCManager.shared.consoleWindow?.backgroundColor = UIColor(dynamicProvider: { traitCollection in
LCManager.shared.consoleViewController.view.backgroundColor = UIColor(dynamicProvider: { traitCollection in
UIColor(white: 0, alpha: traitCollection.userInterfaceStyle == .light ? 0.1 : 0.3)
})
}.startAnimation()
@@ -181,11 +181,11 @@ class ResizeController {
LCManager.shared.snapToCachedEndpoint()
// Update grabbers (layout constraints)
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.consoleViewController.view.layoutIfNeeded()
LCManager.shared.menuButton.alpha = 1
LCManager.shared.consoleWindow?.backgroundColor = .clear
LCManager.shared.consoleViewController.view.backgroundColor = .clear
}.startAnimation()
UIViewPropertyAnimator(duration: 0.2, dampingRatio: 1) { [self] in
@@ -277,7 +277,7 @@ class ResizeController {
LCManager.shared.consoleView.center.y = self.consoleCenterPoint.y
// Animate autolayout updates.
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.consoleViewController.view.layoutIfNeeded()
}.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
@@ -358,7 +358,7 @@ class ResizeController {
LCManager.shared.consoleView.center.x = (UIScreen.main.nativeBounds.width * 1/2).rounded() / UIScreen.main.scale
// Animate autolayout updates.
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.consoleViewController.view.layoutIfNeeded()
}.startAnimation()
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
@@ -396,8 +396,8 @@ class PlatterView: UIView {
addSubview(blurView)
LCManager.shared.viewController.view.addSubview(self)
LCManager.shared.viewController.view.sendSubviewToBack(self)
LCManager.shared.consoleViewController.view.addSubview(self)
LCManager.shared.consoleViewController.view.sendSubviewToBack(self)
_ = backgroundButton
@@ -466,8 +466,8 @@ class PlatterView: UIView {
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)
LCManager.shared.consoleViewController.view.addSubview(backgroundButton)
LCManager.shared.consoleViewController.view.sendSubviewToBack(backgroundButton)
return backgroundButton
}()
@@ -525,7 +525,7 @@ class PlatterView: UIView {
LCManager.shared.consoleSize = LCManager.shared.defaultConsoleSize
LCManager.shared.lumaHeightAnchor.constant = LCManager.shared.defaultConsoleSize.height
LCManager.shared.consoleView.center = ResizeController.shared.consoleCenterPoint
LCManager.shared.consoleWindow?.layoutIfNeeded()
LCManager.shared.consoleViewController.view.layoutIfNeeded()
}.startAnimation()
}), for: .touchUpInside)
@@ -544,7 +544,7 @@ class PlatterView: UIView {
}()
func configureFrame() {
self.frame.size = LCManager.shared.viewController.view.frame.size
self.frame.size = LCManager.shared.consoleViewController.view.frame.size
// Make sure bottom doesn't show on upwards pan.
self.frame.size.height += 50
self.frame.origin = possibleEndpoints[1]
@@ -593,7 +593,7 @@ class PlatterView: UIView {
}
var possibleEndpoints: [CGPoint] { return [CGPoint(x: 0, y: (UIScreen.hasRoundedCorners ? 44 : -8) + 63),
CGPoint(x: 0, y: LCManager.shared.viewController.view.frame.size.height + 5)]
CGPoint(x: 0, y: LCManager.shared.consoleViewController.view.frame.size.height + 5)]
}
var initialPlatterOriginY = CGFloat.zero