Compare commits

..

55 Commits

Author SHA1 Message Date
Duraid Abdul b319499056 Merge pull request #3 from hxperl/add-uicolor-context
🎨 Add UIColor context to fix CI Build compile
2021-06-02 22:12:51 -07:00
hxperl 17d30ad554 🎨 Add UIColor context to fix CI Build compile 2021-06-03 13:09:15 +09:00
Duraid Abdul b56d27ff26 Add OS Compile Date to System Report 2021-06-01 14:46:05 -07:00
Duraid Abdul c7fe1daf26 Update System Report
Reorganized system report code for readability. Added some more valued to the system report.
2021-06-01 14:17:25 -07:00
Duraid Abdul 6db60e25b2 Asynchronous System Report 2021-05-31 21:24:23 -07:00
Duraid Abdul 5094ddc710 Added System Report 2021-05-31 21:15:22 -07:00
Duraid Abdul 9b5e0d84ac Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-24 01:46:08 -07:00
Duraid Abdul 1620fad461 Update Demo_Resize.gif 2021-05-24 01:44:17 -07:00
Duraid Abdul 86d9e18613 Update README.md 2021-05-24 01:40:04 -07:00
Duraid Abdul 832f507ab1 Update README.md 2021-05-24 01:38:59 -07:00
Duraid Abdul 2406568789 Update README.md 2021-05-24 01:35:52 -07:00
Duraid Abdul f57800e8a4 Update Demos 2021-05-24 01:27:11 -07:00
Duraid Abdul e7bc5d221b Update LCManager.swift 2021-05-24 01:01:40 -07:00
Duraid Abdul 6e121fb39b Update LCManager.swift
Fix for early initialization.
2021-05-24 00:56:07 -07:00
Duraid Abdul 4c8a519c4f Update README.md 2021-05-23 16:58:53 -07:00
Duraid Abdul d3d12e2450 Update README.md 2021-05-23 16:44:38 -07:00
Duraid Abdul 062f74a429 Update README.md 2021-05-23 16:35:52 -07:00
Duraid Abdul c8468823c0 Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-23 16:12:40 -07:00
Duraid Abdul c19df26edc Update README.md 2021-05-23 16:11:54 -07:00
Duraid Abdul d1227ee522 Update Demo.gif 2021-05-23 16:11:12 -07:00
Duraid Abdul 97a6ae0899 Update Demo.gif 2021-05-23 15:53:20 -07:00
Duraid Abdul 4b0f9e149a Update Demo.gif 2021-05-23 15:35:04 -07:00
Duraid Abdul 20f88c739f Delete Demo_1.gif 2021-05-23 01:22:21 -07:00
Duraid Abdul 98a66af9b2 Add files via upload 2021-05-23 01:20:59 -07:00
Duraid Abdul 19f2302f13 Add files via upload 2021-05-23 01:18:22 -07:00
Duraid Abdul 308a25f6db Delete Demo.gif 2021-05-23 01:17:48 -07:00
Duraid Abdul 40614dffec Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-23 01:15:05 -07:00
Duraid Abdul 245d6f0310 Update Demo.gif 2021-05-23 01:14:22 -07:00
Duraid Abdul 349d7359eb Update README.md 2021-05-23 01:07:57 -07:00
Duraid Abdul 37e0ab6323 Update Demo.gif 2021-05-22 23:43:37 -07:00
Duraid Abdul 1ea0dc7026 Update Demo.gif 2021-05-22 23:36:08 -07:00
Duraid Abdul a9db186136 Update README.md 2021-05-22 14:51:35 -07:00
Duraid Abdul 67e2ff5ce5 Update LCManager.swift 2021-05-22 14:47:17 -07:00
Duraid Abdul 1c12c101b6 Update LCManager.swift 2021-05-22 14:13:37 -07:00
Duraid Abdul cd31f0d55d Update LCManager.swift 2021-05-22 13:42:43 -07:00
Duraid Abdul 7a4a020c9c Merge pull request #2 from NoahFetz/main
Add ability to copy the console content to the clipboard.
2021-05-22 13:41:07 -07:00
Duraid Abdul 7208c56820 Merge branch 'main' into main 2021-05-22 13:39:29 -07:00
Duraid Abdul 8d7f902344 Update ResizeController.swift 2021-05-22 02:44:05 -07:00
Duraid Abdul eca9258206 Update ResizeController.swift 2021-05-22 02:40:00 -07:00
Duraid Abdul 87824eb760 Update ResizeController.swift 2021-05-22 02:32:44 -07:00
Duraid Abdul 5bcd63c6b9 Update ResizeController.swift 2021-05-22 02:27:21 -07:00
Duraid Abdul ec233369aa Update ResizeController.swift 2021-05-22 02:27:09 -07:00
Duraid Abdul 98ff7089cd Update ResizeController.swift 2021-05-22 02:19:57 -07:00
Duraid Abdul 1d41bbd85e Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-22 02:08:24 -07:00
Duraid Abdul c2bc901427 Resize mode 2021-05-22 02:08:18 -07:00
Noah Fetz 6025ea6cc2 Add ability to copy the console content to the clipboard 2021-05-20 16:00:24 +02:00
Duraid Abdul 1bf0e61732 Update README.md 2021-05-19 22:25:42 -07:00
Duraid Abdul 82c5086787 Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-19 22:11:11 -07:00
Duraid Abdul a8b7f702b4 Update LCManager.swift 2021-05-19 22:11:05 -07:00
Duraid Abdul a7a5cba7d9 Update README.md 2021-05-13 22:35:29 -07:00
Duraid Abdul 20e44ee90a Update LCManager.swift 2021-05-13 09:57:58 -07:00
Duraid Abdul 0d364aa66b Update README.md 2021-05-12 11:23:01 -07:00
Duraid Abdul 3a18df11ea Merge branch 'main' of https://github.com/duraidabdul/LocalConsole into main 2021-05-12 11:19:50 -07:00
Duraid Abdul 5f8cba0388 Update LCManager.swift 2021-05-12 11:19:48 -07:00
Duraid Abdul 930bc42cef Update README.md 2021-05-12 11:09:49 -07:00
9 changed files with 984 additions and 83 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 MiB

