Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeece2fda8 | |||
| 01ee69b8c5 | |||
| 72d9b1fbd5 | |||
| 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 | |||
| 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 |
@@ -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,34 +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 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
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,681 @@
|
||||
//
|
||||
// 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)
|
||||
|
||||
var consoleCenterPoint: CGPoint {
|
||||
let containerViewSize = LCManager.shared.viewController.view.frame.size
|
||||
|
||||
return CGPoint(x: (containerViewSize.width * UIScreen.main.scale / 2).rounded() / UIScreen.main.scale,
|
||||
y: (containerViewSize.height * UIScreen.main.scale / 2).rounded() / UIScreen.main.scale
|
||||
+ (UIScreen.hasRoundedCorners ? 0 : 24))
|
||||
}
|
||||
|
||||
lazy var consoleOutlineView: UIView = {
|
||||
|
||||
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()
|
||||
|
||||
FrameRateRequest().perform(duration: 1.5)
|
||||
|
||||
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.snapToCachedEndpoint()
|
||||
|
||||
// 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
|
||||
|
||||
let verticalPanner_frameRateRequest = FrameRateRequest()
|
||||
|
||||
@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:
|
||||
verticalPanner_frameRateRequest.isActive = true
|
||||
|
||||
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.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
|
||||
|
||||
// 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, UIScreen.portraitSize.height].min()! - 56
|
||||
|
||||
let horizontalPanner_frameRateRequest = FrameRateRequest()
|
||||
|
||||
@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:
|
||||
horizontalPanner_frameRateRequest.isActive = true
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
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
|
||||
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
addSubview(blurView)
|
||||
|
||||
LCManager.shared.viewController.view.addSubview(self)
|
||||
LCManager.shared.viewController.view.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.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
||||
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.frame.origin.y = 28
|
||||
titleLabel.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
||||
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.frame.origin.y = titleLabel.frame.maxY + 8
|
||||
subtitleLabel.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
||||
addSubview(subtitleLabel)
|
||||
|
||||
let buttonContainerView = UIView()
|
||||
buttonContainerView.addSubview(resetButton)
|
||||
buttonContainerView.addSubview(doneButton)
|
||||
addSubview(buttonContainerView)
|
||||
|
||||
buttonContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
resetButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
doneButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
|
||||
buttonContainerView.widthAnchor.constraint(equalToConstant: 264),
|
||||
buttonContainerView.heightAnchor.constraint(equalToConstant: 52),
|
||||
buttonContainerView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
buttonContainerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -possibleEndpoints[0].y * 2),
|
||||
|
||||
resetButton.widthAnchor.constraint(equalToConstant: 116),
|
||||
resetButton.heightAnchor.constraint(equalToConstant: 52),
|
||||
resetButton.leadingAnchor.constraint(equalTo: buttonContainerView.leadingAnchor),
|
||||
resetButton.topAnchor.constraint(equalTo: buttonContainerView.topAnchor),
|
||||
|
||||
doneButton.widthAnchor.constraint(equalToConstant: 116),
|
||||
doneButton.heightAnchor.constraint(equalToConstant: 52),
|
||||
doneButton.trailingAnchor.constraint(equalTo: buttonContainerView.trailingAnchor),
|
||||
doneButton.topAnchor.constraint(equalTo: buttonContainerView.topAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
lazy var backgroundButton: UIButton = {
|
||||
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.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.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.lumaHeightAnchor.constant = LCManager.shared.defaultConsoleSize.height
|
||||
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 configureFrame() {
|
||||
self.frame.size = LCManager.shared.viewController.view.frame.size
|
||||
// Make sure bottom doesn't show on upwards pan.
|
||||
self.frame.size.height += 50
|
||||
self.frame.origin = possibleEndpoints[1]
|
||||
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
|
||||
func reveal() {
|
||||
|
||||
configureFrame()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
self.frame.origin = self.possibleEndpoints[0]
|
||||
}.startAnimation()
|
||||
|
||||
backgroundButton.isHidden = false
|
||||
|
||||
isHidden = false
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 1) {
|
||||
self.frame.origin = self.possibleEndpoints[1]
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
self.isHidden = true
|
||||
}
|
||||
animator.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")
|
||||
}
|
||||
|
||||
var possibleEndpoints: [CGPoint] { return [CGPoint(x: 0, y: (UIScreen.hasRoundedCorners ? 44 : -8) + 63),
|
||||
CGPoint(x: 0, y: LCManager.shared.viewController.view.frame.size.height + 5)]
|
||||
}
|
||||
|
||||
var initialPlatterOriginY = CGFloat.zero
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
if nearestTargetPosition == possibleEndpoints[1] {
|
||||
ResizeController.shared.isActive = false
|
||||
backgroundButton.isHidden = true
|
||||
|
||||
positionAnimator.addCompletion { _ in
|
||||
self.isHidden = true
|
||||
}
|
||||
} else {
|
||||
ResizeController.shared.isActive = true
|
||||
}
|
||||
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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