Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b435de87a2 | |||
| 372c8ce90b | |||
| 0d36867f6b | |||
| 3fff6edff0 | |||
| 485126dcf7 | |||
| 245d69679d | |||
| 7920272bff | |||
| 6892a19b0e | |||
| d2d45f8e03 | |||
| d5a06c013e | |||
| 3c2683c6bf | |||
| fd1114802f | |||
| afe572f4e3 | |||
| ceb5ed0a0c | |||
| 9189fa4173 | |||
| 1e39b362cc | |||
| 265eaeadad | |||
| cf0d3beb76 | |||
| ec386069a2 | |||
| ab36ae5bb8 | |||
| e6846048d0 | |||
| d6d9aad082 | |||
| 6723947fe6 | |||
| 24a7a197b0 | |||
| ab79c1200f | |||
| 0ae97b8162 | |||
| 4b79e2744d | |||
| 641e20bb01 | |||
| bc6c4a91ba | |||
| 82605fcfbb | |||
| 27876dfba9 | |||
| 1a2da892ac | |||
| c9bfed3373 | |||
| 3f732f5054 | |||
| 33de7eb54a | |||
| bad02ce90b |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Duraid Abdul
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -17,38 +17,32 @@ Welcome to LocalConsole! This Swift Package makes on-device debugging easy with
|
||||
```swift
|
||||
import LocalConsole
|
||||
|
||||
let localConsoleManager = LCManager.shared
|
||||
let consoleManager = LCManager.shared
|
||||
```
|
||||
|
||||
## **Usage**
|
||||
Once prepared, the localConsole can be used throughout your project.
|
||||
```swift
|
||||
|
||||
// Show the console view.
|
||||
localConsoleManager.isVisible = true
|
||||
// Activate the console view.
|
||||
consoleManager.isVisible = true
|
||||
|
||||
// Hide the console view.
|
||||
localConsoleManager.isVisible = false
|
||||
// Deactivate the console view.
|
||||
consoleManager.isVisible = false
|
||||
```
|
||||
|
||||
```swift
|
||||
// Print items to the console view.
|
||||
localConsoleManager.print("Hello, world!")
|
||||
consoleManager.print("Hello, world!")
|
||||
|
||||
// Clear console text.
|
||||
localConsoleManager.clear()
|
||||
consoleManager.clear()
|
||||
|
||||
// Copy console text.
|
||||
localConsoleManager.copy()
|
||||
consoleManager.copy()
|
||||
```
|
||||
|
||||
```swift
|
||||
// Change the console view font size.
|
||||
localConsoleManager.fontSize = 5
|
||||
consoleManager.fontSize = 5
|
||||
```
|
||||
|
||||
|
||||
## **To-Do**
|
||||
* Support for iOS 13
|
||||
* Screen edge console hiding
|
||||
* Make console view reactive to landscape/portrait switch
|
||||
|
||||
@@ -22,6 +22,7 @@ extension UIScreen {
|
||||
static var hasRoundedCorners = UIScreen.main.value(forKey: "_" + "display" + "Corner" + "Radius") as! CGFloat > 0
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
extension UIApplication {
|
||||
var statusBarHeight: CGFloat {
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
|
||||
@@ -5,19 +5,18 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
//#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
var GLOBAL_BORDER_TRACKERS: [BorderManager] = []
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
public static let shared = LCManager()
|
||||
|
||||
/// Set the font size. The font can be set to a minimum value of 5.0 and a maximum value of 20.0. The default value is 7.5.
|
||||
public var fontSize: CGFloat = 7.5 {
|
||||
/// Set the font size. The font can be set to a minimum value of 5.0 and a maximum value of 20.0. The default value is 8.
|
||||
public var fontSize: CGFloat = 8 {
|
||||
didSet {
|
||||
guard fontSize >= 4 else { fontSize = 4; return }
|
||||
guard fontSize <= 20 else { fontSize = 20; return }
|
||||
@@ -45,7 +44,63 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let defaultConsoleSize = CGSize(width: 228, height: 142)
|
||||
let defaultConsoleSize = CGSize(width: 240, height: 148)
|
||||
|
||||
lazy var borderView = UIView()
|
||||
|
||||
var lumaWidthAnchor: NSLayoutConstraint!
|
||||
var lumaHeightAnchor: NSLayoutConstraint!
|
||||
|
||||
lazy var lumaView: LumaView = {
|
||||
let lumaView = LumaView()
|
||||
lumaView.foregroundView.backgroundColor = .black
|
||||
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
||||
|
||||
consoleView.addSubview(lumaView)
|
||||
|
||||
lumaView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
lumaWidthAnchor = lumaView.widthAnchor.constraint(equalTo: consoleView.widthAnchor)
|
||||
lumaHeightAnchor = lumaView.heightAnchor.constraint(equalToConstant: consoleView.frame.size.height)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
lumaWidthAnchor,
|
||||
lumaHeightAnchor,
|
||||
lumaView.centerXAnchor.constraint(equalTo: consoleView.centerXAnchor),
|
||||
lumaView.centerYAnchor.constraint(equalTo: consoleView.centerYAnchor)
|
||||
])
|
||||
|
||||
return lumaView
|
||||
}()
|
||||
|
||||
lazy var unhideButton: UIButton = {
|
||||
let button = UIButton()
|
||||
|
||||
button.addAction(UIAction(handler: { [self] _ in
|
||||
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
||||
consoleView.center = nearestTargetTo(consoleView.center, possibleTargets: possibleEndpoints.dropLast())
|
||||
}.startAnimation()
|
||||
grabberMode = false
|
||||
|
||||
UserDefaults.standard.set(consoleView.center.x, forKey: "LocalConsole_X")
|
||||
UserDefaults.standard.set(consoleView.center.y, forKey: "LocalConsole_Y")
|
||||
}), for: .touchUpInside)
|
||||
|
||||
consoleView.addSubview(button)
|
||||
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
button.widthAnchor.constraint(equalTo: consoleView.widthAnchor),
|
||||
button.heightAnchor.constraint(equalTo: consoleView.heightAnchor),
|
||||
button.centerXAnchor.constraint(equalTo: consoleView.centerXAnchor),
|
||||
button.centerYAnchor.constraint(equalTo: consoleView.centerYAnchor)
|
||||
])
|
||||
|
||||
button.isHidden = true
|
||||
|
||||
return button
|
||||
}()
|
||||
|
||||
/// The fixed size of the console view.
|
||||
lazy var consoleSize = defaultConsoleSize {
|
||||
@@ -54,22 +109,22 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// Update text view width.
|
||||
if consoleView.frame.size.width > ResizeController.kMaxConsoleWidth {
|
||||
consoleTextView.frame.size.width = ResizeController.kMaxConsoleWidth - 4
|
||||
consoleTextView.frame.size.width = ResizeController.kMaxConsoleWidth - 2
|
||||
} else if consoleView.frame.size.width < ResizeController.kMinConsoleWidth {
|
||||
consoleTextView.frame.size.width = ResizeController.kMinConsoleWidth - 4
|
||||
consoleTextView.frame.size.width = ResizeController.kMinConsoleWidth - 2
|
||||
} else {
|
||||
consoleTextView.frame.size.width = consoleSize.width - 4
|
||||
consoleTextView.frame.size.width = consoleSize.width - 2
|
||||
}
|
||||
|
||||
// Update text view height.
|
||||
if consoleView.frame.size.height > ResizeController.kMaxConsoleHeight {
|
||||
consoleTextView.frame.size.height = ResizeController.kMaxConsoleHeight - 4
|
||||
consoleTextView.frame.size.height = ResizeController.kMaxConsoleHeight - 2
|
||||
+ (consoleView.frame.size.height - ResizeController.kMaxConsoleHeight) * 2 / 3
|
||||
} else if consoleView.frame.size.height < ResizeController.kMinConsoleHeight {
|
||||
consoleTextView.frame.size.height = ResizeController.kMinConsoleHeight - 4
|
||||
consoleTextView.frame.size.height = ResizeController.kMinConsoleHeight - 2
|
||||
+ (consoleView.frame.size.height - ResizeController.kMinConsoleHeight) * 2 / 3
|
||||
} else {
|
||||
consoleTextView.frame.size.height = consoleSize.height - 4
|
||||
consoleTextView.frame.size.height = consoleSize.height - 2
|
||||
}
|
||||
|
||||
consoleTextView.contentOffset.y = consoleTextView.contentSize.height - consoleTextView.bounds.size.height
|
||||
@@ -85,11 +140,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
var consoleWindow: ConsoleWindow?
|
||||
|
||||
// The console needs a parent view controller in order to display context menus.
|
||||
let viewController = UIViewController()
|
||||
lazy var viewController = UIViewController()
|
||||
lazy var consoleView = viewController.view!
|
||||
|
||||
/// Text view that displays printed items.
|
||||
let consoleTextView = InvertedTextView()
|
||||
lazy var consoleTextView = InvertedTextView()
|
||||
|
||||
/// Button that reveals menu.
|
||||
lazy var menuButton = UIButton()
|
||||
@@ -98,27 +153,92 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
var scrollLocked = true
|
||||
|
||||
/// Feedback generator for the long press action.
|
||||
let feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
lazy var feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
|
||||
lazy var panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(consolePiPPanner(recognizer:)))
|
||||
lazy var longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(recognizer:)))
|
||||
|
||||
/// Gesture endpoints. Each point represents a corner of the screen. TODO: Handle screen rotation.
|
||||
var possibleEndpoints: [CGPoint] {
|
||||
|
||||
if consoleSize.width < UIScreen.portraitSize.width - 112 {
|
||||
return [CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
|
||||
// Four endpoints, one for each corner.
|
||||
var endpoints = [
|
||||
|
||||
// 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)]
|
||||
|
||||
if consoleView.frame.minX <= 0 {
|
||||
|
||||
// Left edge endpoints.
|
||||
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))
|
||||
}
|
||||
} else if consoleView.frame.maxX >= UIScreen.portraitSize.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))
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
|
||||
} else {
|
||||
return [CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
|
||||
// Two endpoints, one for the top, one for the bottom..
|
||||
var endpoints = [CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: (UIScreen.hasRoundedCorners ? 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)]
|
||||
|
||||
if consoleView.frame.minX <= 0 {
|
||||
|
||||
// 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))
|
||||
}
|
||||
} else if consoleView.frame.maxX >= UIScreen.portraitSize.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,
|
||||
y: endpoints[0].y))
|
||||
} else {
|
||||
endpoints.append(CGPoint(x: UIScreen.portraitSize.width + consoleSize.width / 2 - 28,
|
||||
y: endpoints[1].y))
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,21 +248,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleSize = CGSize(width: UserDefaults.standard.object(forKey: "LocalConsole_Width") as? CGFloat ?? consoleSize.width,
|
||||
height: UserDefaults.standard.object(forKey: "LocalConsole_Height") as? CGFloat ?? consoleSize.height)
|
||||
|
||||
consoleView.backgroundColor = .black
|
||||
|
||||
consoleView.layer.shadowRadius = 16
|
||||
consoleView.layer.shadowOpacity = 0.5
|
||||
consoleView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
consoleView.center = possibleEndpoints.first!
|
||||
consoleView.alpha = 0
|
||||
|
||||
consoleView.layer.cornerRadius = 22
|
||||
consoleView.layer.cornerRadius = 24
|
||||
consoleView.layer.cornerCurve = .continuous
|
||||
|
||||
let borderView = UIView()
|
||||
let _ = lumaView
|
||||
|
||||
borderView.frame = CGRect(x: -1, y: -1,
|
||||
width: consoleSize.width + 2,
|
||||
height: consoleSize.height + 2)
|
||||
width: consoleSize.width + 2,
|
||||
height: consoleSize.height + 2)
|
||||
borderView.layer.borderWidth = 1
|
||||
borderView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
|
||||
borderView.layer.cornerRadius = consoleView.layer.cornerRadius + 1
|
||||
@@ -151,10 +270,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.addSubview(borderView)
|
||||
|
||||
// Configure text view.
|
||||
consoleTextView.frame = CGRect(x: 2, y: 2, width: consoleSize.width - 4, height: consoleSize.height - 4)
|
||||
consoleTextView.frame = CGRect(x: 1, y: 1, width: consoleSize.width - 2, height: consoleSize.height - 2)
|
||||
consoleTextView.isEditable = false
|
||||
consoleTextView.backgroundColor = .clear
|
||||
consoleTextView.textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 10, right: 8)
|
||||
consoleTextView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
|
||||
|
||||
consoleTextView.isSelectable = false
|
||||
consoleTextView.showsVerticalScrollIndicator = false
|
||||
@@ -178,7 +297,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
// Prepare menu button.
|
||||
let diameter = CGFloat(28)
|
||||
let diameter = CGFloat(30)
|
||||
|
||||
// This tuned button frame is used to adjust where the menu appears.
|
||||
menuButton = UIButton(frame: CGRect(x: consoleView.bounds.width - 44,
|
||||
@@ -198,7 +317,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
circle.isUserInteractionEnabled = false
|
||||
menuButton.addSubview(circle)
|
||||
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17)))
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)))
|
||||
ellipsisImage.frame.size = circle.bounds.size
|
||||
ellipsisImage.contentMode = .center
|
||||
circle.addSubview(ellipsisImage)
|
||||
@@ -208,6 +328,8 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
consoleView.addSubview(menuButton)
|
||||
|
||||
let _ = unhideButton
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
}
|
||||
@@ -216,6 +338,21 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
func configureWindow() {
|
||||
var windowSceneFound = false
|
||||
|
||||
// Update console cached based on last-cached origin.
|
||||
func updateConsoleOrigin() {
|
||||
snapToCachedEndpoint()
|
||||
|
||||
if consoleView.center.x < 0 || consoleView.center.x > UIScreen.portraitSize.width {
|
||||
grabberMode = true
|
||||
scrollLocked = !grabberMode
|
||||
|
||||
consoleView.layer.removeAllAnimations()
|
||||
lumaView.layer.removeAllAnimations()
|
||||
menuButton.layer.removeAllAnimations()
|
||||
consoleTextView.layer.removeAllAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
// Configure console window.
|
||||
func fetchWindowScene() {
|
||||
let windowScene = UIApplication.shared
|
||||
@@ -234,11 +371,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleWindow?.addSubview(consoleView)
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride
|
||||
|
||||
updateConsoleOrigin()
|
||||
}
|
||||
}
|
||||
|
||||
fetchWindowScene()
|
||||
|
||||
/// Ensures the window is configured (i.e. scene has been found). If not, delay and wait for a scene to prepare itself, then try again.
|
||||
for i in 1...10 {
|
||||
|
||||
@@ -259,6 +396,14 @@ 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)
|
||||
|
||||
consoleView.center = cachedConsolePosition // Update console center so possibleEndpoints are calculated correctly.
|
||||
consoleView.center = nearestTargetTo(cachedConsolePosition, possibleTargets: possibleEndpoints)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
public var isVisible = false {
|
||||
@@ -268,9 +413,11 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
if isVisible {
|
||||
|
||||
if !isConsoleConfigured {
|
||||
configureWindow()
|
||||
configureConsole()
|
||||
isConsoleConfigured = true
|
||||
DispatchQueue.main.async { [self] in
|
||||
configureWindow()
|
||||
configureConsole()
|
||||
isConsoleConfigured = true
|
||||
}
|
||||
}
|
||||
|
||||
commitTextChanges(requestMenuUpdate: true)
|
||||
@@ -302,7 +449,61 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private var _hasRelayedOffsetChange = false
|
||||
var grabberMode: Bool = false {
|
||||
|
||||
didSet {
|
||||
guard oldValue != grabberMode else { return }
|
||||
|
||||
if grabberMode {
|
||||
|
||||
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
||||
lumaHeightAnchor.constant = consoleView.frame.size.height
|
||||
consoleView.layoutIfNeeded()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 0
|
||||
menuButton.alpha = 0
|
||||
borderView.alpha = 0
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) { [self] in
|
||||
lumaView.foregroundView.alpha = 0
|
||||
}.startAnimation()
|
||||
|
||||
lumaWidthAnchor.constant = -34
|
||||
lumaHeightAnchor.constant = 96
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
lumaView.layer.cornerRadius = 8
|
||||
consoleView.layoutIfNeeded()
|
||||
}.startAnimation(afterDelay: 0.06)
|
||||
|
||||
consoleTextView.isUserInteractionEnabled = false
|
||||
unhideButton.isHidden = false
|
||||
|
||||
} else {
|
||||
|
||||
lumaHeightAnchor.constant = consoleView.frame.size.height
|
||||
lumaWidthAnchor.constant = 0
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.layoutIfNeeded()
|
||||
lumaView.layer.cornerRadius = consoleView.layer.cornerRadius
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
menuButton.alpha = 1
|
||||
borderView.alpha = 1
|
||||
}.startAnimation(afterDelay: 0.2)
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.65, dampingRatio: 1) { [self] in
|
||||
lumaView.foregroundView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
consoleTextView.isUserInteractionEnabled = true
|
||||
unhideButton.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print items to the console view.
|
||||
public func print(_ items: Any) {
|
||||
@@ -325,18 +526,25 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
var temporaryKeyboardHeightValueTracker: CGFloat?
|
||||
|
||||
// MARK: Handle keyboard show/hide.
|
||||
private var keyboardHeight: CGFloat? = nil {
|
||||
didSet {
|
||||
|
||||
temporaryKeyboardHeightValueTracker = oldValue
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
temporaryKeyboardHeightValueTracker = keyboardHeight
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,12 +589,46 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var dynamicReportTimer: Timer? {
|
||||
willSet { dynamicReportTimer?.invalidate() }
|
||||
}
|
||||
|
||||
func systemReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
if currentText != "" { print("\n") }
|
||||
|
||||
dynamicReportTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
|
||||
var _currentText = currentText
|
||||
|
||||
// To optimize performance, only scan the last 2500 characters of text for system report changes.
|
||||
let range: NSRange = {
|
||||
if _currentText.count <= 2500 {
|
||||
return NSMakeRange(0, _currentText.count)
|
||||
}
|
||||
return NSMakeRange(_currentText.count - 2500, 2500)
|
||||
}()
|
||||
|
||||
let regex0 = try! NSRegularExpression(pattern: "Thermal State: .*", options: NSRegularExpression.Options.caseInsensitive)
|
||||
_currentText = regex0.stringByReplacingMatches(in: _currentText, options: [], range: range, withTemplate: "Thermal State: \(SystemReport.shared.thermalState)")
|
||||
|
||||
let regex1 = try! NSRegularExpression(pattern: "System Uptime: .*", options: NSRegularExpression.Options.caseInsensitive)
|
||||
_currentText = regex1.stringByReplacingMatches(in: _currentText, options: [], range: range, withTemplate: "System Uptime: \(ProcessInfo.processInfo.systemUptime.formattedString!)")
|
||||
|
||||
let regex2 = try! NSRegularExpression(pattern: "Low Power Mode: .*", options: NSRegularExpression.Options.caseInsensitive)
|
||||
_currentText = regex2.stringByReplacingMatches(in: _currentText, options: [], range: range, withTemplate: "Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)")
|
||||
|
||||
if currentText != _currentText {
|
||||
currentText = _currentText
|
||||
} else {
|
||||
|
||||
// Invalidate the timer if there is no longer anything to update.
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
print(
|
||||
"""
|
||||
\n
|
||||
Model Name: \(SystemReport.shared.gestaltMarketingName)
|
||||
Model Identifier: \(SystemReport.shared.gestaltModelIdentifier)
|
||||
Architecture: \(SystemReport.shared.gestaltArchitecture)
|
||||
@@ -397,20 +639,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
Memory: \(round(100 * Double(ProcessInfo.processInfo.physicalMemory) * pow(10, -9)) / 100) GB
|
||||
Processor Cores: \(Int(ProcessInfo.processInfo.processorCount))
|
||||
Thermal State: \(SystemReport.shared.thermalState)
|
||||
System Uptime: \(Int(ProcessInfo.processInfo.systemUptime))s
|
||||
System Uptime: \(ProcessInfo.processInfo.systemUptime.formattedString!)
|
||||
Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)
|
||||
"""
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func displayReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
if currentText != "" { print("\n") }
|
||||
|
||||
print(
|
||||
"""
|
||||
\n
|
||||
Screen Size: \(UIScreen.main.bounds.size)
|
||||
Screen Corner Radius: \(UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat)
|
||||
Screen Scale: \(UIScreen.main.scale)
|
||||
@@ -421,21 +663,17 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleLock() {
|
||||
scrollLocked.toggle()
|
||||
}
|
||||
|
||||
func commitTextChanges(requestMenuUpdate menuUpdateRequested: Bool) {
|
||||
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - 20 - consoleTextView.bounds.size.height ||
|
||||
_hasRelayedOffsetChange == false {
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - consoleTextView.bounds.size.height - 20 {
|
||||
|
||||
_hasRelayedOffsetChange = true
|
||||
// Weird, weird fix that makes the scroll view bottom pinning system work.
|
||||
consoleTextView.isScrollEnabled.toggle()
|
||||
consoleTextView.isScrollEnabled.toggle()
|
||||
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
}
|
||||
|
||||
consoleTextView.text = currentText
|
||||
|
||||
setAttributedText(currentText)
|
||||
|
||||
if menuUpdateRequested {
|
||||
@@ -460,22 +698,22 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
func makeMenu() -> UIMenu {
|
||||
|
||||
let copy = UIAction(title: "Copy",
|
||||
image: UIImage(systemName: "doc.on.doc"), handler: { _ in
|
||||
self.copy()
|
||||
})
|
||||
image: UIImage(systemName: "doc.on.doc"), handler: { _ in
|
||||
self.copy()
|
||||
})
|
||||
|
||||
let resize = UIAction(title: "Resize Console",
|
||||
image: UIImage(systemName: "arrow.left.and.right.square"), handler: { _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
ResizeController.shared.isActive.toggle()
|
||||
ResizeController.shared.platterView.reveal()
|
||||
}
|
||||
})
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
ResizeController.shared.isActive.toggle()
|
||||
ResizeController.shared.platterView.reveal()
|
||||
}
|
||||
})
|
||||
|
||||
let clear = UIAction(title: "Clear Console",
|
||||
image: UIImage(systemName: "xmark.square"), handler: { _ in
|
||||
self.clear()
|
||||
})
|
||||
self.clear()
|
||||
})
|
||||
|
||||
let consoleActions = UIMenu(title: "", options: .displayInline, children: [clear, resize])
|
||||
|
||||
@@ -486,14 +724,14 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
let viewFrames = UIAction(title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
|
||||
image: UIImage(systemName: frameSymbol), handler: { _ in
|
||||
self.debugBordersEnabled.toggle()
|
||||
self.menuButton.menu = self.makeMenu()
|
||||
})
|
||||
self.debugBordersEnabled.toggle()
|
||||
self.menuButton.menu = self.makeMenu()
|
||||
})
|
||||
|
||||
let systemReport = UIAction(title: "System Report",
|
||||
image: UIImage(systemName: "cpu"), handler: { _ in
|
||||
self.systemReport()
|
||||
})
|
||||
image: UIImage(systemName: "cpu"), handler: { _ in
|
||||
self.systemReport()
|
||||
})
|
||||
|
||||
// Show the right glyph for the current device being used.
|
||||
let deviceSymbol: String = {
|
||||
@@ -518,28 +756,35 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}()
|
||||
|
||||
let displayReport = UIAction(title: "Display Report",
|
||||
image: UIImage(systemName: deviceSymbol), handler: { _ in
|
||||
self.displayReport()
|
||||
})
|
||||
image: UIImage(systemName: deviceSymbol), handler: { _ in
|
||||
self.displayReport()
|
||||
})
|
||||
|
||||
let respring = UIAction(title: "Restart Spring" + "Board",
|
||||
image: UIImage(systemName: "apps.iphone"), handler: { _ in
|
||||
guard let window = UIApplication.shared.windows.first else { return }
|
||||
|
||||
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat
|
||||
window.layer.masksToBounds = true
|
||||
|
||||
let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
||||
window.transform = .init(scaleX: 0.96, y: 0.96)
|
||||
window.alpha = 0
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
while true {
|
||||
window.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
}
|
||||
animator.startAnimation()
|
||||
})
|
||||
|
||||
guard let window = UIApplication.shared.windows.first else { return }
|
||||
|
||||
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat
|
||||
window.layer.masksToBounds = true
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
||||
window.transform = .init(scaleX: 0.96, y: 0.96)
|
||||
window.alpha = 0
|
||||
}.startAnimation()
|
||||
|
||||
// Concurrently run these snapshots to decrease the time to crash.
|
||||
for _ in 0...1000 {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
|
||||
// This will cause jetsam to terminate SpringBoard.
|
||||
while true {
|
||||
window.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let debugActions = UIMenu(title: "", options: .displayInline,
|
||||
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
|
||||
children: [viewFrames, systemReport, displayReport, respring])])
|
||||
@@ -559,6 +804,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
@objc func longPressAction(recognizer: UILongPressGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
|
||||
guard !grabberMode else { return }
|
||||
|
||||
feedbackGenerator.selectionChanged()
|
||||
|
||||
scrollLocked = false
|
||||
@@ -566,25 +814,33 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1.04, y: 1.04)
|
||||
consoleTextView.alpha = 0.5
|
||||
menuButton.alpha = 0.5
|
||||
}.startAnimation()
|
||||
case .cancelled, .ended:
|
||||
|
||||
scrollLocked = true
|
||||
if !grabberMode { scrollLocked = true }
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
consoleView.transform = .identity
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
if !grabberMode {
|
||||
consoleTextView.alpha = 1
|
||||
menuButton.alpha = 1
|
||||
}
|
||||
}.startAnimation()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
let consolePiPPanner_frameRateRequest = FrameRateRequest()
|
||||
|
||||
@objc func consolePiPPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
if recognizer.state == .began {
|
||||
consolePiPPanner_frameRateRequest.isActive = true
|
||||
|
||||
initialViewLocation = consoleView.center
|
||||
}
|
||||
|
||||
@@ -596,11 +852,22 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
switch recognizer.state {
|
||||
case .changed:
|
||||
|
||||
consoleView.center.x = initialViewLocation.x + translation.x
|
||||
consoleView.center.y = initialViewLocation.y + translation.y
|
||||
UIViewPropertyAnimator(duration: 0.175, dampingRatio: 1) { [self] in
|
||||
consoleView.center = CGPoint(x: initialViewLocation.x + translation.x,
|
||||
y: initialViewLocation.y + translation.y)
|
||||
}.startAnimation()
|
||||
|
||||
if consoleView.frame.maxX > 30 && consoleView.frame.minX < UIScreen.portraitSize.width - 30 {
|
||||
grabberMode = false
|
||||
} else {
|
||||
grabberMode = true
|
||||
}
|
||||
|
||||
case .ended, .cancelled:
|
||||
|
||||
consolePiPPanner_frameRateRequest.isActive = true
|
||||
FrameRateRequest().perform(duration: 0.5)
|
||||
|
||||
// After the PiP is thrown, determine the best corner and re-target it there.
|
||||
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
|
||||
|
||||
@@ -616,27 +883,31 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
dy: relativeVelocity(forVelocity: velocity.y, from: consoleView.center.y, to: nearestTargetPosition.y)
|
||||
)
|
||||
|
||||
let timingParameters = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)
|
||||
let timingParameters = UISpringTimingParameters(damping: 0.85, response: 0.45, initialVelocity: relativeInitialVelocity)
|
||||
let positionAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
|
||||
positionAnimator.addAnimations { [self] in
|
||||
consoleView.center = nearestTargetPosition
|
||||
}
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
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.scrollLocked = !self.grabberMode
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// Animate touch down.
|
||||
func consolePiPTouchDown() {
|
||||
UIViewPropertyAnimator(duration: 1, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.96, y: 0.96)
|
||||
}.startAnimation()
|
||||
guard !grabberMode else { return }
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
if !scrollLocked {
|
||||
consoleView.backgroundColor = #colorLiteral(red: 0.1331297589, green: 0.1331297589, blue: 0.1331297589, alpha: 1)
|
||||
}
|
||||
UIViewPropertyAnimator(duration: 1.25, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.95, y: 0.95)
|
||||
}.startAnimation()
|
||||
}
|
||||
|
||||
@@ -647,11 +918,12 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) { [self] in
|
||||
consoleView.backgroundColor = .black
|
||||
if !grabberMode {
|
||||
consoleTextView.alpha = 1
|
||||
if !ResizeController.shared.isActive {
|
||||
menuButton.alpha = 1
|
||||
}
|
||||
}
|
||||
}.startAnimation()
|
||||
}
|
||||
|
||||
@@ -664,15 +936,9 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
consolePiPTouchDown()
|
||||
case .cancelled:
|
||||
consolePiPTouchUp()
|
||||
case .changed:
|
||||
break
|
||||
case .ended:
|
||||
consolePiPTouchUp()
|
||||
case .failed:
|
||||
consolePiPTouchUp()
|
||||
case .possible:
|
||||
case .ended, .cancelled, .possible, .failed:
|
||||
consolePiPTouchUp()
|
||||
@unknown default:
|
||||
break
|
||||
@@ -715,7 +981,7 @@ extension UIView {
|
||||
let swizzledMethod = class_getInstanceMethod(UIView.self, #selector(swizzled_layoutSubviews)) else { return }
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}
|
||||
|
||||
|
||||
@objc func swizzled_layoutSubviews() {
|
||||
swizzled_layoutSubviews()
|
||||
|
||||
@@ -740,7 +1006,80 @@ extension UIWindow {
|
||||
}
|
||||
}
|
||||
|
||||
//#endif
|
||||
class LumaView: UIView {
|
||||
lazy var visualEffectView: UIView = {
|
||||
Bundle(path: "/Sys" + "tem/Lib" + "rary/Private" + "Frameworks/Material" + "Kit." + "framework")!.load()
|
||||
|
||||
let Pill = NSClassFromString("MT" + "Luma" + "Dodge" + "Pill" + "View") as! UIView.Type
|
||||
|
||||
let pillView = Pill.init()
|
||||
|
||||
enum Style: Int {
|
||||
case none = 0
|
||||
case thin = 1
|
||||
case gray = 2
|
||||
case black = 3
|
||||
case white = 4
|
||||
}
|
||||
|
||||
enum BackgroundLuminance: Int {
|
||||
case unknown = 0
|
||||
case dark = 1
|
||||
case light = 2
|
||||
}
|
||||
|
||||
pillView.setValue(2, forKey: "style")
|
||||
pillView.setValue(1, forKey: "background" + "Luminance")
|
||||
pillView.perform(NSSelectorFromString("_" + "update" + "Style"))
|
||||
|
||||
addSubview(pillView)
|
||||
|
||||
pillView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
pillView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
pillView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
pillView.topAnchor.constraint(equalTo: topAnchor),
|
||||
pillView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
return pillView
|
||||
}()
|
||||
|
||||
lazy var foregroundView: UIView = {
|
||||
let view = UIView()
|
||||
|
||||
addSubview(view)
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
view.topAnchor.constraint(equalTo: topAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let _ = visualEffectView
|
||||
let _ = foregroundView
|
||||
|
||||
visualEffectView.isUserInteractionEnabled = false
|
||||
foregroundView.isUserInteractionEnabled = false
|
||||
|
||||
layer.cornerCurve = .continuous
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class InvertedTextView: UITextView {
|
||||
|
||||
@@ -749,7 +1088,7 @@ class InvertedTextView: UITextView {
|
||||
// Thanks to WWDC21 Lab!
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
|
||||
if panGestureRecognizer.numberOfTouches == 0 && pendingOffsetChange {
|
||||
contentOffset.y = contentSize.height - bounds.size.height
|
||||
} else {
|
||||
@@ -771,3 +1110,65 @@ class InvertedTextView: UITextView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
var formattedString: String? {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.hour, .minute, .second]
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func _debugPrint(_ items: Any) {
|
||||
print(items)
|
||||
}
|
||||
|
||||
// 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
|
||||
let request = FrameRateRequest(preferredFrameRate: 120,
|
||||
duration: 0.4)
|
||||
request.perform()
|
||||
```
|
||||
*/
|
||||
class FrameRateRequest {
|
||||
|
||||
lazy private var displayLink = CADisplayLink(target: self, selector: #selector(dummyFunction))
|
||||
|
||||
var isActive: Bool = false {
|
||||
didSet {
|
||||
guard #available(iOS 15, *) else { return }
|
||||
guard isActive != oldValue else { return }
|
||||
|
||||
if isActive {
|
||||
displayLink.add(to: .current, forMode: .common)
|
||||
} else {
|
||||
displayLink.remove(from: .current, forMode: .common)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform frame rate request.
|
||||
func perform(duration: Double) {
|
||||
isActive = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [self] in
|
||||
isActive = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dummyFunction() {}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class ResizeController {
|
||||
|
||||
public static let shared = ResizeController()
|
||||
@@ -114,6 +115,8 @@ class ResizeController {
|
||||
// Ensure initial autolayout is performed unanimated.
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
|
||||
FrameRateRequest().perform(duration: 1.5)
|
||||
|
||||
if isActive {
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) {
|
||||
@@ -169,7 +172,7 @@ class ResizeController {
|
||||
LCManager.shared.consoleView.layer.shadowOpacity = 0.5
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
LCManager.shared.consoleView.center = LCManager.shared.possibleEndpoints.first!
|
||||
LCManager.shared.snapToCachedEndpoint()
|
||||
|
||||
// Update grabbers (layout constraints)
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
@@ -200,6 +203,8 @@ class ResizeController {
|
||||
static let kMinConsoleHeight: CGFloat = 108
|
||||
static let kMaxConsoleHeight: CGFloat = 346
|
||||
|
||||
let verticalPanner_frameRateRequest = FrameRateRequest()
|
||||
|
||||
@objc func verticalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: bottomGrabber.superview)
|
||||
@@ -209,6 +214,8 @@ class ResizeController {
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
verticalPanner_frameRateRequest.isActive = true
|
||||
|
||||
initialHeight = LCManager.shared.consoleSize.height
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
@@ -235,16 +242,23 @@ class ResizeController {
|
||||
}
|
||||
}()
|
||||
|
||||
LCManager.shared.lumaHeightAnchor.constant = resolvedHeight
|
||||
LCManager.shared.consoleSize.height = resolvedHeight
|
||||
LCManager.shared.consoleView.center.y = consoleCenterPoint.y
|
||||
|
||||
case .ended, .cancelled:
|
||||
verticalPanner_frameRateRequest.isActive = false
|
||||
|
||||
FrameRateRequest().perform(duration: 0.4)
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
|
||||
if LCManager.shared.consoleSize.height > maxHeight {
|
||||
LCManager.shared.consoleSize.height = maxHeight
|
||||
LCManager.shared.lumaHeightAnchor.constant = maxHeight
|
||||
}
|
||||
if LCManager.shared.consoleSize.height < minHeight {
|
||||
LCManager.shared.consoleSize.height = minHeight
|
||||
LCManager.shared.lumaHeightAnchor.constant = minHeight
|
||||
}
|
||||
|
||||
LCManager.shared.consoleView.center.y = self.consoleCenterPoint.y
|
||||
@@ -266,6 +280,8 @@ class ResizeController {
|
||||
static let kMinConsoleWidth: CGFloat = 112
|
||||
static let kMaxConsoleWidth: CGFloat = UIScreen.portraitSize.width - 56
|
||||
|
||||
let horizontalPanner_frameRateRequest = FrameRateRequest()
|
||||
|
||||
@objc func horizontalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: bottomGrabber.superview)
|
||||
@@ -275,6 +291,8 @@ class ResizeController {
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
horizontalPanner_frameRateRequest.isActive = true
|
||||
|
||||
initialWidth = LCManager.shared.consoleSize.width
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
@@ -306,6 +324,10 @@ class ResizeController {
|
||||
|
||||
case .ended, .cancelled:
|
||||
|
||||
horizontalPanner_frameRateRequest.isActive = false
|
||||
|
||||
FrameRateRequest().perform(duration: 0.4)
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
|
||||
if LCManager.shared.consoleSize.width > maxWidth {
|
||||
LCManager.shared.consoleSize.width = maxWidth
|
||||
@@ -329,6 +351,7 @@ class ResizeController {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class PlatterView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@@ -473,6 +496,7 @@ class PlatterView: UIView {
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
|
||||
LCManager.shared.consoleSize = LCManager.shared.defaultConsoleSize
|
||||
LCManager.shared.lumaHeightAnchor.constant = LCManager.shared.defaultConsoleSize.height
|
||||
LCManager.shared.consoleView.center = ResizeController.shared.consoleCenterPoint
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
}.startAnimation()
|
||||
|
||||
Reference in New Issue
Block a user