Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64e18b18fc | |||
| 9dcfa5accd | |||
| adbb2d763f | |||
| c08c26f4a7 | |||
| d995119198 | |||
| b4d7c06432 | |||
| a7b95a4379 | |||
| 3efe25f804 | |||
| 0a3e28b28f | |||
| 009fab95be | |||
| a1275a7f49 | |||
| e08d439d2b | |||
| da9d78f559 | |||
| 50e4ce4e03 | |||
| ae73be37b4 | |||
| eeece2fda8 | |||
| 01ee69b8c5 | |||
| 72d9b1fbd5 | |||
| b435de87a2 |
@@ -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
|
||||
|
||||
@@ -21,7 +21,7 @@ let consoleManager = LCManager.shared
|
||||
```
|
||||
|
||||
## **Usage**
|
||||
Once prepared, the localConsole can be used throughout your project.
|
||||
Once prepared, the consoleManager can be used throughout your project.
|
||||
```swift
|
||||
|
||||
// Activate the console view.
|
||||
|
||||
@@ -82,8 +82,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}.startAnimation()
|
||||
grabberMode = false
|
||||
|
||||
UserDefaults.standard.set(consoleView.center.x, forKey: "LocalConsole_X")
|
||||
UserDefaults.standard.set(consoleView.center.y, forKey: "LocalConsole_Y")
|
||||
UserDefaults.standard.set(consoleView.center.x, forKey: "LocalConsole.X")
|
||||
UserDefaults.standard.set(consoleView.center.y, forKey: "LocalConsole.Y")
|
||||
}), for: .touchUpInside)
|
||||
|
||||
consoleView.addSubview(button)
|
||||
@@ -131,20 +131,22 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// TODO: Snap to nearest position.
|
||||
|
||||
UserDefaults.standard.set(consoleSize.width, forKey: "LocalConsole_Width")
|
||||
UserDefaults.standard.set(consoleSize.height, forKey: "LocalConsole_Height")
|
||||
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 parent view controller in order to display context menus.
|
||||
lazy var viewController = UIViewController()
|
||||
lazy var consoleView = viewController.view!
|
||||
/// Enables rotation.
|
||||
lazy var viewController = 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()
|
||||
|
||||
/// Text view that displays printed items.
|
||||
lazy var consoleTextView = InvertedTextView()
|
||||
lazy var consoleTextView = InvertedTextView()
|
||||
|
||||
/// Button that reveals menu.
|
||||
lazy var menuButton = UIButton()
|
||||
@@ -153,30 +155,46 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
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] {
|
||||
guard let consoleWindow else { return [] }
|
||||
|
||||
if consoleSize.width < UIScreen.portraitSize.width - 112 {
|
||||
|
||||
let screenSize = viewController.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
|
||||
|
||||
// Fix incorrect reported orientation on phone.
|
||||
let isLandscapePhone = UIDevice.current.userInterfaceIdiom == .phone && viewController.view.frame.width > viewController.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 leftEndpointX = consoleSize.width / 2 + consoleWindow.safeAreaInsets.left + (isLandscapePhone ? 4 : 12) + (isLandscapeRightNotchedPhone ? -16 : 0)
|
||||
let rightEndpointX = screenSize.width - (consoleSize.width / 2 + consoleWindow.safeAreaInsets.right) - (isLandscapePhone ? 4 : 12) + (isLandscapeLeftNotchedPhone ? 16 : 0)
|
||||
let topEndpointY = consoleSize.height / 2 + consoleWindow.safeAreaInsets.top + 12 + (isPortraitNotchedPhone ? -10 : 0)
|
||||
let bottomEndpointY = screenSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow.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,
|
||||
y: (UIScreen.hasRoundedCorners ? 38 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 38 : 16) + consoleSize.height / 2 + 12),
|
||||
|
||||
// Bottom endpoints.
|
||||
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)]
|
||||
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 {
|
||||
|
||||
@@ -184,56 +202,59 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
endpoints = [endpoints[0], endpoints[2]]
|
||||
|
||||
// Left edge hiding endpoints.
|
||||
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
||||
y: endpoints[1].y))
|
||||
if !isLandscapeLeftNotchedPhone {
|
||||
if consoleView.center.y < (screenSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
}
|
||||
} else if consoleView.frame.maxX >= UIScreen.portraitSize.width {
|
||||
} else if consoleView.frame.maxX >= screenSize.width {
|
||||
|
||||
// Right edge endpoints.
|
||||
endpoints = [endpoints[1], endpoints[3]]
|
||||
|
||||
// Right edge hiding endpoints.
|
||||
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
||||
y: endpoints[1].y))
|
||||
if !isLandscapeRightNotchedPhone {
|
||||
if consoleView.center.y < (screenSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
endpoints.append(CGPoint(x: screenSize.width + consoleSize.width / 2 - 28,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: screenSize.width + consoleSize.width / 2 - 28,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
|
||||
} else {
|
||||
|
||||
// Two endpoints, one for the top, one for the bottom..
|
||||
var endpoints = [CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: (UIScreen.hasRoundedCorners ? 38 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: UIScreen.portraitSize.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 {
|
||||
|
||||
// Left edge hiding endpoints.
|
||||
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
if consoleView.center.y < (screenSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: -consoleSize.width / 2 + 28,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
} else if consoleView.frame.maxX >= UIScreen.portraitSize.width {
|
||||
} else if consoleView.frame.maxX >= screenSize.width {
|
||||
|
||||
// Right edge hiding endpoints.
|
||||
if consoleView.center.y < (UIScreen.portraitSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
||||
if consoleView.center.y < (screenSize.height - (temporaryKeyboardHeightValueTracker ?? 0)) / 2 {
|
||||
endpoints.append(CGPoint(x: screenSize.width + consoleSize.width / 2 - 28,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
||||
endpoints.append(CGPoint(x: screenSize.width + consoleSize.width / 2 - 28,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
}
|
||||
@@ -245,8 +266,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
lazy var initialViewLocation: CGPoint = .zero
|
||||
|
||||
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)
|
||||
consoleSize = CGSize(width: UserDefaults.standard.object(forKey: "LocalConsole.Width") as? CGFloat ?? consoleSize.width,
|
||||
height: UserDefaults.standard.object(forKey: "LocalConsole.Height") as? CGFloat ?? consoleSize.height)
|
||||
|
||||
|
||||
consoleView.layer.shadowRadius = 16
|
||||
@@ -262,7 +283,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
borderView.frame = CGRect(x: -1, y: -1,
|
||||
width: consoleSize.width + 2,
|
||||
height: consoleSize.height + 2)
|
||||
borderView.layer.borderWidth = 1
|
||||
borderView.layer.borderWidth = 2 - 1 / consoleView.traitCollection.displayScale
|
||||
borderView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
|
||||
borderView.layer.cornerRadius = consoleView.layer.cornerRadius + 1
|
||||
borderView.layer.cornerCurve = .continuous
|
||||
@@ -290,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)
|
||||
@@ -342,7 +363,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
func updateConsoleOrigin() {
|
||||
snapToCachedEndpoint()
|
||||
|
||||
if consoleView.center.x < 0 || consoleView.center.x > UIScreen.portraitSize.width {
|
||||
if consoleView.center.x < 0 || consoleView.center.x > viewController.view.frame.size.width {
|
||||
grabberMode = true
|
||||
scrollLocked = !grabberMode
|
||||
|
||||
@@ -361,16 +382,21 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
.first
|
||||
|
||||
if let windowScene = windowScene as? UIWindowScene {
|
||||
|
||||
windowSceneFound = true
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride()
|
||||
SwizzleTool().swizzleContextMenuReverseOrder()
|
||||
|
||||
consoleWindow = ConsoleWindow(windowScene: windowScene)
|
||||
consoleWindow?.frame = UIScreen.main.bounds
|
||||
consoleWindow?.windowLevel = UIWindow.Level.statusBar
|
||||
consoleWindow?.isHidden = false
|
||||
consoleWindow?.addSubview(consoleView)
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride
|
||||
viewController.view = PassthroughView()
|
||||
|
||||
consoleWindow?.rootViewController = viewController
|
||||
|
||||
viewController.view.addSubview(consoleView)
|
||||
|
||||
updateConsoleOrigin()
|
||||
}
|
||||
@@ -397,8 +423,8 @@ 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)
|
||||
@@ -455,7 +481,6 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
guard oldValue != grabberMode else { return }
|
||||
|
||||
if grabberMode {
|
||||
|
||||
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
||||
lumaHeightAnchor.constant = consoleView.frame.size.height
|
||||
consoleView.layoutIfNeeded()
|
||||
@@ -473,7 +498,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 = 9
|
||||
consoleView.layoutIfNeeded()
|
||||
}.startAnimation(afterDelay: 0.06)
|
||||
|
||||
@@ -505,12 +530,34 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var hasShortened = false
|
||||
|
||||
public var isCharacterLimitDisabled = false
|
||||
public var isCharacterLimitWarningDisabled = false
|
||||
|
||||
/// Print items to the console view.
|
||||
public func print(_ items: Any) {
|
||||
if currentText == "" {
|
||||
currentText = "\(items)"
|
||||
let _currentText: String = {
|
||||
if currentText == "" {
|
||||
return "\(items)"
|
||||
} else {
|
||||
return currentText + "\n\(items)"
|
||||
}
|
||||
}()
|
||||
|
||||
// Cut down string if it exceeds 50,000 characters to keep text view running smoothly.
|
||||
if _currentText.count > 50000 && !isCharacterLimitDisabled {
|
||||
|
||||
if !hasShortened && !isCharacterLimitWarningDisabled {
|
||||
hasShortened = true
|
||||
Swift.print("LocalConsole's content has exceeded 50,000 characters.\nTo maintain performance, LCManager cuts down the beginning of the printed content. To disable this behaviour, set LCManager.shared.isCharacterLimitDisabled to true.\nTo disable this warning, set LCManager.shared.isCharacterLimitWarningDisabled = true.")
|
||||
|
||||
}
|
||||
|
||||
let shortenedString = String(_currentText.suffix(50000))
|
||||
currentText = shortenedString.stringAfterFirstOccurenceOf(delimiter: "\n") ?? shortenedString
|
||||
} else {
|
||||
currentText = currentText + "\n\(items)"
|
||||
currentText = _currentText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,8 +584,6 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
if consoleView.center != possibleEndpoints[0] && consoleView.center != possibleEndpoints[1] {
|
||||
let nearestTargetPosition = nearestTargetTo(consoleView.center, possibleTargets: possibleEndpoints.suffix(2))
|
||||
|
||||
Swift.print(possibleEndpoints.suffix(2))
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.55, dampingRatio: 1) {
|
||||
self.consoleView.center = nearestTargetPosition
|
||||
}.startAnimation()
|
||||
@@ -590,15 +635,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
|
||||
|
||||
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.
|
||||
@@ -620,10 +673,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,13 +713,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
if currentText != "" { print("\n") }
|
||||
|
||||
let safeAreaInsets = consoleWindow?.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))
|
||||
"""
|
||||
)
|
||||
}
|
||||
@@ -695,11 +764,16 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleTextView.attributedText = NSAttributedString(string: string, attributes: attributes)
|
||||
}
|
||||
|
||||
// Displays all UserDefaults keys, including unneeded keys that are included by default.
|
||||
public var showAllUserDefaultsKeys = false
|
||||
|
||||
func makeMenu() -> UIMenu {
|
||||
|
||||
let copy = UIAction(title: "Copy",
|
||||
image: UIImage(systemName: "doc.on.doc"), handler: { _ in
|
||||
self.copy()
|
||||
let share = UIAction(title: "Share Text...",
|
||||
image: UIImage(systemName: "square.and.arrow.up"), handler: { _ in
|
||||
let activityViewController = UIActivityViewController(activityItems: [self.consoleTextView.text ?? ""],
|
||||
applicationActivities: nil)
|
||||
self.viewController.present(activityViewController, animated: true)
|
||||
})
|
||||
|
||||
let resize = UIAction(title: "Resize Console",
|
||||
@@ -710,6 +784,14 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
})
|
||||
|
||||
// If device is phone in landscape, disable resize controller.
|
||||
if UIDevice.current.userInterfaceIdiom == .phone && viewController.view.frame.width > viewController.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
|
||||
self.clear()
|
||||
@@ -718,10 +800,127 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
let consoleActions = UIMenu(title: "", options: .displayInline, children: [clear, resize])
|
||||
|
||||
var frameSymbol = "rectangle.3.offgrid"
|
||||
|
||||
var debugActions: [UIMenuElement] = []
|
||||
|
||||
if #available(iOS 15, *) {
|
||||
frameSymbol = "square.inset.filled"
|
||||
|
||||
let deferredUserDefaultsList = UIDeferredMenuElement.uncached { completion in
|
||||
var actions: [UIAction] = []
|
||||
|
||||
let keys: [String] = {
|
||||
|
||||
if self.showAllUserDefaultsKeys {
|
||||
return UserDefaults.standard.dictionaryRepresentation().map { $0.key }
|
||||
}
|
||||
|
||||
// Show keys the developer has added to the app (+ LocalConsole keys), excluding all of Apple's keys.
|
||||
if let bundle: String = Bundle.main.bundleIdentifier {
|
||||
let preferencePath: String = NSHomeDirectory() + "/Library/Preferences/\(bundle).plist"
|
||||
|
||||
let _keys = NSDictionary(contentsOfFile: preferencePath)?.allKeys as! [String]
|
||||
|
||||
return _keys.filter {
|
||||
!$0.contains("LocalConsole.")
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}()
|
||||
|
||||
if keys.isEmpty {
|
||||
actions.append(UIAction(title: "No Entries",
|
||||
image: nil, attributes: .disabled, handler: { _ in }
|
||||
))
|
||||
} else {
|
||||
for key in keys.sorted(by: { $0.lowercased() < $1.lowercased() }) {
|
||||
|
||||
// Old LocalConsole Key Cleanup
|
||||
guard !key.contains("LocalConsole_") else {
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
continue
|
||||
}
|
||||
|
||||
if let value = UserDefaults.standard.value(forKey: key) {
|
||||
let action = UIAction(title: key, image: nil) { _ in
|
||||
let alertController = UIAlertController(title: key,
|
||||
message: nil,
|
||||
preferredStyle: .alert)
|
||||
|
||||
let headerParagraphStyle = NSMutableParagraphStyle()
|
||||
headerParagraphStyle.paragraphSpacing = 6
|
||||
let contentParagraphStyle = NSMutableParagraphStyle()
|
||||
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.paragraphStyle: contentParagraphStyle,
|
||||
.foregroundColor: UIColor.label,
|
||||
.font: UIFont.systemFont(ofSize: 13, weight: .semibold, design: .monospaced)
|
||||
]
|
||||
|
||||
let attributedTitle: NSMutableAttributedString = {
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: "Key\n" + key, attributes: attributes)
|
||||
attributedString.addAttributes(
|
||||
[NSAttributedString.Key.foregroundColor : UIColor.label.withAlphaComponent(0.5),
|
||||
NSAttributedString.Key.paragraphStyle : headerParagraphStyle],
|
||||
range: NSRange(location: 0, length: 3))
|
||||
|
||||
return attributedString
|
||||
}()
|
||||
|
||||
let attributedMessage: NSMutableAttributedString = {
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: "\nValue\n" + "\(value)", attributes: attributes)
|
||||
attributedString.addAttributes(
|
||||
[NSAttributedString.Key.foregroundColor : UIColor.label.withAlphaComponent(0.5),
|
||||
NSAttributedString.Key.paragraphStyle : headerParagraphStyle],
|
||||
range: NSRange(location: 0, length: 7))
|
||||
|
||||
return attributedString
|
||||
}()
|
||||
|
||||
alertController.setValue(attributedTitle, forKey: "attributedTitle")
|
||||
alertController.setValue(attributedMessage, forKey: "attributedMessage")
|
||||
|
||||
alertController.addAction(UIAlertAction(title: "Copy Value", style: .default, handler: { _ in
|
||||
UIPasteboard.general.string = "\(value)"
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "Clear Value", style: .destructive, handler: { _ in
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in
|
||||
}))
|
||||
|
||||
self.viewController.present(alertController,
|
||||
animated: true)
|
||||
}
|
||||
action.subtitle = "\(value)"
|
||||
actions.append(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
actions.append(
|
||||
UIAction(title: "Clear Defaults",
|
||||
image: UIImage(systemName: "trash"), attributes: .destructive, handler: { _ in
|
||||
keys.forEach {
|
||||
UserDefaults.standard.removeObject(forKey: $0)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
completion(actions)
|
||||
}
|
||||
|
||||
let userDefaults = UIMenu(title: "UserDefaults", image: UIImage(systemName: "doc.badge.gearshape"), children: [deferredUserDefaultsList])
|
||||
|
||||
debugActions.append(userDefaults)
|
||||
}
|
||||
|
||||
|
||||
let viewFrames = UIAction(title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
|
||||
image: UIImage(systemName: frameSymbol), handler: { _ in
|
||||
self.debugBordersEnabled.toggle()
|
||||
@@ -785,34 +984,42 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
})
|
||||
|
||||
let debugActions = UIMenu(title: "", options: .displayInline,
|
||||
debugActions.append(contentsOf: [viewFrames, systemReport, displayReport, respring])
|
||||
|
||||
let debugMenu = UIMenu(title: "", options: .displayInline,
|
||||
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
|
||||
children: [viewFrames, systemReport, displayReport, respring])])
|
||||
children: debugActions)])
|
||||
|
||||
var menuContent: [UIMenuElement] = []
|
||||
|
||||
if consoleTextView.text != "" {
|
||||
menuContent.append(contentsOf: [copy, consoleActions])
|
||||
menuContent.append(contentsOf: [share, consoleActions])
|
||||
} else {
|
||||
menuContent.append(resize)
|
||||
}
|
||||
menuContent.append(debugActions)
|
||||
menuContent.append(debugMenu)
|
||||
|
||||
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()
|
||||
@@ -820,7 +1027,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
if !grabberMode { scrollLocked = true }
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 0.5) { [self] in
|
||||
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 0.6) { [self] in
|
||||
consoleView.transform = .identity
|
||||
}.startAnimation()
|
||||
|
||||
@@ -834,13 +1041,23 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var consolePiPPanner_frameRateRequestID: UUID?
|
||||
|
||||
@objc func consolePiPPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
if recognizer.state == .began {
|
||||
if #available(iOS 15, *) {
|
||||
consolePiPPanner_frameRateRequestID = UUID()
|
||||
FrameRateRequest.shared.activate(id: consolePiPPanner_frameRateRequestID!)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -853,14 +1070,17 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
y: initialViewLocation.y + translation.y)
|
||||
}.startAnimation()
|
||||
|
||||
if consoleView.frame.maxX > 30 && consoleView.frame.minX < UIScreen.portraitSize.width - 30 {
|
||||
grabberMode = false
|
||||
} else {
|
||||
grabberMode = true
|
||||
}
|
||||
reassessGrabberMode()
|
||||
|
||||
case .ended, .cancelled:
|
||||
|
||||
if #available(iOS 15, *), let id = consolePiPPanner_frameRateRequestID {
|
||||
consolePiPPanner_frameRateRequestID = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
FrameRateRequest.shared.deactivate(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
// After the PiP is thrown, determine the best corner and re-target it there.
|
||||
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
|
||||
|
||||
@@ -883,11 +1103,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
UserDefaults.standard.set(nearestTargetPosition.x, forKey: "LocalConsole_X")
|
||||
UserDefaults.standard.set(nearestTargetPosition.y, forKey: "LocalConsole_Y")
|
||||
UserDefaults.standard.set(nearestTargetPosition.x, forKey: "LocalConsole.X")
|
||||
UserDefaults.standard.set(nearestTargetPosition.y, forKey: "LocalConsole.Y")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
self.grabberMode = nearestTargetPosition.x < 0 || nearestTargetPosition.x > UIScreen.portraitSize.width
|
||||
self.reassessGrabberMode()
|
||||
self.scrollLocked = !self.grabberMode
|
||||
}
|
||||
|
||||
@@ -895,29 +1115,45 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// Animate touch down.
|
||||
func consolePiPTouchDown() {
|
||||
guard !grabberMode else { return }
|
||||
|
||||
UIViewPropertyAnimator(duration: 1.25, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.95, y: 0.95)
|
||||
}.startAnimation()
|
||||
func reassessGrabberMode() {
|
||||
if consoleView.frame.maxX > 30 && consoleView.frame.minX < viewController.view.frame.size.width - 30 {
|
||||
grabberMode = false
|
||||
} else {
|
||||
grabberMode = true
|
||||
}
|
||||
}
|
||||
|
||||
// Animate touch up.
|
||||
func consolePiPTouchUp() {
|
||||
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 0.4) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
if !grabberMode {
|
||||
consoleTextView.alpha = 1
|
||||
if !ResizeController.shared.isActive {
|
||||
menuButton.alpha = 1
|
||||
var consolePiPTouchDownAnimator: UIViewPropertyAnimator?
|
||||
|
||||
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.
|
||||
@@ -928,11 +1164,11 @@ 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
|
||||
}
|
||||
@@ -951,6 +1187,18 @@ class ConsoleWindow: UIWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// Custom view for the console to appear above other windows while passing touches down.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import UIKit.UIGestureRecognizerSubclass
|
||||
|
||||
@@ -987,21 +1235,49 @@ extension UIView {
|
||||
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" + "Sta" + "tus" + "Bar" + "Appe" + "arance")),
|
||||
static func swizzleStatusBarAppearanceOverride() {
|
||||
guard let originalMethod = class_getInstanceMethod(UIWindow.self, NSSelectorFromString("_can" + "Affect" + "Status" + "Bar" + "Appearance")),
|
||||
let swizzledMethod = class_getInstanceMethod(UIWindow.self, #selector(swizzled_statusBarAppearance))
|
||||
else { return }
|
||||
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}()
|
||||
}
|
||||
|
||||
@objc func swizzled_statusBarAppearance() -> Bool {
|
||||
return isKeyWindow
|
||||
}
|
||||
}
|
||||
|
||||
class SwizzleTool: NSObject {
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LumaView: UIView {
|
||||
lazy var visualEffectView: UIView = {
|
||||
Bundle(path: "/Sys" + "tem/Lib" + "rary/Private" + "Frameworks/Material" + "Kit." + "framework")!.load()
|
||||
Bundle(path: "/Sys" + "tem/Lib" + "rary/Private" + "Framework" + "s/Material" + "Kit." + "framework")!.load()
|
||||
|
||||
let Pill = NSClassFromString("MT" + "Luma" + "Dodge" + "Pill" + "View") as! UIView.Type
|
||||
|
||||
@@ -1078,7 +1354,7 @@ class InvertedTextView: UITextView {
|
||||
|
||||
var pendingOffsetChange = false
|
||||
|
||||
// Thanks to WWDC21 Lab!
|
||||
// Thanks to WWDC21 UIKit Lab!
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
@@ -1104,6 +1380,20 @@ class InvertedTextView: UITextView {
|
||||
}
|
||||
}
|
||||
|
||||
extension UIDevice {
|
||||
var hasNotch: Bool {
|
||||
return UIApplication.shared.windows[0].safeAreaInsets.bottom > 0
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func stringAfterFirstOccurenceOf(delimiter: String) -> String? {
|
||||
guard let upperIndex = (self.range(of: delimiter)?.upperBound) else { return nil }
|
||||
let trailingString: String = .init(self.suffix(from: upperIndex))
|
||||
return trailingString
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
var formattedString: String? {
|
||||
let formatter = DateComponentsFormatter()
|
||||
@@ -1115,3 +1405,150 @@ extension TimeInterval {
|
||||
fileprivate func _debugPrint(_ items: Any) {
|
||||
print(items)
|
||||
}
|
||||
|
||||
// Support for auto-rotate.
|
||||
class ConsoleViewController: UIViewController {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Frame Rate Request
|
||||
/**
|
||||
An object that allows you to manually request an increased display refresh rate on ProMotion devices.
|
||||
|
||||
*The display refresh rate does not exceed 60 Hz when low power mode is enabled.*
|
||||
|
||||
**Do not set an excessive duration. Doing so will negatively impact battery life.**
|
||||
|
||||
```
|
||||
// Example
|
||||
FrameRateRequest.shared.perform(duration: 0.5)
|
||||
request.perform()
|
||||
```
|
||||
*/
|
||||
@available(iOS 15, *)
|
||||
final class FrameRateRequest {
|
||||
|
||||
static let shared = FrameRateRequest()
|
||||
|
||||
lazy private var displayLink = CADisplayLink(target: self, selector: #selector(dummyFunction))
|
||||
|
||||
private var requestIdentifiers: [UUID] = [] {
|
||||
didSet {
|
||||
isActive = requestIdentifiers.count > 0
|
||||
}
|
||||
}
|
||||
|
||||
private var isActive: Bool = false {
|
||||
didSet {
|
||||
guard isActive != oldValue, UIScreen.main.maximumFramesPerSecond > 60 else { return }
|
||||
|
||||
if isActive {
|
||||
displayLink.add(to: .current, forMode: .common)
|
||||
} else {
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
guard UIScreen.main.maximumFramesPerSecond > 60 else { return }
|
||||
|
||||
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 90, maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond))
|
||||
|
||||
// Ensure the DisplayLink stops when the app enters the background, or else the system will shut high frame rate capabilities until the app is suspended and relaunched.
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground),
|
||||
name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground),
|
||||
name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
/// Perform frame rate request.
|
||||
public func perform(duration: Double) {
|
||||
|
||||
guard UIScreen.main.maximumFramesPerSecond > 60 else { return }
|
||||
|
||||
let id = UUID()
|
||||
|
||||
requestIdentifiers.append(id)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [self] in
|
||||
requestIdentifiers = requestIdentifiers.filter { $0 != id }
|
||||
}
|
||||
}
|
||||
|
||||
public func activate(id: UUID) {
|
||||
requestIdentifiers.append(id)
|
||||
}
|
||||
|
||||
public func deactivate(id: UUID) {
|
||||
requestIdentifiers = requestIdentifiers.filter { $0 != id }
|
||||
}
|
||||
|
||||
@objc private func willEnterForeground() {
|
||||
if isActive {
|
||||
displayLink.add(to: .current, forMode: .common)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func didEnterBackground() {
|
||||
if isActive {
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dummyFunction() {}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,13 @@ class 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))
|
||||
var consoleCenterPoint: CGPoint {
|
||||
let containerViewSize = LCManager.shared.viewController.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
|
||||
+ (UIScreen.hasRoundedCorners ? 0 : 24))
|
||||
}
|
||||
|
||||
lazy var consoleOutlineView: UIView = {
|
||||
|
||||
@@ -115,6 +119,10 @@ class ResizeController {
|
||||
// Ensure initial autolayout is performed unanimated.
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
|
||||
if #available(iOS 15, *) {
|
||||
FrameRateRequest.shared.perform(duration: 1.5)
|
||||
}
|
||||
|
||||
if isActive {
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) {
|
||||
@@ -201,6 +209,8 @@ class ResizeController {
|
||||
static let kMinConsoleHeight: CGFloat = 108
|
||||
static let kMaxConsoleHeight: CGFloat = 346
|
||||
|
||||
var verticalPanner_frameRateRequestID: UUID?
|
||||
|
||||
@objc func verticalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: bottomGrabber.superview)
|
||||
@@ -210,6 +220,11 @@ class ResizeController {
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if #available(iOS 15, *) {
|
||||
verticalPanner_frameRateRequestID = UUID()
|
||||
FrameRateRequest.shared.activate(id: verticalPanner_frameRateRequestID!)
|
||||
}
|
||||
|
||||
initialHeight = LCManager.shared.consoleSize.height
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
@@ -241,6 +256,14 @@ class ResizeController {
|
||||
LCManager.shared.consoleView.center.y = consoleCenterPoint.y
|
||||
|
||||
case .ended, .cancelled:
|
||||
|
||||
if #available(iOS 15, *), let id = verticalPanner_frameRateRequestID {
|
||||
verticalPanner_frameRateRequestID = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
FrameRateRequest.shared.deactivate(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
|
||||
if LCManager.shared.consoleSize.height > maxHeight {
|
||||
LCManager.shared.consoleSize.height = maxHeight
|
||||
@@ -268,7 +291,9 @@ class ResizeController {
|
||||
var initialWidth = CGFloat.zero
|
||||
|
||||
static let kMinConsoleWidth: CGFloat = 112
|
||||
static let kMaxConsoleWidth: CGFloat = UIScreen.portraitSize.width - 56
|
||||
static let kMaxConsoleWidth: CGFloat = [UIScreen.portraitSize.width, UIScreen.portraitSize.height].min()! - 56
|
||||
|
||||
var horizontalPanner_frameRateRequestID: UUID?
|
||||
|
||||
@objc func horizontalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
@@ -279,6 +304,11 @@ class ResizeController {
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if #available(iOS 15, *) {
|
||||
horizontalPanner_frameRateRequestID = UUID()
|
||||
FrameRateRequest.shared.activate(id: horizontalPanner_frameRateRequestID!)
|
||||
}
|
||||
|
||||
initialWidth = LCManager.shared.consoleSize.width
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
@@ -310,6 +340,13 @@ class ResizeController {
|
||||
|
||||
case .ended, .cancelled:
|
||||
|
||||
if #available(iOS 15, *), let id = horizontalPanner_frameRateRequestID {
|
||||
horizontalPanner_frameRateRequestID = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
FrameRateRequest.shared.deactivate(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
|
||||
if LCManager.shared.consoleSize.width > maxWidth {
|
||||
LCManager.shared.consoleSize.width = maxWidth
|
||||
@@ -339,12 +376,6 @@ 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)
|
||||
@@ -361,11 +392,12 @@ class PlatterView: UIView {
|
||||
blurView.clipsToBounds = true
|
||||
|
||||
blurView.frame = bounds
|
||||
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
addSubview(blurView)
|
||||
|
||||
LCManager.shared.consoleWindow?.addSubview(self)
|
||||
LCManager.shared.consoleWindow?.sendSubviewToBack(self)
|
||||
LCManager.shared.viewController.view.addSubview(self)
|
||||
LCManager.shared.viewController.view.sendSubviewToBack(self)
|
||||
|
||||
_ = backgroundButton
|
||||
|
||||
@@ -376,7 +408,7 @@ class PlatterView: UIView {
|
||||
let grabber = UIView()
|
||||
grabber.frame.size = CGSize(width: 36, height: 5)
|
||||
grabber.frame.origin.y = 10
|
||||
grabber.center.x = bounds.width / 2
|
||||
grabber.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
||||
grabber.backgroundColor = .label
|
||||
grabber.alpha = 0.1
|
||||
grabber.layer.cornerRadius = 2.5
|
||||
@@ -387,9 +419,8 @@ class PlatterView: UIView {
|
||||
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()
|
||||
titleLabel.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
||||
addSubview(titleLabel)
|
||||
|
||||
let subtitleLabel = UILabel()
|
||||
@@ -397,20 +428,36 @@ class PlatterView: UIView {
|
||||
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()
|
||||
subtitleLabel.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
||||
addSubview(subtitleLabel)
|
||||
|
||||
addSubview(resetButton)
|
||||
resetButton.center = CGPoint(x: UIScreen.portraitSize.width / 2 - 74,
|
||||
y: UIScreen.portraitSize.height - possibleEndpoints[0].y * 2)
|
||||
resetButton.roundOriginToPixel()
|
||||
let buttonContainerView = UIView()
|
||||
buttonContainerView.addSubview(resetButton)
|
||||
buttonContainerView.addSubview(doneButton)
|
||||
addSubview(buttonContainerView)
|
||||
|
||||
addSubview(doneButton)
|
||||
doneButton.center = CGPoint(x: UIScreen.portraitSize.width / 2 + 74,
|
||||
y: UIScreen.portraitSize.height - possibleEndpoints[0].y * 2)
|
||||
doneButton.roundOriginToPixel()
|
||||
buttonContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
resetButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
doneButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
|
||||
buttonContainerView.widthAnchor.constraint(equalToConstant: 264),
|
||||
buttonContainerView.heightAnchor.constraint(equalToConstant: 52),
|
||||
buttonContainerView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
buttonContainerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -possibleEndpoints[0].y * 2),
|
||||
|
||||
resetButton.widthAnchor.constraint(equalToConstant: 116),
|
||||
resetButton.heightAnchor.constraint(equalToConstant: 52),
|
||||
resetButton.leadingAnchor.constraint(equalTo: buttonContainerView.leadingAnchor),
|
||||
resetButton.topAnchor.constraint(equalTo: buttonContainerView.topAnchor),
|
||||
|
||||
doneButton.widthAnchor.constraint(equalToConstant: 116),
|
||||
doneButton.heightAnchor.constraint(equalToConstant: 52),
|
||||
doneButton.trailingAnchor.constraint(equalTo: buttonContainerView.trailingAnchor),
|
||||
doneButton.topAnchor.constraint(equalTo: buttonContainerView.topAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
lazy var backgroundButton: UIButton = {
|
||||
@@ -430,7 +477,6 @@ class PlatterView: UIView {
|
||||
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
|
||||
|
||||
@@ -465,7 +511,6 @@ class PlatterView: UIView {
|
||||
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
|
||||
|
||||
@@ -498,18 +543,35 @@ class PlatterView: UIView {
|
||||
return button
|
||||
}()
|
||||
|
||||
func configureFrame() {
|
||||
self.frame.size = LCManager.shared.viewController.view.frame.size
|
||||
// Make sure bottom doesn't show on upwards pan.
|
||||
self.frame.size.height += 50
|
||||
self.frame.origin = possibleEndpoints[1]
|
||||
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
|
||||
func reveal() {
|
||||
|
||||
configureFrame()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
self.frame.origin = self.possibleEndpoints[0]
|
||||
}.startAnimation()
|
||||
|
||||
backgroundButton.isHidden = false
|
||||
|
||||
isHidden = false
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
self.frame.origin = self.possibleEndpoints[1]
|
||||
}.startAnimation()
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
self.isHidden = true
|
||||
}
|
||||
animator.startAnimation()
|
||||
|
||||
backgroundButton.isHidden = true
|
||||
}
|
||||
@@ -530,7 +592,9 @@ class PlatterView: UIView {
|
||||
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 possibleEndpoints: [CGPoint] { return [CGPoint(x: 0, y: (UIScreen.hasRoundedCorners ? 44 : -8) + 63),
|
||||
CGPoint(x: 0, y: LCManager.shared.viewController.view.frame.size.height + 5)]
|
||||
}
|
||||
|
||||
var initialPlatterOriginY = CGFloat.zero
|
||||
|
||||
@@ -612,15 +676,20 @@ class PlatterView: UIView {
|
||||
$0.transform = .identity
|
||||
}
|
||||
}
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
if nearestTargetPosition == possibleEndpoints[1] {
|
||||
ResizeController.shared.isActive = false
|
||||
backgroundButton.isHidden = true
|
||||
|
||||
positionAnimator.addCompletion { _ in
|
||||
self.isHidden = true
|
||||
}
|
||||
} else {
|
||||
ResizeController.shared.isActive = true
|
||||
}
|
||||
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user