Adjust naming
This commit is contained in:
+6
-16
@@ -5,24 +5,14 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "LocalConsole",
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "LocalConsole",
|
||||
targets: ["LocalConsole"]),
|
||||
platforms: [
|
||||
.iOS(.v14),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
products: [
|
||||
.library(
|
||||
name: "LocalConsole", targets: ["LocalConsole"]),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "LocalConsole",
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "LocalConsoleTests",
|
||||
dependencies: ["LocalConsole"]),
|
||||
.target(name: "LocalConsole", dependencies: [])
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# LocalConsole
|
||||
# **LocalConsole**
|
||||
|
||||
A description of this package.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// BorderManager.swift
|
||||
//
|
||||
// Created by Duraid Abdul.
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
/// This class handles enabling and disabling debug borders on a specified view.
|
||||
class BorderManager {
|
||||
weak var layer: CALayer?
|
||||
|
||||
// Debug configuration defined.
|
||||
static let outlineWidth = 1 - 1 / UIScreen.main.scale
|
||||
let outlineColor: CGColor
|
||||
|
||||
// Previous configuration cache.
|
||||
var cachedWidth: CGFloat?
|
||||
var cachedColor: CGColor?
|
||||
|
||||
init(view: UIView) {
|
||||
layer = view.layer
|
||||
|
||||
// Different colors for different UIView types.
|
||||
if "\(view.classForCoder)".contains("UIImageView") {
|
||||
outlineColor = UIColor.systemGreen.withAlphaComponent(0.85).cgColor
|
||||
} else if "\(view.classForCoder)".contains("UILabel") {
|
||||
outlineColor = UIColor.systemBlue.withAlphaComponent(0.85).cgColor
|
||||
} else if "\(view.classForCoder)".contains("UIVisualEffectView") {
|
||||
outlineColor = UIColor.systemIndigo.withAlphaComponent(0.85).cgColor
|
||||
} else {
|
||||
outlineColor = UIColor.systemYellow.withAlphaComponent(0.85).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
// Activates debug borders.
|
||||
func activate() {
|
||||
cachedWidth = layer?.borderWidth
|
||||
cachedColor = layer?.borderColor
|
||||
|
||||
layer?.borderWidth = Self.outlineWidth
|
||||
layer?.borderColor = outlineColor
|
||||
}
|
||||
|
||||
// Deactivates debug borders, restoring previous border properties.
|
||||
func deactivate() {
|
||||
|
||||
guard let cachedWidth = cachedWidth, let cachedColor = cachedColor else {
|
||||
layer?.borderWidth = 0.0
|
||||
layer?.borderColor = UIColor.clear.cgColor
|
||||
return
|
||||
}
|
||||
|
||||
// If the border width has changed since it was outlined, refrain from reverting it to the previous width.
|
||||
if layer?.borderWidth == Self.outlineWidth {
|
||||
layer?.borderWidth = cachedWidth
|
||||
}
|
||||
// If the border color has changed since it was outlined, refrain from reverting it to the previous color.
|
||||
if layer?.borderColor == outlineColor {
|
||||
layer?.borderColor = cachedColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,449 @@
|
||||
//
|
||||
// LocalConsole.swift
|
||||
//
|
||||
// Created by Duraid Abdul.
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
var GLOBAL_DEBUG_BORDERS = false
|
||||
var GLOBAL_BORDER_TRACKERS: [BorderManager] = []
|
||||
|
||||
public class Console: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
public static let shared = Console()
|
||||
|
||||
let consoleSize = CGSize(width: 208, height: 116)
|
||||
|
||||
// Strong reference needed to keep the window alive.
|
||||
var consoleWindow: ConsoleWindow?
|
||||
|
||||
// The console needs a view controller to display context menus.
|
||||
let viewController = UIViewController()
|
||||
lazy var consoleView = viewController.view!
|
||||
|
||||
let consoleTextView = UITextView()
|
||||
|
||||
var menuButton: UIButton!
|
||||
|
||||
var scrollLocked = true
|
||||
|
||||
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 initialViewLocation: CGPoint = .zero
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// 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.normal
|
||||
consoleWindow?.isHidden = false
|
||||
consoleWindow?.addSubview(consoleView)
|
||||
|
||||
UIWindow.swizzleStatusBarAppearanceOverride
|
||||
}
|
||||
|
||||
// Configure console view.
|
||||
consoleView.frame.size = consoleSize
|
||||
consoleView.backgroundColor = .black
|
||||
|
||||
consoleView.layer.shadowRadius = 16
|
||||
consoleView.layer.shadowOpacity = 0.5
|
||||
consoleView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
consoleView.center = possibleEndpoints.first!
|
||||
consoleView.alpha = 0
|
||||
|
||||
consoleView.layer.borderWidth = 1
|
||||
consoleView.layer.borderColor = UIColor(white: 1, alpha: 0.08).cgColor
|
||||
|
||||
consoleView.layer.cornerRadius = 19
|
||||
consoleView.layer.cornerCurve = .continuous
|
||||
|
||||
// Configure text view.
|
||||
consoleTextView.frame = CGRect(x: 0, y: 2, width: consoleSize.width, height: consoleSize.height - 4)
|
||||
consoleTextView.isEditable = false
|
||||
consoleTextView.backgroundColor = .clear
|
||||
consoleTextView.textContainerInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
|
||||
|
||||
consoleTextView.isSelectable = false
|
||||
consoleTextView.showsVerticalScrollIndicator = false
|
||||
consoleTextView.contentInsetAdjustmentBehavior = .never
|
||||
consoleView.addSubview(consoleTextView)
|
||||
|
||||
// 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)
|
||||
consoleView.addGestureRecognizer(tapRecognizer)
|
||||
consoleView.addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
// Prepare menu button.
|
||||
let diameter = CGFloat(25)
|
||||
|
||||
menuButton = UIButton(frame: CGRect(x: consoleView.bounds.width - diameter - (consoleView.layer.cornerRadius - diameter / 2),
|
||||
y: consoleView.bounds.height - diameter - (consoleView.layer.cornerRadius - diameter / 2),
|
||||
width: diameter, height: diameter))
|
||||
menuButton.layer.cornerRadius = diameter / 2
|
||||
menuButton.backgroundColor = UIColor(white: 1, alpha: 0.20)
|
||||
|
||||
let ellipsisImage = UIImageView(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16)))
|
||||
ellipsisImage.frame.size = menuButton!.bounds.size
|
||||
ellipsisImage.contentMode = .center
|
||||
menuButton.addSubview(ellipsisImage)
|
||||
|
||||
menuButton.tintColor = UIColor(white: 1, alpha: 0.75)
|
||||
menuButton.menu = makeMenu()
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
consoleView.addSubview(menuButton!)
|
||||
|
||||
UIView.swizzleDebugBehaviour
|
||||
|
||||
_print(consoleView)
|
||||
_print(menuButton)
|
||||
_print("Hello, world!")
|
||||
}
|
||||
|
||||
public var isVisible = false {
|
||||
|
||||
didSet {
|
||||
guard oldValue != isVisible else { return }
|
||||
|
||||
if isVisible {
|
||||
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
|
||||
consoleView.alpha = 1
|
||||
}.startAnimation()
|
||||
} else {
|
||||
UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.9, y: 0.9)
|
||||
consoleView.alpha = 0
|
||||
}.startAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var debugBordersEnabled = false {
|
||||
didSet {
|
||||
GLOBAL_DEBUG_BORDERS = debugBordersEnabled
|
||||
guard debugBordersEnabled else {
|
||||
GLOBAL_BORDER_TRACKERS.forEach {
|
||||
$0.deactivate()
|
||||
}
|
||||
GLOBAL_BORDER_TRACKERS = []
|
||||
return
|
||||
}
|
||||
|
||||
func subviewsRecursive(in _view: UIView) -> [UIView] {
|
||||
return _view.subviews + _view.subviews.flatMap { subviewsRecursive(in: $0) }
|
||||
}
|
||||
|
||||
var allViews: [UIView] = []
|
||||
|
||||
for window in UIApplication.shared.windows {
|
||||
allViews.append(contentsOf: subviewsRecursive(in: window))
|
||||
}
|
||||
allViews.forEach {
|
||||
let tracker = BorderManager(view: $0)
|
||||
GLOBAL_BORDER_TRACKERS.append(tracker)
|
||||
tracker.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
|
||||
isVisible = false
|
||||
} else {
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
consoleView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
// Renders color properly (for dark appearance).
|
||||
consoleView.backgroundColor = .black
|
||||
}
|
||||
|
||||
public func _print(_ items: Any) {
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.headIndent = 7
|
||||
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.paragraphStyle: paragraphStyle,
|
||||
.foregroundColor: UIColor.white,
|
||||
.font: UIFont.systemFont(ofSize: 7, weight: .semibold, design: .monospaced)
|
||||
]
|
||||
|
||||
let string: String = {
|
||||
if consoleTextView.attributedText.string == "" {
|
||||
return "\(items)"
|
||||
} else {
|
||||
return "\(items)\n" + consoleTextView.text
|
||||
}
|
||||
}()
|
||||
|
||||
consoleTextView.attributedText = NSAttributedString(string: string, attributes: attributes)
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
consoleTextView.text = ""
|
||||
}
|
||||
|
||||
func makeMenu() -> UIMenu {
|
||||
let viewFrames = UIAction(title: debugBordersEnabled ? "Hide View Frames" : "Show View Frames",
|
||||
image: UIImage(systemName: "rectangle.3.offgrid"), handler: { _ in
|
||||
self.debugBordersEnabled.toggle()
|
||||
self.menuButton?.menu = self.makeMenu()
|
||||
})
|
||||
|
||||
let respring = UIAction(title: "Restart SpringBoard",
|
||||
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.masksToBounds = true
|
||||
|
||||
let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
|
||||
window.transform = .init(scaleX: 0.96, y: 0.96)
|
||||
window.alpha = 0
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
while true {
|
||||
window.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
}
|
||||
animator.startAnimation()
|
||||
})
|
||||
|
||||
return UIMenu(title: "", children: [viewFrames, respring])
|
||||
}
|
||||
|
||||
@objc func longPressAction(recognizer: UILongPressGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
feedbackGenerator.selectionChanged()
|
||||
|
||||
scrollLocked = false
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1.04, y: 1.04)
|
||||
consoleTextView.alpha = 0.5
|
||||
}.startAnimation()
|
||||
case .cancelled, .ended:
|
||||
|
||||
scrollLocked = true
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
}.startAnimation()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
@objc func consolePiPPanner(recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
if recognizer.state == .began {
|
||||
initialViewLocation = consoleView.center
|
||||
}
|
||||
|
||||
guard !scrollLocked else { return }
|
||||
|
||||
let translation = recognizer.translation(in: consoleView.superview)
|
||||
let velocity = recognizer.velocity(in: consoleView.superview)
|
||||
|
||||
switch recognizer.state {
|
||||
case .changed:
|
||||
|
||||
consoleView.center.x = initialViewLocation.x + translation.x
|
||||
consoleView.center.y = initialViewLocation.y + translation.y
|
||||
|
||||
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: consoleView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
|
||||
y: consoleView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
|
||||
)
|
||||
|
||||
let nearestTargetPosition = nearestTargetTo(projectedPosition, possibleTargets: possibleEndpoints)
|
||||
|
||||
let relativeInitialVelocity = CGVector(
|
||||
dx: relativeVelocity(forVelocity: velocity.x, from: consoleView.center.x, to: nearestTargetPosition.x),
|
||||
dy: relativeVelocity(forVelocity: velocity.y, from: consoleView.center.y, to: nearestTargetPosition.y)
|
||||
)
|
||||
|
||||
let timingParameters = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)
|
||||
let positionAnimator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
|
||||
positionAnimator.addAnimations { [self] in
|
||||
consoleView.center = nearestTargetPosition
|
||||
}
|
||||
positionAnimator.startAnimation()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// Animate touch down.
|
||||
func consolePiPTouchDown() {
|
||||
UIViewPropertyAnimator(duration: 1, dampingRatio: 0.5) { [self] in
|
||||
consoleView.transform = .init(scaleX: 0.96, y: 0.96)
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
if !scrollLocked {
|
||||
consoleView.backgroundColor = #colorLiteral(red: 0.1331297589, green: 0.1331297589, blue: 0.1331297589, alpha: 1)
|
||||
}
|
||||
}.startAnimation()
|
||||
}
|
||||
|
||||
// Animate touch up.
|
||||
func consolePiPTouchUp() {
|
||||
UIViewPropertyAnimator(duration: 0.8, dampingRatio: 0.4) { [self] in
|
||||
consoleView.transform = .init(scaleX: 1, y: 1)
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) { [self] in
|
||||
consoleTextView.alpha = 1
|
||||
}.startAnimation()
|
||||
|
||||
UIViewPropertyAnimator(duration: 0.75, dampingRatio: 1) { [self] in
|
||||
consoleView.backgroundColor = .black
|
||||
}.startAnimation()
|
||||
}
|
||||
|
||||
// Simulataneously listen to all gesture recognizers.
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func consolePiPTapStartEnd(recognizer: UITapStartEndGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
consolePiPTouchDown()
|
||||
case .cancelled:
|
||||
consolePiPTouchUp()
|
||||
case .changed:
|
||||
break
|
||||
case .ended:
|
||||
consolePiPTouchUp()
|
||||
case .failed:
|
||||
consolePiPTouchUp()
|
||||
case .possible:
|
||||
consolePiPTouchUp()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom window for the console to appear above other windows while passing touches down.
|
||||
class ConsoleWindow: UIWindow {
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
|
||||
let hitView = super.hitTest(point, with: event)!
|
||||
|
||||
return hitView.isKind(of: ConsoleWindow.self) ? nil : hitView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import UIKit.UIGestureRecognizerSubclass
|
||||
|
||||
public class UITapStartEndGestureRecognizer: UITapGestureRecognizer {
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with: UIEvent) {
|
||||
self.state = .began
|
||||
}
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with: UIEvent) {
|
||||
self.state = .changed
|
||||
}
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with: UIEvent) {
|
||||
self.state = .ended
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Fun hacks!
|
||||
extension UIView {
|
||||
/// Swizzle UIView to use custom frame system when needed.
|
||||
static let swizzleDebugBehaviour: Void = {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")),
|
||||
let swizzledMethod = class_getInstanceMethod(UIWindow.self, #selector(swizzled_statusBarAppearance))
|
||||
else { return }
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}()
|
||||
|
||||
@objc func swizzled_statusBarAppearance() -> Bool {
|
||||
return isKeyWindow
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Extensions.swift
|
||||
//
|
||||
// Created by Duraid Abdul.
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIScreen {
|
||||
|
||||
/// Screen size.
|
||||
static var size: CGSize {
|
||||
return UIScreen.main.bounds.size
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
var statusBarHeight: CGFloat {
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
return window.safeAreaInsets.top
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIFont {
|
||||
class func systemFont(ofSize size: CGFloat, weight: UIFont.Weight, design: UIFontDescriptor.SystemDesign) -> UIFont {
|
||||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).addingAttributes([UIFontDescriptor.AttributeName.traits : [UIFontDescriptor.TraitKey.weight : weight]]).withDesign(design)
|
||||
|
||||
return UIFont(descriptor: descriptor!, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// GestureEndpointPredictor.swift
|
||||
//
|
||||
// Created by Duraid Abdul.
|
||||
// Copyright © 2021 Duraid Abdul. All rights reserved.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
import UIKit
|
||||
|
||||
extension CGPoint {
|
||||
|
||||
/// Calculates the distance between two points in 2D space.
|
||||
/// + returns: The distance from this point to the given point.
|
||||
func distance(to point: CGPoint) -> CGFloat {
|
||||
// Pythagoras
|
||||
return sqrt(pow(point.x - self.x, 2) + pow(point.y - self.y, 2))
|
||||
}
|
||||
}
|
||||
|
||||
extension UISpringTimingParameters {
|
||||
|
||||
/**
|
||||
Simplified spring animation timing parameters.
|
||||
|
||||
- Parameters:
|
||||
- damping: ζ (damping ratio)
|
||||
- frequency: T (frequency response)
|
||||
- initialVelocity: [See Here](https://developer.apple.com/documentation/uikit/uispringtimingparameters/1649909-initialvelocity)
|
||||
*/
|
||||
convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) {
|
||||
// Stiffness represents the spring constant, k
|
||||
let stiffness = pow(2 * .pi / response, 2)
|
||||
let dampingCoefficient = 4 * .pi * damping / response
|
||||
self.init(mass: 1, stiffness: stiffness, damping: dampingCoefficient, initialVelocity: initialVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Calculates a unit vector for the initial velocity of a spring animation.
|
||||
|
||||
- Parameters:
|
||||
- currentLocation: The current location of the view that will be animated.
|
||||
- targetLocation: The location that the view will be animated to.
|
||||
- velocity: The current velocity of the moving view. For more information, see [initialVelocity](https://developer.apple.com/documentation/uikit/uispringtimingparameters/1649909-initialvelocity).
|
||||
- Returns:
|
||||
A unit vector representing the initial velocity of the view
|
||||
|
||||
This function can be used to form a CGVector to be used in UISpringTimingParameters.
|
||||
For more information, see [UISpringTimingParameters](https://developer.apple.com/documentation/uikit/uispringtimingparameters).
|
||||
*/
|
||||
func relativeVelocity(forVelocity velocity: CGFloat, from currentLocation: CGFloat, to targetLocation: CGFloat) -> CGFloat {
|
||||
let travelDistance = (targetLocation - currentLocation)
|
||||
|
||||
// Returns an intitial velocity of 0 if
|
||||
guard travelDistance != 0 else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return velocity / travelDistance
|
||||
}
|
||||
|
||||
/// Distance traveled after decelerating to zero velocity at a constant rate.
|
||||
func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
|
||||
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
|
||||
}
|
||||
|
||||
/// Calculates the nearest point from a specified array to the specified point.
|
||||
func nearestTargetTo(_ point: CGPoint, possibleTargets: [CGPoint]) -> CGPoint {
|
||||
|
||||
var currentShortestDistance = CGFloat.greatestFiniteMagnitude
|
||||
var nearestEndpoint = CGPoint.zero
|
||||
for endpoint in possibleTargets {
|
||||
let distance = point.distance(to: endpoint)
|
||||
if distance < currentShortestDistance {
|
||||
nearestEndpoint = endpoint
|
||||
currentShortestDistance = distance
|
||||
}
|
||||
}
|
||||
return nearestEndpoint
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,3 +0,0 @@
|
||||
struct LocalConsole {
|
||||
var text = "Hello, World!"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import XCTest
|
||||
@testable import LocalConsole
|
||||
|
||||
final class LocalConsoleTests: XCTestCase {
|
||||
func testExample() {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||
// results.
|
||||
XCTAssertEqual(LocalConsole().text, "Hello, World!")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user