|
|
|
@@ -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,8 +131,8 @@ 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")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -155,13 +155,14 @@ 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 = consoleWindow else { return [] }
|
|
|
|
|
|
|
|
|
|
let screenSize = viewController.view.frame.size
|
|
|
|
|
|
|
|
|
@@ -169,32 +170,31 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
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.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) && UIDevice.current.userInterfaceIdiom == .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 + (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 +231,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 {
|
|
|
|
|
|
|
|
|
@@ -267,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
|
|
|
|
@@ -281,10 +280,12 @@ 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 +313,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)
|
|
|
|
@@ -383,9 +384,11 @@ 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
|
|
|
|
@@ -397,8 +400,6 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
|
|
|
|
|
viewController.view.addSubview(consoleView)
|
|
|
|
|
|
|
|
|
|
UIWindow.swizzleStatusBarAppearanceOverride
|
|
|
|
|
|
|
|
|
|
updateConsoleOrigin()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -424,8 +425,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)
|
|
|
|
@@ -482,7 +483,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()
|
|
|
|
@@ -500,7 +500,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)
|
|
|
|
|
|
|
|
|
@@ -532,12 +532,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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -558,10 +580,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) {
|
|
|
|
@@ -615,15 +636,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.
|
|
|
|
@@ -645,10 +674,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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -676,13 +714,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))
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
@@ -720,11 +765,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",
|
|
|
|
@@ -751,10 +801,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()
|
|
|
|
@@ -818,34 +985,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()
|
|
|
|
@@ -853,7 +1028,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()
|
|
|
|
|
|
|
|
|
@@ -867,17 +1042,23 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let consolePiPPanner_frameRateRequest = FrameRateRequest()
|
|
|
|
|
var consolePiPPanner_frameRateRequestID: UUID?
|
|
|
|
|
|
|
|
|
|
@objc func consolePiPPanner(recognizer: UIPanGestureRecognizer) {
|
|
|
|
|
|
|
|
|
|
if recognizer.state == .began {
|
|
|
|
|
consolePiPPanner_frameRateRequest.isActive = true
|
|
|
|
|
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)
|
|
|
|
@@ -894,8 +1075,12 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
|
|
|
|
|
case .ended, .cancelled:
|
|
|
|
|
|
|
|
|
|
consolePiPPanner_frameRateRequest.isActive = false
|
|
|
|
|
FrameRateRequest().perform(duration: 0.5)
|
|
|
|
|
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
|
|
|
|
@@ -919,8 +1104,8 @@ 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.reassessGrabberMode()
|
|
|
|
@@ -939,29 +1124,37 @@ 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()
|
|
|
|
|
}
|
|
|
|
|
var consolePiPTouchDownAnimator: UIViewPropertyAnimator?
|
|
|
|
|
|
|
|
|
|
// 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 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.
|
|
|
|
@@ -972,11 +1165,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
|
|
|
|
|
}
|
|
|
|
@@ -985,28 +1178,19 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
|
|
|
|
|
|
|
|
|
// 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? {
|
|
|
|
|
|
|
|
|
|
if let hitView = super.hitTest(point, with: event) {
|
|
|
|
|
return hitView.isKind(of: ConsoleWindow.self) ? nil : hitView
|
|
|
|
|
}
|
|
|
|
|
return super.hitTest(point, with: event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
if hitView.isKind(of: PassthroughView.self) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return hitView
|
|
|
|
|
}
|
|
|
|
|
return super.hitTest(point, with: event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Custom view that is passed through if it is the returned hitTest for ConsoleWindow.
|
|
|
|
|
class PassthroughView: UIView { }
|
|
|
|
|
|
|
|
|
|
import UIKit.UIGestureRecognizerSubclass
|
|
|
|
|
|
|
|
|
@@ -1043,56 +1227,99 @@ 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()
|
|
|
|
|
|
|
|
|
|
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 = {
|
|
|
|
@@ -1134,7 +1361,7 @@ class InvertedTextView: UITextView {
|
|
|
|
|
|
|
|
|
|
var pendingOffsetChange = false
|
|
|
|
|
|
|
|
|
|
// Thanks to WWDC21 Lab!
|
|
|
|
|
// Thanks to WWDC21 UIKit Lab!
|
|
|
|
|
override func layoutSubviews() {
|
|
|
|
|
super.layoutSubviews()
|
|
|
|
|
|
|
|
|
@@ -1166,6 +1393,14 @@ extension UIDevice {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
@@ -1181,7 +1416,6 @@ fileprivate func _debugPrint(_ items: Any) {
|
|
|
|
|
// 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.
|
|
|
|
@@ -1249,40 +1483,77 @@ An object that allows you to manually request an increased display refresh rate
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
// Example
|
|
|
|
|
let request = FrameRateRequest(preferredFrameRate: 120,
|
|
|
|
|
duration: 0.4)
|
|
|
|
|
FrameRateRequest.shared.perform(duration: 0.5)
|
|
|
|
|
request.perform()
|
|
|
|
|
```
|
|
|
|
|
*/
|
|
|
|
|
class FrameRateRequest {
|
|
|
|
|
@available(iOS 15, *)
|
|
|
|
|
final class FrameRateRequest {
|
|
|
|
|
|
|
|
|
|
static let shared = FrameRateRequest()
|
|
|
|
|
|
|
|
|
|
lazy private var displayLink = CADisplayLink(target: self, selector: #selector(dummyFunction))
|
|
|
|
|
|
|
|
|
|
var isActive: Bool = false {
|
|
|
|
|
private var requestIdentifiers: [UUID] = [] {
|
|
|
|
|
didSet {
|
|
|
|
|
guard #available(iOS 15, *) else { return }
|
|
|
|
|
guard isActive != oldValue else { return }
|
|
|
|
|
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.remove(from: .current, forMode: .common)
|
|
|
|
|
displayLink.invalidate()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Prepares your frame rate request parameters.
|
|
|
|
|
init(preferredFrameRate: Float = Float(UIScreen.main.maximumFramesPerSecond)) {
|
|
|
|
|
if #available(iOS 15, *) {
|
|
|
|
|
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 30, maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: preferredFrameRate)
|
|
|
|
|
}
|
|
|
|
|
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.
|
|
|
|
|
func perform(duration: Double) {
|
|
|
|
|
isActive = true
|
|
|
|
|
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
|
|
|
|
|
isActive = false
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|