Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b79e2744d | |||
| 641e20bb01 | |||
| bc6c4a91ba | |||
| 82605fcfbb | |||
| 27876dfba9 | |||
| 1a2da892ac | |||
| c9bfed3373 | |||
| 3f732f5054 | |||
| 33de7eb54a | |||
| bad02ce90b | |||
| c2cdc1c822 | |||
| 61ed2d92db | |||
| a522748e3c | |||
| 58aecf3a9d | |||
| 971de252bf | |||
| ef0bd6cd8a | |||
| 5f8c210c62 | |||
| 0bbfabc568 | |||
| 157ab2153f | |||
| 51ddd6c2c2 | |||
| c5a5641906 | |||
| cbbbf2c6db | |||
| a75c5763a4 | |||
| a66afaef04 | |||
| 7ca52868ff | |||
| 417bdf2fb3 | |||
| 92299c62de | |||
| 22cd8d12ff | |||
| de62ed79af | |||
| 261ba2a83b | |||
| 697d636cbc | |||
| cdbb4fef4d | |||
| dea5e6e4e0 | |||
| b28605e52e | |||
| 35a5350f17 | |||
| b319499056 | |||
| 17d30ad554 | |||
| b56d27ff26 | |||
| c7fe1daf26 | |||
| 6db60e25b2 | |||
| 5094ddc710 | |||
| 9b5e0d84ac | |||
| 1620fad461 | |||
| 86d9e18613 | |||
| 832f507ab1 | |||
| 2406568789 | |||
| f57800e8a4 | |||
| e7bc5d221b | |||
| 6e121fb39b | |||
| 4c8a519c4f | |||
| d3d12e2450 | |||
| 062f74a429 | |||
| c8468823c0 | |||
| c19df26edc | |||
| d1227ee522 | |||
| 97a6ae0899 | |||
| 4b0f9e149a | |||
| 20f88c739f | |||
| 98a66af9b2 | |||
| 19f2302f13 | |||
| 308a25f6db | |||
| 40614dffec | |||
| 245d6f0310 | |||
| 349d7359eb | |||
| 37e0ab6323 | |||
| 1ea0dc7026 | |||
| a9db186136 | |||
| 67e2ff5ce5 | |||
| 1c12c101b6 | |||
| cd31f0d55d | |||
| 7a4a020c9c | |||
| 7208c56820 | |||
| 8d7f902344 | |||
| eca9258206 | |||
| 87824eb760 | |||
| 5bcd63c6b9 | |||
| ec233369aa | |||
| 98ff7089cd | |||
| 1d41bbd85e | |||
| c2bc901427 | |||
| 6025ea6cc2 | |||
| 1bf0e61732 | |||
| 82c5086787 | |||
| a8b7f702b4 | |||
| a7a5cba7d9 | |||
| 20e44ee90a |
@@ -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.
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
Welcome to LocalConsole! This Swift Package makes on-device debugging easy with a convenient PiP-style console that can display items in the same way ```print()``` will in Xcode. This tool can also dynamically display view frames and restart SpringBoard right from your live app.
|
||||
|
||||
<img src="https://github.com/duraidabdul/LocalConsole/blob/main/Demo.gif?raw=true" width="250" height="500">
|
||||
<div>
|
||||
<img src="https://github.com/duraidabdul/Demos/blob/main/Demo_Pan.gif?raw=true" width="320">
|
||||
<img src="https://github.com/duraidabdul/Demos/blob/main/Demo_Resize.gif?raw=true" width="320">
|
||||
</div>
|
||||
|
||||
## **Setup**
|
||||
|
||||
@@ -14,7 +17,7 @@ Welcome to LocalConsole! This Swift Package makes on-device debugging easy with
|
||||
```swift
|
||||
import LocalConsole
|
||||
|
||||
let localConsoleManager = LCManager.shared
|
||||
let consoleManager = LCManager.shared
|
||||
```
|
||||
|
||||
## **Usage**
|
||||
@@ -22,26 +25,29 @@ Once prepared, the localConsole can be used throughout your project.
|
||||
```swift
|
||||
|
||||
// Show the console view.
|
||||
localConsoleManager.isVisible = true
|
||||
consoleManager.isVisible = true
|
||||
|
||||
// Hide the console view.
|
||||
localConsoleManager.isVisible = false
|
||||
consoleManager.isVisible = false
|
||||
```
|
||||
|
||||
```swift
|
||||
// Print items to the console view.
|
||||
localConsoleManager.print("Hello, world!")
|
||||
consoleManager.print("Hello, world!")
|
||||
|
||||
// Clear text in the console view.
|
||||
localConsoleManager.clear()
|
||||
// Clear console text.
|
||||
consoleManager.clear()
|
||||
|
||||
// Copy console text.
|
||||
consoleManager.copy()
|
||||
```
|
||||
|
||||
```swift
|
||||
// Change the console view font size.
|
||||
localConsoleManager.fontSize = 5
|
||||
consoleManager.fontSize = 5
|
||||
```
|
||||
|
||||
|
||||
## **Upcoming Features**
|
||||
* Custom console view size
|
||||
* Support for iOS 13
|
||||
## **To-Do**
|
||||
* Screen edge console hiding
|
||||
* Make console view reactive to landscape/portrait switch
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
/// This class handles enabling and disabling debug borders on a specified view.
|
||||
@@ -64,5 +62,3 @@ class BorderManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIScreen {
|
||||
@@ -15,8 +13,16 @@ extension UIScreen {
|
||||
static var size: CGSize {
|
||||
return UIScreen.main.bounds.size
|
||||
}
|
||||
|
||||
static var portraitSize: CGSize {
|
||||
return CGSize(width: UIScreen.main.nativeBounds.width / UIScreen.main.nativeScale,
|
||||
height: UIScreen.main.nativeBounds.height / UIScreen.main.nativeScale)
|
||||
}
|
||||
|
||||
static var hasRoundedCorners = UIScreen.main.value(forKey: "_" + "display" + "Corner" + "Radius") as! CGFloat > 0
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
extension UIApplication {
|
||||
var statusBarHeight: CGFloat {
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
@@ -35,4 +41,20 @@ extension UIFont {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
extension UIControl {
|
||||
func addActions(highlightAction: UIAction, unhighlightAction: UIAction) {
|
||||
addAction(highlightAction, for: .touchDown)
|
||||
addAction(highlightAction, for: .touchDragEnter)
|
||||
|
||||
addAction(unhighlightAction, for: .touchUpInside)
|
||||
addAction(unhighlightAction, for: .touchDragExit)
|
||||
addAction(unhighlightAction, for: .touchCancel)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
func roundOriginToPixel() {
|
||||
frame.origin.x = (round(frame.origin.x * UIScreen.main.scale)) / UIScreen.main.scale
|
||||
frame.origin.y = (round(frame.origin.y * UIScreen.main.scale)) / UIScreen.main.scale
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension CGPoint {
|
||||
@@ -80,5 +78,3 @@ func nearestTargetTo(_ point: CGPoint, possibleTargets: [CGPoint]) -> CGPoint {
|
||||
}
|
||||
return nearestEndpoint
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,18 +5,19 @@
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
//#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
var GLOBAL_DEBUG_BORDERS = false
|
||||
var GLOBAL_BORDER_TRACKERS: [BorderManager] = []
|
||||
|
||||
@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.
|
||||
/// Set the font size. The font can be set to a minimum value of 5.0 and a maximum value of 20.0. The default value is 7.5.
|
||||
public var fontSize: CGFloat = 7.5 {
|
||||
didSet {
|
||||
guard fontSize >= 4 else { fontSize = 4; return }
|
||||
@@ -26,55 +27,108 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let consoleSize = CGSize(width: 212, height: 124)
|
||||
var isConsoleConfigured = false
|
||||
|
||||
// Strong reference needed to keep the window alive.
|
||||
/// A high performance text tracker that only updates the view's text if the view is visible. This allows the app to run print to the console with virtually no performance implications when the console isn't visible.
|
||||
var currentText: String = "" {
|
||||
didSet {
|
||||
if isVisible {
|
||||
|
||||
// Ensure we are performing UI updates on the main thread.
|
||||
DispatchQueue.main.async {
|
||||
|
||||
// Ensure the console doesn't get caught into any external animation blocks.
|
||||
UIView.performWithoutAnimation {
|
||||
self.commitTextChanges(requestMenuUpdate: oldValue == "" || (oldValue != "" && self.currentText == ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let defaultConsoleSize = CGSize(width: 228, height: 142)
|
||||
|
||||
/// The fixed size of the console view.
|
||||
lazy var consoleSize = defaultConsoleSize {
|
||||
didSet {
|
||||
consoleView.frame.size = consoleSize
|
||||
|
||||
// Update text view width.
|
||||
if consoleView.frame.size.width > ResizeController.kMaxConsoleWidth {
|
||||
consoleTextView.frame.size.width = ResizeController.kMaxConsoleWidth - 4
|
||||
} else if consoleView.frame.size.width < ResizeController.kMinConsoleWidth {
|
||||
consoleTextView.frame.size.width = ResizeController.kMinConsoleWidth - 4
|
||||
} else {
|
||||
consoleTextView.frame.size.width = consoleSize.width - 4
|
||||
}
|
||||
|
||||
// Update text view height.
|
||||
if consoleView.frame.size.height > ResizeController.kMaxConsoleHeight {
|
||||
consoleTextView.frame.size.height = ResizeController.kMaxConsoleHeight - 4
|
||||
+ (consoleView.frame.size.height - ResizeController.kMaxConsoleHeight) * 2 / 3
|
||||
} else if consoleView.frame.size.height < ResizeController.kMinConsoleHeight {
|
||||
consoleTextView.frame.size.height = ResizeController.kMinConsoleHeight - 4
|
||||
+ (consoleView.frame.size.height - ResizeController.kMinConsoleHeight) * 2 / 3
|
||||
} else {
|
||||
consoleTextView.frame.size.height = consoleSize.height - 4
|
||||
}
|
||||
|
||||
consoleTextView.contentOffset.y = consoleTextView.contentSize.height - consoleTextView.bounds.size.height
|
||||
|
||||
// TODO: Snap to nearest position.
|
||||
|
||||
UserDefaults.standard.set(consoleSize.width, forKey: "LocalConsole_Width")
|
||||
UserDefaults.standard.set(consoleSize.height, forKey: "LocalConsole_Height")
|
||||
}
|
||||
}
|
||||
|
||||
/// Strong reference keeps the window alive.
|
||||
var consoleWindow: ConsoleWindow?
|
||||
|
||||
// The console needs a view controller to display context menus.
|
||||
// The console needs a parent view controller in order to display context menus.
|
||||
let viewController = UIViewController()
|
||||
lazy var consoleView = viewController.view!
|
||||
|
||||
let consoleTextView = UITextView()
|
||||
/// Text view that displays printed items.
|
||||
let consoleTextView = InvertedTextView()
|
||||
|
||||
var menuButton: UIButton!
|
||||
/// Button that reveals menu.
|
||||
lazy var menuButton = UIButton()
|
||||
|
||||
/// Tracks whether the PiP console is in text view scroll mode or pan mode.
|
||||
var scrollLocked = true
|
||||
|
||||
/// Feedback generator for the long press action.
|
||||
let feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
|
||||
lazy var possibleEndpoints = [CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: UIApplication.shared.statusBarHeight + consoleSize.height / 2 + 5),
|
||||
CGPoint(x: UIScreen.size.width - consoleSize.width / 2 - 12,
|
||||
y: UIApplication.shared.statusBarHeight + consoleSize.height / 2 + 5),
|
||||
CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: UIScreen.size.height - consoleSize.height / 2 - 56),
|
||||
CGPoint(x: UIScreen.size.width - consoleSize.width / 2 - 12,
|
||||
y: UIScreen.size.height - consoleSize.height / 2 - 56)]
|
||||
lazy var panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(consolePiPPanner(recognizer:)))
|
||||
lazy var longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(recognizer:)))
|
||||
|
||||
/// Gesture endpoints. Each point represents a corner of the screen. TODO: Handle screen rotation.
|
||||
var possibleEndpoints: [CGPoint] {
|
||||
if consoleSize.width < UIScreen.portraitSize.width - 112 {
|
||||
return [CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: consoleSize.width / 2 + 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
} else {
|
||||
return [CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: (UIScreen.hasRoundedCorners ? 44 : 16) + consoleSize.height / 2 + 12),
|
||||
CGPoint(x: UIScreen.portraitSize.width / 2,
|
||||
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (keyboardHeight ?? consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
|
||||
}
|
||||
}
|
||||
|
||||
lazy var initialViewLocation: CGPoint = .zero
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
func configureConsole() {
|
||||
consoleSize = CGSize(width: UserDefaults.standard.object(forKey: "LocalConsole_Width") as? CGFloat ?? consoleSize.width,
|
||||
height: UserDefaults.standard.object(forKey: "LocalConsole_Height") as? CGFloat ?? consoleSize.height)
|
||||
|
||||
// Configure console window.
|
||||
let windowScene = UIApplication.shared
|
||||
.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.first
|
||||
|
||||
if let windowScene = windowScene as? UIWindowScene {
|
||||
consoleWindow = ConsoleWindow(windowScene: windowScene)
|
||||
consoleWindow?.frame = UIScreen.main.bounds
|
||||
consoleWindow?.windowLevel = UIWindow.Level.statusBar
|
||||
consoleWindow?.isHidden = false
|
||||
consoleWindow?.addSubview(consoleView)
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride
|
||||
}
|
||||
|
||||
// Configure console view.
|
||||
consoleView.frame.size = consoleSize
|
||||
consoleView.backgroundColor = .black
|
||||
|
||||
consoleView.layer.shadowRadius = 16
|
||||
@@ -83,39 +137,41 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.center = possibleEndpoints.first!
|
||||
consoleView.alpha = 0
|
||||
|
||||
consoleView.layer.cornerRadius = 20
|
||||
consoleView.layer.cornerRadius = 22
|
||||
consoleView.layer.cornerCurve = .continuous
|
||||
|
||||
let borderLayer = CALayer()
|
||||
borderLayer.frame = CGRect(x: -1, y: -1,
|
||||
let borderView = UIView()
|
||||
borderView.frame = CGRect(x: -1, y: -1,
|
||||
width: consoleSize.width + 2,
|
||||
height: consoleSize.height + 2)
|
||||
borderLayer.borderWidth = 1
|
||||
borderLayer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
|
||||
borderLayer.cornerRadius = consoleView.layer.cornerRadius + 1
|
||||
borderLayer.cornerCurve = .continuous
|
||||
consoleView.layer.addSublayer(borderLayer)
|
||||
borderView.layer.borderWidth = 1
|
||||
borderView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
|
||||
borderView.layer.cornerRadius = consoleView.layer.cornerRadius + 1
|
||||
borderView.layer.cornerCurve = .continuous
|
||||
borderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
consoleView.addSubview(borderView)
|
||||
|
||||
// Configure text view.
|
||||
consoleTextView.frame = CGRect(x: 0, y: 2, width: consoleSize.width, height: consoleSize.height - 4)
|
||||
consoleTextView.frame = CGRect(x: 2, y: 2, width: consoleSize.width - 4, height: consoleSize.height - 4)
|
||||
consoleTextView.isEditable = false
|
||||
consoleTextView.backgroundColor = .clear
|
||||
consoleTextView.textContainerInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
|
||||
consoleTextView.textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 10, right: 8)
|
||||
|
||||
consoleTextView.isSelectable = false
|
||||
consoleTextView.showsVerticalScrollIndicator = false
|
||||
consoleTextView.contentInsetAdjustmentBehavior = .never
|
||||
consoleView.addSubview(consoleTextView)
|
||||
|
||||
consoleTextView.layer.cornerRadius = consoleView.layer.cornerRadius - 2
|
||||
consoleTextView.layer.cornerCurve = .continuous
|
||||
|
||||
// Configure gesture recognizers.
|
||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(consolePiPPanner(recognizer:)))
|
||||
panRecognizer.maximumNumberOfTouches = 1
|
||||
panRecognizer.delegate = self
|
||||
|
||||
let tapRecognizer = UITapStartEndGestureRecognizer(target: self, action: #selector(consolePiPTapStartEnd(recognizer:)))
|
||||
tapRecognizer.delegate = self
|
||||
|
||||
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(recognizer:)))
|
||||
longPressRecognizer.minimumPressDuration = 0.1
|
||||
|
||||
consoleView.addGestureRecognizer(panRecognizer)
|
||||
@@ -123,13 +179,14 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
consoleView.addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
// Prepare menu button.
|
||||
let diameter = CGFloat(26)
|
||||
let diameter = CGFloat(28)
|
||||
|
||||
// This tuned button frame is used to adjust where the menu appears.
|
||||
menuButton = UIButton(frame: CGRect(x: consoleView.bounds.width - 44,
|
||||
y: consoleView.bounds.height - 36,
|
||||
width: 44,
|
||||
height: 36 + 4 /*Offests the context menu by the desired amount*/))
|
||||
menuButton.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin]
|
||||
|
||||
let circleFrame = CGRect(
|
||||
x: menuButton.bounds.width - diameter - (consoleView.layer.cornerRadius - diameter / 2),
|
||||
@@ -139,9 +196,10 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
let circle = UIView(frame: circleFrame)
|
||||
circle.backgroundColor = UIColor(white: 0.2, alpha: 0.95)
|
||||
circle.layer.cornerRadius = diameter / 2
|
||||
circle.isUserInteractionEnabled = false
|
||||
menuButton.addSubview(circle)
|
||||
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16)))
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17)))
|
||||
ellipsisImage.frame.size = circle.bounds.size
|
||||
ellipsisImage.contentMode = .center
|
||||
circle.addSubview(ellipsisImage)
|
||||
@@ -149,59 +207,156 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
menuButton.tintColor = UIColor(white: 1, alpha: 0.75)
|
||||
menuButton.menu = makeMenu()
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
consoleView.addSubview(menuButton!)
|
||||
consoleView.addSubview(menuButton)
|
||||
|
||||
UIView.swizzleDebugBehaviour
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
}
|
||||
|
||||
/// Adds a LocalConsole window to the app's main scene.
|
||||
func configureWindow() {
|
||||
var windowSceneFound = false
|
||||
|
||||
// Configure console window.
|
||||
func fetchWindowScene() {
|
||||
let windowScene = UIApplication.shared
|
||||
.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.first
|
||||
|
||||
if let windowScene = windowScene as? UIWindowScene {
|
||||
|
||||
windowSceneFound = true
|
||||
|
||||
consoleWindow = ConsoleWindow(windowScene: windowScene)
|
||||
consoleWindow?.frame = UIScreen.main.bounds
|
||||
consoleWindow?.windowLevel = UIWindow.Level.statusBar
|
||||
consoleWindow?.isHidden = false
|
||||
consoleWindow?.addSubview(consoleView)
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride
|
||||
}
|
||||
}
|
||||
|
||||
fetchWindowScene()
|
||||
|
||||
/// Ensures the window is configured (i.e. scene has been found). If not, delay and wait for a scene to prepare itself, then try again.
|
||||
for i in 1...10 {
|
||||
|
||||
let delay = Double(i) / 10
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [self] in
|
||||
|
||||
guard !windowSceneFound else { return }
|
||||
|
||||
fetchWindowScene()
|
||||
|
||||
if isVisible {
|
||||
isVisible = false
|
||||
consoleView.layer.removeAllAnimations()
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
public var isVisible = false {
|
||||
|
||||
didSet {
|
||||
guard oldValue != isVisible else { return }
|
||||
|
||||
if isVisible {
|
||||
|
||||
if !isConsoleConfigured {
|
||||
configureWindow()
|
||||
configureConsole()
|
||||
isConsoleConfigured = true
|
||||
}
|
||||
|
||||
commitTextChanges(requestMenuUpdate: true)
|
||||
|
||||
consoleView.transform = .init(scaleX: 0.9, y: 0.9)
|
||||
UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.6) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
}.startAnimation()
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "shadowOpacity")
|
||||
animation.fromValue = 0
|
||||
animation.toValue = 0.5
|
||||
animation.duration = 0.6
|
||||
consoleView.layer.add(animation, forKey: animation.keyPath)
|
||||
consoleView.layer.shadowOpacity = 0.5
|
||||
|
||||
} else {
|
||||
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) { [self] in
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.9, y: 0.9)
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) { [self] in
|
||||
consoleView.alpha = 0
|
||||
}.startAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _hasRelayedOffsetChange = false
|
||||
|
||||
/// Print items to the console view.
|
||||
public func print(_ items: Any) {
|
||||
|
||||
let string: String = {
|
||||
if consoleTextView.text == "" {
|
||||
return "\(items)"
|
||||
} else {
|
||||
return "\(items)\n" + consoleTextView.text
|
||||
}
|
||||
}()
|
||||
|
||||
setAttributedText(string)
|
||||
if currentText == "" {
|
||||
currentText = "\(items)"
|
||||
} else {
|
||||
currentText = currentText + "\n\(items)"
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear text in the console view.
|
||||
public func clear() {
|
||||
consoleTextView.text = ""
|
||||
currentText = ""
|
||||
}
|
||||
|
||||
/// Copy the console view text to the device's clipboard.
|
||||
public func copy() {
|
||||
UIPasteboard.general.string = consoleTextView.text
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
// MARK: Handle keyboard show/hide.
|
||||
private var keyboardHeight: CGFloat? = nil {
|
||||
didSet {
|
||||
|
||||
if consoleView.center != possibleEndpoints[0] && consoleView.center != possibleEndpoints[1] {
|
||||
let nearestTargetPosition = nearestTargetTo(consoleView.center, possibleTargets: possibleEndpoints.suffix(2))
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.55, dampingRatio: 1) {
|
||||
self.consoleView.center = nearestTargetPosition
|
||||
}.startAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func keyboardWillShow(_ notification: Notification) {
|
||||
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
|
||||
let keyboardRectangle = keyboardFrame.cgRectValue
|
||||
self.keyboardHeight = keyboardRectangle.height
|
||||
}
|
||||
}
|
||||
|
||||
@objc func keyboardWillHide() {
|
||||
keyboardHeight = nil
|
||||
}
|
||||
|
||||
private var debugBordersEnabled = false {
|
||||
didSet {
|
||||
GLOBAL_DEBUG_BORDERS = debugBordersEnabled
|
||||
|
||||
UIView.swizzleDebugBehaviour_UNTRACKABLE_TOGGLE()
|
||||
|
||||
guard debugBordersEnabled else {
|
||||
GLOBAL_BORDER_TRACKERS.forEach {
|
||||
$0.deactivate()
|
||||
@@ -227,29 +382,98 @@ 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
|
||||
|
||||
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 {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
print(
|
||||
"""
|
||||
Model Name: \(SystemReport.shared.gestaltMarketingName)
|
||||
Model Identifier: \(SystemReport.shared.gestaltModelIdentifier)
|
||||
Architecture: \(SystemReport.shared.gestaltArchitecture)
|
||||
Firmware: \(SystemReport.shared.gestaltFirmwareVersion)
|
||||
Kernel Version: \(SystemReport.shared.kernel) \(SystemReport.shared.kernelVersion)
|
||||
System Version: \(SystemReport.shared.versionString)
|
||||
OS Compile Date: \(SystemReport.shared.compileDate)
|
||||
Memory: \(round(100 * Double(ProcessInfo.processInfo.physicalMemory) * pow(10, -9)) / 100) GB
|
||||
Processor Cores: \(Int(ProcessInfo.processInfo.processorCount))
|
||||
Thermal State: \(SystemReport.shared.thermalState)
|
||||
System Uptime: \(ProcessInfo.processInfo.systemUptime.formattedString!)
|
||||
Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func displayReport() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
||||
if currentText != "" { print("\n") }
|
||||
|
||||
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))
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleLock() {
|
||||
scrollLocked.toggle()
|
||||
}
|
||||
|
||||
func toggleVisibility() {
|
||||
if isVisible {
|
||||
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.9, y: 0.9)
|
||||
consoleView.alpha = 0
|
||||
}.startAnimation()
|
||||
func commitTextChanges(requestMenuUpdate menuUpdateRequested: Bool) {
|
||||
|
||||
if consoleTextView.contentOffset.y > consoleTextView.contentSize.height - consoleTextView.bounds.size.height - 20
|
||||
|| _hasRelayedOffsetChange == false {
|
||||
|
||||
isVisible = false
|
||||
} else {
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
consoleView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
isVisible = true
|
||||
consoleTextView.pendingOffsetChange = true
|
||||
_hasRelayedOffsetChange = true
|
||||
}
|
||||
|
||||
// Renders color properly (for dark appearance).
|
||||
consoleView.backgroundColor = .black
|
||||
consoleTextView.text = currentText
|
||||
|
||||
setAttributedText(currentText)
|
||||
|
||||
if menuUpdateRequested {
|
||||
// Update the context menu to show the clipboard/clear actions.
|
||||
menuButton.menu = makeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
func setAttributedText(_ string: String) {
|
||||
@@ -266,17 +490,75 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
func makeMenu() -> UIMenu {
|
||||
|
||||
let copy = UIAction(title: "Copy",
|
||||
image: UIImage(systemName: "doc.on.doc"), handler: { _ in
|
||||
self.copy()
|
||||
})
|
||||
|
||||
let resize = UIAction(title: "Resize Console",
|
||||
image: UIImage(systemName: "arrow.left.and.right.square"), handler: { _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
ResizeController.shared.isActive.toggle()
|
||||
ResizeController.shared.platterView.reveal()
|
||||
}
|
||||
})
|
||||
|
||||
let clear = UIAction(title: "Clear Console",
|
||||
image: UIImage(systemName: "xmark.square"), handler: { _ in
|
||||
self.clear()
|
||||
})
|
||||
|
||||
let consoleActions = UIMenu(title: "", options: .displayInline, children: [clear, resize])
|
||||
|
||||
var frameSymbol = "rectangle.3.offgrid"
|
||||
if #available(iOS 15, *) {
|
||||
frameSymbol = "square.inset.filled"
|
||||
}
|
||||
|
||||
let viewFrames = UIAction(title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
|
||||
image: UIImage(systemName: "rectangle.3.offgrid"), handler: { _ in
|
||||
image: UIImage(systemName: frameSymbol), handler: { _ in
|
||||
self.debugBordersEnabled.toggle()
|
||||
self.menuButton?.menu = self.makeMenu()
|
||||
self.menuButton.menu = self.makeMenu()
|
||||
})
|
||||
|
||||
let respring = UIAction(title: "Restart SpringBoard",
|
||||
let systemReport = UIAction(title: "System Report",
|
||||
image: UIImage(systemName: "cpu"), handler: { _ in
|
||||
self.systemReport()
|
||||
})
|
||||
|
||||
// Show the right glyph for the current device being used.
|
||||
let deviceSymbol: String = {
|
||||
|
||||
let hasHomeButton = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat == 0
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
if hasHomeButton {
|
||||
return "ipad.homebutton"
|
||||
} else {
|
||||
return "ipad"
|
||||
}
|
||||
} else if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
if hasHomeButton {
|
||||
return "iphone.homebutton"
|
||||
} else {
|
||||
return "iphone"
|
||||
}
|
||||
} else {
|
||||
return "rectangle"
|
||||
}
|
||||
}()
|
||||
|
||||
let displayReport = UIAction(title: "Display Report",
|
||||
image: UIImage(systemName: deviceSymbol), handler: { _ in
|
||||
self.displayReport()
|
||||
})
|
||||
|
||||
let respring = UIAction(title: "Restart Spring" + "Board",
|
||||
image: UIImage(systemName: "apps.iphone"), handler: { _ in
|
||||
guard let window = UIApplication.shared.windows.first else { return }
|
||||
|
||||
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displayCornerRadius") as! CGFloat
|
||||
window.layer.cornerRadius = UIScreen.main.value(forKey: "_displ" + "ayCorn" + "erRa" + "dius") as! CGFloat
|
||||
window.layer.masksToBounds = true
|
||||
|
||||
let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
||||
@@ -290,8 +572,20 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
animator.startAnimation()
|
||||
})
|
||||
let debugActions = UIMenu(title: "", options: .displayInline,
|
||||
children: [UIMenu(title: "Debug", image: UIImage(systemName: "ant"),
|
||||
children: [viewFrames, systemReport, displayReport, respring])])
|
||||
|
||||
return UIMenu(title: "", children: [viewFrames, respring])
|
||||
var menuContent: [UIMenuElement] = []
|
||||
|
||||
if consoleTextView.text != "" {
|
||||
menuContent.append(contentsOf: [copy, consoleActions])
|
||||
} else {
|
||||
menuContent.append(resize)
|
||||
}
|
||||
menuContent.append(debugActions)
|
||||
|
||||
return UIMenu(title: "", children: menuContent)
|
||||
}
|
||||
|
||||
@objc func longPressAction(recognizer: UILongPressGestureRecognizer) {
|
||||
@@ -423,9 +717,10 @@ class ConsoleWindow: UIWindow {
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
|
||||
let hitView = super.hitTest(point, with: event)!
|
||||
|
||||
return hitView.isKind(of: ConsoleWindow.self) ? nil : hitView
|
||||
if let hitView = super.hitTest(point, with: event) {
|
||||
return hitView.isKind(of: ConsoleWindow.self) ? nil : hitView
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,20 +742,18 @@ public class UITapStartEndGestureRecognizer: UITapGestureRecognizer {
|
||||
// MARK: Fun hacks!
|
||||
extension UIView {
|
||||
/// Swizzle UIView to use custom frame system when needed.
|
||||
static let swizzleDebugBehaviour: Void = {
|
||||
static func swizzleDebugBehaviour_UNTRACKABLE_TOGGLE() {
|
||||
guard let originalMethod = class_getInstanceMethod(UIView.self, #selector(layoutSubviews)),
|
||||
let swizzledMethod = class_getInstanceMethod(UIView.self, #selector(swizzled_layoutSubviews)) else { return }
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}()
|
||||
}
|
||||
|
||||
@objc func swizzled_layoutSubviews() {
|
||||
swizzled_layoutSubviews()
|
||||
|
||||
if GLOBAL_DEBUG_BORDERS {
|
||||
let tracker = BorderManager(view: self)
|
||||
GLOBAL_BORDER_TRACKERS.append(tracker)
|
||||
tracker.activate()
|
||||
}
|
||||
let tracker = BorderManager(view: self)
|
||||
GLOBAL_BORDER_TRACKERS.append(tracker)
|
||||
tracker.activate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,7 +761,7 @@ extension UIWindow {
|
||||
|
||||
/// Make sure this window does not have control over the status bar appearance.
|
||||
static let swizzleStatusBarAppearanceOverride: Void = {
|
||||
guard let originalMethod = class_getInstanceMethod(UIWindow.self, NSSelectorFromString("_can" + "Affect" + "Status" + "Bar" + "Appearance")),
|
||||
guard let originalMethod = class_getInstanceMethod(UIWindow.self, NSSelectorFromString("_can" + "Affect" + "Sta" + "tus" + "Bar" + "Appe" + "arance")),
|
||||
let swizzledMethod = class_getInstanceMethod(UIWindow.self, #selector(swizzled_statusBarAppearance))
|
||||
else { return }
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
@@ -479,4 +772,46 @@ extension UIWindow {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
//#endif
|
||||
|
||||
class InvertedTextView: UITextView {
|
||||
|
||||
var pendingOffsetChange = false
|
||||
|
||||
// Thanks to WWDC21 Lab!
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if panGestureRecognizer.numberOfTouches == 0 && pendingOffsetChange {
|
||||
contentOffset.y = contentSize.height - bounds.size.height
|
||||
} else {
|
||||
pendingOffsetChange = false
|
||||
}
|
||||
}
|
||||
|
||||
var cancelNextContentSizeDidSet = false
|
||||
|
||||
override var contentSize: CGSize {
|
||||
didSet {
|
||||
cancelNextContentSizeDidSet = true
|
||||
|
||||
if contentSize.height < bounds.size.height {
|
||||
contentInset.top = bounds.size.height - contentSize.height
|
||||
} else {
|
||||
contentInset.top = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,623 @@
|
||||
//
|
||||
// ResizeController.swift
|
||||
//
|
||||
// Created by Duraid Abdul.
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class ResizeController {
|
||||
|
||||
public static let shared = ResizeController()
|
||||
|
||||
lazy var platterView = PlatterView(frame: .zero)
|
||||
|
||||
lazy var consoleCenterPoint = CGPoint(x: (UIScreen.main.nativeBounds.width / 2).rounded() / UIScreen.main.scale,
|
||||
y: (UIScreen.main.nativeBounds.height / 2).rounded() / UIScreen.main.scale
|
||||
+ (UIScreen.hasRoundedCorners ? 0 : 24))
|
||||
|
||||
lazy var consoleOutlineView: UIView = {
|
||||
|
||||
let consoleViewReference = LCManager.shared.consoleView
|
||||
|
||||
let view = UIView()
|
||||
view.layer.borderWidth = 2
|
||||
view.layer.borderColor = UIColor.systemGreen.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light)).cgColor
|
||||
view.layer.cornerRadius = consoleViewReference.layer.cornerRadius + 6
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.alpha = 0
|
||||
|
||||
consoleViewReference.addSubview(view)
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
view.leadingAnchor.constraint(equalTo: consoleViewReference.leadingAnchor, constant: -6),
|
||||
view.trailingAnchor.constraint(equalTo: consoleViewReference.trailingAnchor, constant: 6),
|
||||
view.topAnchor.constraint(equalTo: consoleViewReference.topAnchor, constant: -6),
|
||||
view.bottomAnchor.constraint(equalTo: consoleViewReference.bottomAnchor, constant: 6)
|
||||
])
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var bottomGrabberPillView = UIView()
|
||||
|
||||
lazy var bottomGrabber: UIView = {
|
||||
let view = UIView()
|
||||
LCManager.shared.consoleWindow?.addSubview(view)
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 116),
|
||||
view.heightAnchor.constraint(equalToConstant: 46),
|
||||
view.centerXAnchor.constraint(equalTo: consoleOutlineView.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: consoleOutlineView.bottomAnchor, constant: -18)
|
||||
])
|
||||
|
||||
bottomGrabberPillView.frame = CGRect(x: 58 - 18, y: 25, width: 36, height: 5)
|
||||
bottomGrabberPillView.backgroundColor = UIColor.label
|
||||
bottomGrabberPillView.alpha = 0.3
|
||||
bottomGrabberPillView.layer.cornerRadius = 2.5
|
||||
bottomGrabberPillView.layer.cornerCurve = .continuous
|
||||
view.addSubview(bottomGrabberPillView)
|
||||
|
||||
let verticalPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(verticalPanner(recognizer:)))
|
||||
verticalPanGestureRecognizer.maximumNumberOfTouches = 1
|
||||
view.addGestureRecognizer(verticalPanGestureRecognizer)
|
||||
|
||||
view.alpha = 0
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var rightGrabberPillView = UIView()
|
||||
|
||||
lazy var rightGrabber: UIView = {
|
||||
let view = UIView()
|
||||
LCManager.shared.consoleWindow?.addSubview(view)
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 46),
|
||||
view.heightAnchor.constraint(equalToConstant: 116),
|
||||
view.centerYAnchor.constraint(equalTo: consoleOutlineView.centerYAnchor),
|
||||
view.leftAnchor.constraint(equalTo: consoleOutlineView.rightAnchor, constant: -18)
|
||||
])
|
||||
|
||||
rightGrabberPillView.frame = CGRect(x: 25, y: 58 - 18, width: 5, height: 36)
|
||||
rightGrabberPillView.backgroundColor = UIColor.label
|
||||
rightGrabberPillView.alpha = 0.3
|
||||
rightGrabberPillView.layer.cornerRadius = 2.5
|
||||
rightGrabberPillView.layer.cornerCurve = .continuous
|
||||
view.addSubview(rightGrabberPillView)
|
||||
|
||||
let horizontalPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(horizontalPanner(recognizer:)))
|
||||
horizontalPanGestureRecognizer.maximumNumberOfTouches = 1
|
||||
view.addGestureRecognizer(horizontalPanGestureRecognizer)
|
||||
|
||||
view.alpha = 0
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
var isActive: Bool = false {
|
||||
didSet {
|
||||
guard isActive != oldValue else { return }
|
||||
|
||||
// Initialize views outside of animation.
|
||||
_ = platterView
|
||||
_ = consoleOutlineView
|
||||
_ = bottomGrabber
|
||||
_ = rightGrabber
|
||||
|
||||
// Ensure initial autolayout is performed unanimated.
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
|
||||
if isActive {
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) {
|
||||
|
||||
let textView = LCManager.shared.consoleTextView
|
||||
|
||||
textView.contentOffset.y = textView.contentSize.height - textView.bounds.size.height
|
||||
}.startAnimation()
|
||||
|
||||
|
||||
if LCManager.shared.consoleView.traitCollection.userInterfaceStyle == .light {
|
||||
LCManager.shared.consoleView.layer.shadowOpacity = 0.25
|
||||
}
|
||||
|
||||
// Ensure background color animates in right the first time.
|
||||
LCManager.shared.consoleWindow?.backgroundColor = .clear
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
LCManager.shared.consoleView.center = self.consoleCenterPoint
|
||||
|
||||
// Update grabbers (layout constraints)
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
|
||||
LCManager.shared.menuButton.alpha = 0
|
||||
|
||||
LCManager.shared.consoleWindow?.backgroundColor = UIColor(dynamicProvider: { traitCollection in
|
||||
UIColor(white: 0, alpha: traitCollection.userInterfaceStyle == .light ? 0.1 : 0.3)
|
||||
})
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleOutlineView.alpha = 1
|
||||
}.startAnimation(afterDelay: 0.3)
|
||||
|
||||
bottomGrabber.transform = .init(translationX: 0, y: -5)
|
||||
rightGrabber.transform = .init(translationX: -5, y: 0)
|
||||
|
||||
UIViewPropertyAnimator(duration: 1, dampingRatio: 1) { [self] in
|
||||
bottomGrabber.alpha = 1
|
||||
rightGrabber.alpha = 1
|
||||
|
||||
bottomGrabber.transform = .identity
|
||||
rightGrabber.transform = .identity
|
||||
}.startAnimation(afterDelay: 0.3)
|
||||
|
||||
LCManager.shared.panRecognizer.isEnabled = false
|
||||
LCManager.shared.longPressRecognizer.isEnabled = false
|
||||
|
||||
// Activate full screen button.
|
||||
consoleOutlineView.isUserInteractionEnabled = true
|
||||
} else {
|
||||
|
||||
LCManager.shared.consoleView.layer.shadowOpacity = 0.5
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
LCManager.shared.consoleView.center = LCManager.shared.possibleEndpoints.first!
|
||||
|
||||
// Update grabbers (layout constraints)
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
|
||||
LCManager.shared.menuButton.alpha = 1
|
||||
|
||||
LCManager.shared.consoleWindow?.backgroundColor = .clear
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.2, dampingRatio: 1) { [self] in
|
||||
consoleOutlineView.alpha = 0
|
||||
|
||||
bottomGrabber.alpha = 0
|
||||
rightGrabber.alpha = 0
|
||||
}.startAnimation()
|
||||
|
||||
LCManager.shared.panRecognizer.isEnabled = true
|
||||
LCManager.shared.longPressRecognizer.isEnabled = true
|
||||
|
||||
// Deactivate full screen button.
|
||||
consoleOutlineView.isUserInteractionEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var initialHeight = CGFloat.zero
|
||||
|
||||
static let kMinConsoleHeight: CGFloat = 108
|
||||
static let kMaxConsoleHeight: CGFloat = 346
|
||||
|
||||
@objc func verticalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: bottomGrabber.superview)
|
||||
|
||||
let minHeight = Self.kMinConsoleHeight
|
||||
let maxHeight = Self.kMaxConsoleHeight
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
initialHeight = LCManager.shared.consoleSize.height
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
bottomGrabberPillView.alpha = 0.6
|
||||
}.startAnimation()
|
||||
|
||||
case .changed:
|
||||
|
||||
let resolvedHeight: CGFloat = {
|
||||
let initialEstimate = initialHeight + 2 * translation.y
|
||||
if initialEstimate <= maxHeight && initialEstimate > minHeight {
|
||||
return initialEstimate
|
||||
} else if initialEstimate > maxHeight {
|
||||
|
||||
var excess = initialEstimate - maxHeight
|
||||
excess = 25 * log(1/25 * excess + 1)
|
||||
|
||||
return maxHeight + excess
|
||||
} else {
|
||||
var excess = minHeight - initialEstimate
|
||||
excess = 7 * log(1/7 * excess + 1)
|
||||
|
||||
return minHeight - excess
|
||||
}
|
||||
}()
|
||||
|
||||
LCManager.shared.consoleSize.height = resolvedHeight
|
||||
LCManager.shared.consoleView.center.y = consoleCenterPoint.y
|
||||
|
||||
case .ended, .cancelled:
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
|
||||
if LCManager.shared.consoleSize.height > maxHeight {
|
||||
LCManager.shared.consoleSize.height = maxHeight
|
||||
}
|
||||
if LCManager.shared.consoleSize.height < minHeight {
|
||||
LCManager.shared.consoleSize.height = minHeight
|
||||
}
|
||||
|
||||
LCManager.shared.consoleView.center.y = self.consoleCenterPoint.y
|
||||
|
||||
// Animate autolayout updates.
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
bottomGrabberPillView.alpha = 0.3
|
||||
}.startAnimation()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
var initialWidth = CGFloat.zero
|
||||
|
||||
static let kMinConsoleWidth: CGFloat = 112
|
||||
static let kMaxConsoleWidth: CGFloat = UIScreen.portraitSize.width - 56
|
||||
|
||||
@objc func horizontalPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: bottomGrabber.superview)
|
||||
|
||||
let minWidth = Self.kMinConsoleWidth
|
||||
let maxWidth = Self.kMaxConsoleWidth
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
initialWidth = LCManager.shared.consoleSize.width
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
rightGrabberPillView.alpha = 0.6
|
||||
}.startAnimation()
|
||||
|
||||
case .changed:
|
||||
|
||||
let resolvedWidth: CGFloat = {
|
||||
let initialEstimate = initialWidth + 2 * translation.x
|
||||
if initialEstimate <= maxWidth && initialEstimate > minWidth {
|
||||
return initialEstimate
|
||||
} else if initialEstimate > maxWidth {
|
||||
|
||||
var excess = initialEstimate - maxWidth
|
||||
excess = 25 * log(1/25 * excess + 1)
|
||||
|
||||
return maxWidth + excess
|
||||
} else {
|
||||
var excess = minWidth - initialEstimate
|
||||
excess = 7 * log(1/7 * excess + 1)
|
||||
|
||||
return minWidth - excess
|
||||
}
|
||||
}()
|
||||
|
||||
LCManager.shared.consoleSize.width = resolvedWidth
|
||||
LCManager.shared.consoleView.center.x = (UIScreen.main.nativeBounds.width * 1/2).rounded() / UIScreen.main.scale
|
||||
|
||||
case .ended, .cancelled:
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 0.7) {
|
||||
if LCManager.shared.consoleSize.width > maxWidth {
|
||||
LCManager.shared.consoleSize.width = maxWidth
|
||||
}
|
||||
if LCManager.shared.consoleSize.width < minWidth {
|
||||
LCManager.shared.consoleSize.width = minWidth
|
||||
}
|
||||
|
||||
LCManager.shared.consoleView.center.x = (UIScreen.main.nativeBounds.width * 1/2).rounded() / UIScreen.main.scale
|
||||
|
||||
// Animate autolayout updates.
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
rightGrabberPillView.alpha = 0.3
|
||||
}.startAnimation()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
class PlatterView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.frame.size = UIScreen.portraitSize
|
||||
// Make sure bottom doesn't show on upwards pan.
|
||||
self.frame.size.height += 50
|
||||
self.frame.origin = possibleEndpoints[1]
|
||||
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
layer.shadowRadius = 10
|
||||
layer.shadowOpacity = 0.125
|
||||
layer.shadowOffset = CGSize(width: 0, height: 0)
|
||||
|
||||
layer.borderColor = dynamicBorderColor.cgColor
|
||||
layer.borderWidth = 1 / UIScreen.main.scale
|
||||
layer.cornerRadius = 30
|
||||
layer.cornerCurve = .continuous
|
||||
|
||||
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
|
||||
|
||||
blurView.layer.cornerRadius = 30
|
||||
blurView.layer.cornerCurve = .continuous
|
||||
blurView.clipsToBounds = true
|
||||
|
||||
blurView.frame = bounds
|
||||
|
||||
addSubview(blurView)
|
||||
|
||||
LCManager.shared.consoleWindow?.addSubview(self)
|
||||
LCManager.shared.consoleWindow?.sendSubviewToBack(self)
|
||||
|
||||
_ = backgroundButton
|
||||
|
||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(platterPanner(recognizer:)))
|
||||
panRecognizer.maximumNumberOfTouches = 1
|
||||
addGestureRecognizer(panRecognizer)
|
||||
|
||||
let grabber = UIView()
|
||||
grabber.frame.size = CGSize(width: 36, height: 5)
|
||||
grabber.frame.origin.y = 10
|
||||
grabber.center.x = bounds.width / 2
|
||||
grabber.backgroundColor = .label
|
||||
grabber.alpha = 0.1
|
||||
grabber.layer.cornerRadius = 2.5
|
||||
grabber.layer.cornerCurve = .continuous
|
||||
addSubview(grabber)
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "Resize Console"
|
||||
titleLabel.font = .systemFont(ofSize: 30, weight: .bold)
|
||||
titleLabel.sizeToFit()
|
||||
titleLabel.center.x = bounds.width / 2
|
||||
titleLabel.frame.origin.y = 28
|
||||
titleLabel.roundOriginToPixel()
|
||||
addSubview(titleLabel)
|
||||
|
||||
let subtitleLabel = UILabel()
|
||||
subtitleLabel.text = "Use the grabbers to resize the console."
|
||||
subtitleLabel.font = .systemFont(ofSize: 17, weight: .medium)
|
||||
subtitleLabel.sizeToFit()
|
||||
subtitleLabel.alpha = 0.5
|
||||
subtitleLabel.center.x = bounds.width / 2
|
||||
subtitleLabel.frame.origin.y = titleLabel.frame.maxY + 8
|
||||
subtitleLabel.roundOriginToPixel()
|
||||
addSubview(subtitleLabel)
|
||||
|
||||
addSubview(resetButton)
|
||||
resetButton.center = CGPoint(x: UIScreen.portraitSize.width / 2 - 74,
|
||||
y: UIScreen.portraitSize.height - possibleEndpoints[0].y * 2)
|
||||
resetButton.roundOriginToPixel()
|
||||
|
||||
addSubview(doneButton)
|
||||
doneButton.center = CGPoint(x: UIScreen.portraitSize.width / 2 + 74,
|
||||
y: UIScreen.portraitSize.height - possibleEndpoints[0].y * 2)
|
||||
doneButton.roundOriginToPixel()
|
||||
}
|
||||
|
||||
lazy var backgroundButton: UIButton = {
|
||||
let backgroundButton = UIButton(primaryAction: UIAction(handler: { _ in
|
||||
ResizeController.shared.isActive = false
|
||||
self.dismiss()
|
||||
}))
|
||||
backgroundButton.frame.size = CGSize(width: self.frame.size.width, height: possibleEndpoints[0].y + 30)
|
||||
LCManager.shared.consoleWindow?.addSubview(backgroundButton)
|
||||
LCManager.shared.consoleWindow?.sendSubviewToBack(backgroundButton)
|
||||
return backgroundButton
|
||||
}()
|
||||
|
||||
lazy var doneButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = UIColor.systemBlue.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
|
||||
button.setTitle("Done", for: .normal)
|
||||
button.setTitleColor(.white, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
|
||||
button.frame.size = CGSize(width: 116, height: 52)
|
||||
button.layer.cornerRadius = 20
|
||||
button.layer.cornerCurve = .continuous
|
||||
|
||||
button.addAction(UIAction(handler: { _ in
|
||||
ResizeController.shared.isActive = false
|
||||
self.dismiss()
|
||||
}), for: .touchUpInside)
|
||||
|
||||
button.addActions(highlightAction: UIAction(handler: { _ in
|
||||
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) {
|
||||
button.alpha = 0.6
|
||||
}.startAnimation()
|
||||
}), unhighlightAction: UIAction(handler: { _ in
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
|
||||
button.alpha = 1
|
||||
}.startAnimation()
|
||||
}))
|
||||
|
||||
return button
|
||||
}()
|
||||
|
||||
lazy var resetButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.backgroundColor = UIColor(dynamicProvider: { traitCollection in
|
||||
if traitCollection.userInterfaceStyle == .dark {
|
||||
return UIColor(white: 1, alpha: 0.125)
|
||||
} else {
|
||||
return UIColor(white: 0, alpha: 0.1)
|
||||
}
|
||||
})
|
||||
|
||||
button.setTitle("Reset", for: .normal)
|
||||
button.setTitleColor(.label, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .medium)
|
||||
button.frame.size = CGSize(width: 116, height: 52)
|
||||
button.layer.cornerRadius = 20
|
||||
button.layer.cornerCurve = .continuous
|
||||
|
||||
button.addAction(UIAction(handler: { _ in
|
||||
|
||||
// Resolves a text view frame animation bug that occurs when *decreasing* text view width.
|
||||
if LCManager.shared.consoleSize.width > LCManager.shared.defaultConsoleSize.width {
|
||||
LCManager.shared.consoleTextView.frame.size.width = LCManager.shared.defaultConsoleSize.width - 4
|
||||
}
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
|
||||
LCManager.shared.consoleSize = LCManager.shared.defaultConsoleSize
|
||||
LCManager.shared.consoleView.center = ResizeController.shared.consoleCenterPoint
|
||||
LCManager.shared.consoleWindow?.layoutIfNeeded()
|
||||
}.startAnimation()
|
||||
|
||||
}), for: .touchUpInside)
|
||||
|
||||
button.addActions(highlightAction: UIAction(handler: { _ in
|
||||
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) {
|
||||
button.alpha = 0.6
|
||||
}.startAnimation()
|
||||
}), unhighlightAction: UIAction(handler: { _ in
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
|
||||
button.alpha = 1
|
||||
}.startAnimation()
|
||||
}))
|
||||
|
||||
return button
|
||||
}()
|
||||
|
||||
func reveal() {
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
self.frame.origin = self.possibleEndpoints[0]
|
||||
}.startAnimation()
|
||||
|
||||
backgroundButton.isHidden = false
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
self.frame.origin = self.possibleEndpoints[1]
|
||||
}.startAnimation()
|
||||
|
||||
backgroundButton.isHidden = true
|
||||
}
|
||||
|
||||
let dynamicBorderColor = UIColor(dynamicProvider: { traitCollection in
|
||||
if traitCollection.userInterfaceStyle == .dark {
|
||||
return UIColor(white: 1, alpha: 0.075)
|
||||
} else {
|
||||
return UIColor(white: 0, alpha: 0.125)
|
||||
}
|
||||
})
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
layer.borderColor = dynamicBorderColor.cgColor
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
lazy var possibleEndpoints = [CGPoint(x: 0, y: (UIScreen.hasRoundedCorners ? 44 : -8) + 63), CGPoint(x: 0, y: UIScreen.portraitSize.height + 5)]
|
||||
|
||||
var initialPlatterOriginY = CGFloat.zero
|
||||
|
||||
@objc func platterPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
let translation = recognizer.translation(in: superview)
|
||||
let velocity = recognizer.velocity(in: superview)
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
initialPlatterOriginY = frame.origin.y
|
||||
case .changed:
|
||||
|
||||
let resolvedOriginY: CGFloat = {
|
||||
let initialEstimate = initialPlatterOriginY + translation.y
|
||||
if initialEstimate >= possibleEndpoints[0].y {
|
||||
|
||||
// Stick buttons to bottom.
|
||||
[doneButton, resetButton,
|
||||
ResizeController.shared.bottomGrabber, ResizeController.shared.rightGrabber,
|
||||
LCManager.shared.consoleView
|
||||
].forEach {
|
||||
$0.transform = .identity
|
||||
}
|
||||
|
||||
return initialEstimate
|
||||
} else {
|
||||
var excess = possibleEndpoints[0].y - initialEstimate
|
||||
excess = 10 * log(1/10 * excess + 1)
|
||||
|
||||
// Stick buttons to bottom.
|
||||
doneButton.transform = .init(translationX: 0, y: excess)
|
||||
resetButton.transform = .init(translationX: 0, y: excess)
|
||||
|
||||
ResizeController.shared.bottomGrabber.transform = .init(translationX: 0, y: -excess / 2.5)
|
||||
ResizeController.shared.rightGrabber.transform = .init(translationX: 0, y: -excess / 2)
|
||||
LCManager.shared.consoleView.transform = .init(translationX: 0, y: -excess / 2)
|
||||
|
||||
return possibleEndpoints[0].y - excess
|
||||
}
|
||||
}()
|
||||
|
||||
if frame.origin.y > possibleEndpoints[0].y + 40 {
|
||||
ResizeController.shared.isActive = false
|
||||
} else {
|
||||
ResizeController.shared.isActive = true
|
||||
}
|
||||
|
||||
frame.origin.y = resolvedOriginY
|
||||
|
||||
case .ended, .cancelled:
|
||||
|
||||
// After the PiP is thrown, determine the best corner and re-target it there.
|
||||
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
|
||||
|
||||
let projectedPosition = CGPoint(
|
||||
x: 0,
|
||||
y: frame.origin.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
|
||||
)
|
||||
|
||||
let nearestTargetPosition = nearestTargetTo(projectedPosition, possibleTargets: possibleEndpoints)
|
||||
|
||||
let relativeInitialVelocity = CGVector(
|
||||
dx: 0,
|
||||
dy: frame.origin.y >= possibleEndpoints[0].y
|
||||
? relativeVelocity(forVelocity: velocity.y, from: frame.origin.y, to: nearestTargetPosition.y)
|
||||
: 0
|
||||
)
|
||||
|
||||
let timingParameters = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)
|
||||
let positionAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
|
||||
positionAnimator.addAnimations { [self] in
|
||||
frame.origin = nearestTargetPosition
|
||||
|
||||
[doneButton, resetButton,
|
||||
ResizeController.shared.bottomGrabber, ResizeController.shared.rightGrabber,
|
||||
LCManager.shared.consoleView
|
||||
].forEach {
|
||||
$0.transform = .identity
|
||||
}
|
||||
}
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
if nearestTargetPosition == possibleEndpoints[1] {
|
||||
ResizeController.shared.isActive = false
|
||||
backgroundButton.isHidden = true
|
||||
} else {
|
||||
ResizeController.shared.isActive = true
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// SystemReport.swift
|
||||
// LocalConsole
|
||||
//
|
||||
// Created by Duraid Abdul on 2021-06-01.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MachO
|
||||
|
||||
class SystemReport {
|
||||
static let shared = SystemReport()
|
||||
|
||||
var versionString: String {
|
||||
ProcessInfo.processInfo.operatingSystemVersionString
|
||||
.replacingOccurrences(of: "Build ", with: "")
|
||||
.replacingOccurrences(of: "Version ", with: "")
|
||||
}
|
||||
|
||||
// Current device thermal state.
|
||||
var thermalState: String {
|
||||
let state = ProcessInfo.processInfo.thermalState
|
||||
|
||||
switch state {
|
||||
case .nominal: return "Nominal"
|
||||
case .fair : return "Fair"
|
||||
case .serious : return "Serious"
|
||||
case .critical : return "Critical"
|
||||
default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve device mobile gestalt cache.
|
||||
lazy var gestaltCacheExtra: NSDictionary? = {
|
||||
let url = URL(fileURLWithPath: "/pri" + "vate/va" + "r/containe" + "rs/Shared/Sys" + "temGroup/sys" + "temgroup.com.apple.mobilegestal" + "tcache/Libr" + "ary/Ca" + "ches/com.app" + "le.MobileGes" + "talt.plist")
|
||||
|
||||
let dictionary = NSDictionary(contentsOf: url)
|
||||
return dictionary?.value(forKey: "CacheE" + "xtra") as? NSDictionary
|
||||
}()
|
||||
|
||||
// Device marketing name.
|
||||
lazy var gestaltMarketingName: Any = gestaltCacheExtra?.value(forKey: "Z/dqyWS6OZ" + "TRy10UcmUAhw") ?? "Unknown"
|
||||
|
||||
// iBoot (second-stage loader) version.
|
||||
lazy var gestaltFirmwareVersion: Any = gestaltCacheExtra?.value(forKey: "LeSRsiLoJC" + "Mhjn6nd6GWbQ") ?? "Unknown"
|
||||
|
||||
// CPU architecture.
|
||||
lazy var gestaltArchitecture: Any = gestaltCacheExtra?.value(forKey: "k7QIBwZJJO" + "Vw+Sej/8h8VA") ?? deviceArchitecture
|
||||
|
||||
// Fallback in case gestaltArchitecture doesn't return a value.
|
||||
var deviceArchitecture: String {
|
||||
let info = NXGetLocalArchInfo()
|
||||
return String(utf8String: (info?.pointee.description)!) ?? "Unknown"
|
||||
}
|
||||
|
||||
lazy var gestaltModelIdentifier: Any = gestaltCacheExtra?.value(forKey: "h9jDsbgj7xI" + "VeIQ8S3/X3Q") ?? modelIdentifier
|
||||
|
||||
// Fallback in case gestaltModelIdentifier doesn't return a value.
|
||||
var modelIdentifier: String {
|
||||
if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MO" + "DEL_IDENTIFIER"] { return simulatorModelIdentifier }
|
||||
var sysinfo = utsname()
|
||||
uname(&sysinfo) // ignore return value
|
||||
return String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)?.trimmingCharacters(in: .controlCharacters) ?? "Unknown"
|
||||
}
|
||||
|
||||
var kernel: String {
|
||||
var size = 0
|
||||
sysctlbyname("ker" + "n.os" + "type", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("ker" + "n.os" + "type", &string, &size, nil, 0)
|
||||
return String(cString: string)
|
||||
}
|
||||
|
||||
var kernelVersion: String {
|
||||
var size = 0
|
||||
sysctlbyname("ker" + "n.os" + "release", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("ker" + "n.os" + "release", &string, &size, nil, 0)
|
||||
return String(cString: string)
|
||||
}
|
||||
|
||||
var compileDate: String {
|
||||
var size = 0
|
||||
sysctlbyname("ker" + "n.ve" + "rsion", nil, &size, nil, 0)
|
||||
|
||||
var string = [CChar](repeating: 0, count: Int(size))
|
||||
sysctlbyname("ker" + "n.ve" + "rsion", &string, &size, nil, 0)
|
||||
let fullString = String(cString: string) /// Ex: Darwin Kernel Version 20.6.0: Mon May 10 03:15:29 PDT 2021; root:xnu-7195.140.13.0.1~20/RELEASE_ARM64_T8101
|
||||
|
||||
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue)
|
||||
if let matches = detector?.matches(in: fullString, options: [], range: NSRange(location: 0, length: fullString.utf16.count)) {
|
||||
for match in matches {
|
||||
|
||||
if let date = match.date {
|
||||
|
||||
let dateformatter = DateFormatter()
|
||||
dateformatter.dateStyle = .medium
|
||||
|
||||
return dateformatter.string(from: date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user