After

Width:  |  Height:  |  Size: 11 MiB

+26 -12
View File
@@ -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/LocalConsole/blob/main/Additional%20Files/Demo_Pan.gif?raw=true" width="320">
<img src="https://github.com/duraidabdul/LocalConsole/blob/main/Additional%20Files/Demo_Resize.gif?raw=true" width="320">
</div>
## **Setup**
@@ -21,20 +24,31 @@ let localConsoleManager = LCManager.shared
Once prepared, the localConsole can be used throughout your project.
```swift
// Show local console.
// Show the console view.
localConsoleManager.isVisible = true
// Hide local console.
// Hide the console view.
localConsoleManager.isVisible = false
// Print items to local console.
localConsoleManager.print("Hello, world!")
// Clear local console text.
localConsoleManager.clear()
```
## **Upcoming Features**
* Custom console view size
* Custom console view font size
```swift
// Print items to the console view.
localConsoleManager.print("Hello, world!")
// Clear console text.
localConsoleManager.clear()
// Copy console text.
localConsoleManager.copy()
```
```swift
// Change the console view font size.
localConsoleManager.fontSize = 5
```
## **To-Do**
* Support for iOS 13
* Screen edge console hiding
* Make console view reactive to landscape/portrait switch
-4
View File
@@ -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
+24 -3
View File
@@ -5,8 +5,6 @@
// Copyright © 2021 Duraid Abdul. All rights reserved.
//
#if canImport(UIKit)
import UIKit
extension UIScreen {
@@ -15,6 +13,13 @@ 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
}
extension UIApplication {
@@ -35,4 +40,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
+218 -60
View File
@@ -5,7 +5,7 @@
// Copyright © 2021 Duraid Abdul. All rights reserved.
//
#if canImport(UIKit)
//#if canImport(UIKit)
import UIKit
@@ -16,55 +16,85 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
public static let shared = LCManager()
let consoleSize = CGSize(width: 212, height: 124)
/// 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 }
guard fontSize <= 20 else { fontSize = 20; return }
setAttributedText(consoleTextView.text)
}
}
// Strong reference needed to keep the window alive.
let defaultConsoleSize = CGSize(width: 212, height: 124)
/// The fixed size of the console view.
lazy var consoleSize = defaultConsoleSize {
didSet {
consoleView.frame.size = consoleSize
if consoleView.frame.size.width > ResizeController.kMaxConsoleWidth {
consoleTextView.frame.size.width = ResizeController.kMaxConsoleWidth
} else {
consoleTextView.frame.size.width = consoleSize.width
}
// 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!
/// Text view that displays printed items.
let consoleTextView = UITextView()
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 - (consoleWindow?.safeAreaInsets.bottom ?? 0) - 12),
CGPoint(x: UIScreen.portraitSize.width - consoleSize.width / 2 - 12,
y: UIScreen.portraitSize.height - consoleSize.height / 2 - (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 - (consoleWindow?.safeAreaInsets.bottom ?? 0) - 12)]
}
}
lazy var initialViewLocation: CGPoint = .zero
override init() {
super.init()
// Configure console window.
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
configureWindow()
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
}
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 view.
consoleView.frame.size = consoleSize
consoleView.backgroundColor = .black
consoleView.layer.shadowRadius = 16
@@ -76,15 +106,16 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
consoleView.layer.cornerRadius = 20
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)
@@ -95,17 +126,16 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
consoleTextView.isSelectable = false
consoleTextView.showsVerticalScrollIndicator = false
consoleTextView.contentInsetAdjustmentBehavior = .never
consoleTextView.autoresizingMask = [.flexibleHeight]
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)
@@ -120,6 +150,7 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
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),
@@ -129,6 +160,7 @@ 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)))
@@ -139,11 +171,60 @@ 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
}
/// 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 {
@@ -166,6 +247,37 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
}
/// 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)
// Update the context menu to show the clipboard/clear actions.
menuButton.menu = makeMenu()
}
/// Clear text in the console view.
public func clear() {
consoleTextView.text = ""
// Update the context menu to hide the clipboard/clear actions.
menuButton.menu = makeMenu()
}
/// Copy the console view text to the device's clipboard.
public func copy() {
UIPasteboard.general.string = consoleTextView.text
}
// MARK: - Private
private var debugBordersEnabled = false {
didSet {
GLOBAL_DEBUG_BORDERS = debugBordersEnabled
@@ -194,6 +306,28 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
}
func systemReport() {
DispatchQueue.main.async { [self] in
print("Screen Scale: \(UIScreen.main.scale)\n")
print("Screen Radius: \(UIScreen.main.value(forKey: "_displayCornerRadius") as! CGFloat)")
print("Screen Size: \(UIScreen.main.bounds.size)")
print("Max Frame Rate: \(UIScreen.main.maximumFramesPerSecond) Hz")
print("Low Power Mode: \(ProcessInfo.processInfo.isLowPowerModeEnabled)")
print("System Uptime: \(Int(ProcessInfo.processInfo.systemUptime))s")
print("Thermal State: \(SystemReport.shared.thermalState)")
print("Processor Cores: \(Int(ProcessInfo.processInfo.processorCount))")
print("Memory: \(round(100 * Double(ProcessInfo.processInfo.physicalMemory) * pow(10, -9)) / 100) GB")
print("OS Compile Date: \(SystemReport.shared.compileDate)")
print("System Version: \(SystemReport.shared.versionString)")
print("Kernel Version: \(SystemReport.shared.kernel) \(SystemReport.shared.kernelVersion)")
print("Firmware: \(SystemReport.shared.gestaltFirmwareVersion)")
print("Architecture: \(SystemReport.shared.gestaltArchitecture)")
print("Model Identifier: \(SystemReport.shared.gestaltModelIdentifier)")
print("Marketing Name: \(SystemReport.shared.gestaltMarketingName)")
}
}
@objc func toggleLock() {
scrollLocked.toggle()
}
@@ -219,37 +353,50 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
consoleView.backgroundColor = .black
}
public func print(_ items: Any) {
func setAttributedText(_ string: String) {
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)
.font: UIFont.systemFont(ofSize: fontSize, 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 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])
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()
self.menuButton.menu = self.makeMenu()
})
let systemReport = UIAction(title: "System Report",
image: UIImage(systemName: "doc.badge.gearshape"), handler: { _ in
self.systemReport()
})
let respring = UIAction(title: "Restart SpringBoard",
@@ -270,8 +417,18 @@ public class LCManager: NSObject, UIGestureRecognizerDelegate {
}
animator.startAnimation()
})
let debugActions = UIMenu(title: "", options: .displayInline, children: [viewFrames, systemReport, 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) {
@@ -403,9 +560,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)
}
}
@@ -459,4 +617,4 @@ extension UIWindow {
}
}
#endif
//#endif
+608
View File
@@ -0,0 +1,608 @@
//
// ResizeController.swift
//
// Created by Duraid Abdul.
// Copyright © 2021 Duraid Abdul. All rights reserved.
//
import UIKit
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 {
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
@objc func verticalPanner(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: bottomGrabber.superview)
let maxHeight: CGFloat = 346
let minHeight: CGFloat = 108
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 kMaxConsoleWidth: CGFloat = UIScreen.portraitSize.width - 56
@objc func horizontalPanner(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: bottomGrabber.superview)
let maxWidth: CGFloat = Self.kMaxConsoleWidth
let minWidth: CGFloat = 112
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
}
}
}
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.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
}
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
}
}
}
+108
View File
@@ -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: "/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist")
let dictionary = NSDictionary(contentsOf: url)
return dictionary?.value(forKey: "CacheExtra") as? NSDictionary
}()
// Device marketing name.
lazy var gestaltMarketingName: Any = gestaltCacheExtra?.value(forKey: "Z/dqyWS6OZTRy10UcmUAhw") ?? "Unknown"
// iBoot (second-stage loader) version.
lazy var gestaltFirmwareVersion: Any = gestaltCacheExtra?.value(forKey: "LeSRsiLoJCMhjn6nd6GWbQ") ?? "Unknown"
// CPU architecture.
lazy var gestaltArchitecture: Any = gestaltCacheExtra?.value(forKey: "k7QIBwZJJOVw+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: "h9jDsbgj7xIVeIQ8S3/X3Q") ?? modelIdentifier
// Fallback in case gestaltModelIdentifier doesn't return a value.
var modelIdentifier: String {
if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MODEL_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("kern.ostype", nil, &size, nil, 0)
var string = [CChar](repeating: 0, count: Int(size))
sysctlbyname("kern.ostype", &string, &size, nil, 0)
return String(cString: string)
}
var kernelVersion: String {
var size = 0
sysctlbyname("kern.osrelease", nil, &size, nil, 0)
var string = [CChar](repeating: 0, count: Int(size))
sysctlbyname("kern.osrelease", &string, &size, nil, 0)
return String(cString: string)
}
var compileDate: String {
var size = 0
sysctlbyname("kern.version", nil, &size, nil, 0)
var string = [CChar](repeating: 0, count: Int(size))
sysctlbyname("kern.version", &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"
}
}