Files
VoidLink/VoidLink/Utility/GenericUtils.swift
T

920 lines
33 KiB
Swift

//
// GenericUtils.swift
// VoidLink
//
// Created by True砖家 on 2026/2/26.
// Copyright © 2026 True砖家@Bilibili. All rights reserved.
//
import Foundation
import GameController
import ObjectiveC
import UIKit
@objc public class GenericUtils: NSObject {
@objc static func installSegmentedControlPreviousSelectionTracking() {
UISegmentedControl.installPreviousSelectionTracking()
}
@objc static var hardwareKeyboardAlreadyDetected: Bool = false
@objc static func isHardwareKeyboardConnected() -> Bool {
if #available(iOS 14.0, tvOS 14.0, *) {
hardwareKeyboardAlreadyDetected = GCKeyboard.coalesced != nil
return GCKeyboard.coalesced != nil
}
return false
}
@objc static func isFirstHardwareKeyboardOrMouseConnection() -> Bool {
let key = "hasConnectedHardwareKeyboardOrMouse"
guard !UserDefaults.standard.bool(forKey: key) else {
return false
}
UserDefaults.standard.set(true, forKey: key)
return true
}
@objc static func handleKeyboardOrMouseConnectionTip(in vc: UIViewController?) {
if isRunningOnMacAsiPadApp {
return
}
if hardwareKeyboardAlreadyDetected {
_ = isFirstHardwareKeyboardOrMouseConnection()
return;
}
else if isFirstHardwareKeyboardOrMouseConnection() {
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Keyboard/Mouse Connected"),
message: LocalizationHelper.localizedString(forKey:"keyboard&MouseStreamingTip"),
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
countdown: 11,
completion: {})
}
}
@objc static func needUpdateDefaultSettings() -> Bool {
// let key = "needUpdateDefaultSettings20260226-1"
let key = "needUpdateDefaultSettings20260408-3"
guard !UserDefaults.standard.bool(forKey: key) else {
return false
}
UserDefaults.standard.set(true, forKey: key)
return true
}
@objc static func needUpdatePartialSettings() -> Bool {
// let key = "needUpdateDefaultSettings20260226-1"
let key = "needUpdatePartialSettings20260620"
guard !UserDefaults.standard.bool(forKey: key) else {
return false
}
UserDefaults.standard.set(true, forKey: key)
return true
}
@objc static func isEnableOswForNativeTouchSwitchFirstFlipping() -> Bool {
let key = "enableOswForNativeTouchSwitchFlipped"
guard !UserDefaults.standard.bool(forKey: key) else {
return false
}
UserDefaults.standard.set(true, forKey: key)
return true
}
@objc static func isFirstLaunchPressureCurveTool() -> Bool {
let key = "hasLaunchedPressureCurveTool"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func isFirstLaunchGamepadOverlayFeature() -> Bool {
let key = "hasTouchedGamepadOverlayFeature20260405-2"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func isFirstTappingGameProfileSelectorFromMainFrame() -> Bool {
let key = "hasTappedGameProfileSelectorFromMainFrame-6"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func isFirstTappingFolderInLayoutTool() -> Bool {
let key = "hasTappedFolderInLayoutTool2"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static var hasTappedMagnifier = false
@objc static func isFirstTappingMagnifier() -> Bool {
guard !hasTappedMagnifier else { return false }
hasTappedMagnifier = true
let key = "hasTappedMagnifier2"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleMagnifierTip(in vc: UIViewController?) {
if isFirstTappingMagnifier() {
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Magnifier"),
message: "\n\(LocalizationHelper.localizedString(forKey: "magnifierTip"))\n\n\(LocalizationHelper.localizedString(forKey: "magnifierPersistTip"))",
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 5,
)
}
}
@objc static var hasTappedVelocityBasedTouchpad = false
@objc static func isFirstTappingVelocityBasedTouchpad() -> Bool {
guard !hasTappedVelocityBasedTouchpad else { return false }
hasTappedVelocityBasedTouchpad = true
let key = "hasTappedVelocityBasedTouchpad6"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleVelocityBasedTouchpadTip(in vc: UIViewController?) {
if isFirstTappingVelocityBasedTouchpad() {
AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial")
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "velocityBasedTouchpadTip"))",
withCancel: true,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 5,
completion: {
if AlertControllerUtil.actionCancelled {
GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "velocityBasedTouchpadLink"))
}
}
)
}
}
@objc static var hasTappedSlideAndHoldFolderButton = false
@objc static func isFirstTappingSlideAndHoldFolderButton() -> Bool {
guard !hasTappedSlideAndHoldFolderButton else { return false }
hasTappedSlideAndHoldFolderButton = true
let key = "hasTappedSlideAndHoldFolderButton2"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleSlideAndHoldFolderButtonTip(in vc: UIViewController?) {
if isFirstTappingSlideAndHoldFolderButton() {
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "slideHoldFolderButtonTip"))",
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 5,
)
}
}
@objc static var hasTappedGamingLayoutFolderInEditMode: Bool = false
@objc static func isFirstTappingGamingLayoutFolderInEditMode() -> Bool {
guard !hasTappedGamingLayoutFolderInEditMode else { return false }
hasTappedGamingLayoutFolderInEditMode = true
let key = "hasTappedGamingLayoutFolderInEditMode"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleGamingLayoutFolderTip(in vc: UIViewController?) {
if isFirstTappingGamingLayoutFolderInEditMode() {
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "gamingLayoutFolderTip"))",
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 5,
)
}
}
@objc static func isFirstEnteringLayoutTool() -> Bool {
let key = "hasEnteredLayoutTool1"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleLayoutToolTip(in vc: UIViewController?) {
if isFirstEnteringLayoutTool() {
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "layoutToolTip"))",
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 7,
)
}
}
@objc static var hasTappedOnscreenGyroButton = false
@objc static func isFirstTappingOnscreenGyroButton() -> Bool {
guard !hasTappedOnscreenGyroButton else { return false }
hasTappedOnscreenGyroButton = true
let key = "hasTappedOnscreenGyroButton"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleGyroButtonTip(in vc: UIViewController?) {
if isFirstTappingOnscreenGyroButton() {
AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial")
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "gyroButtonTip"))",
withCancel: true,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 5,
completion: {
if AlertControllerUtil.actionCancelled {
GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "yourMotionControlSoution"))
}
}
)
}
}
@objc static var hasTappedButtonModeSelector = false
@objc static func isFirstTappingButtonModeSelector() -> Bool {
guard !hasTappedButtonModeSelector else { return false }
hasTappedButtonModeSelector = true
let key = "hasTappedButtonModeSelector3"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleButtonModeTip(in vc: UIViewController?) {
if isFirstTappingButtonModeSelector() {
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "buttonModeTip"))",
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "Tutorial"),
countdown: 3,
completion: {
GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "buttonModeLink"))
}
)
}
}
@objc static var hasTappedStickWheel = false
@objc static func isFirstTappingStickWheel() -> Bool {
guard !hasTappedStickWheel else { return false }
hasTappedStickWheel = true
let key = "hasTappedStickWheel"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleStickWheelTip(in vc: UIViewController?) {
if isFirstTappingStickWheel() {
AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial")
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "stickWheelTip"))",
withCancel: true,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 5,
completion: {
if AlertControllerUtil.actionCancelled {
GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "stickWheelLink"))
}
}
)
}
}
@objc static var hasTappedWidgetPanel = false
@objc static func isFirstTappingWidgetPanel() -> Bool {
guard !hasTappedWidgetPanel else { return false }
hasTappedWidgetPanel = true
let key = "hasTappedWidgetPanel7"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleWidgetPanelTip(in vc: UIViewController?) {
if isFirstTappingWidgetPanel() {
// AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial")
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "widgetPanelTip"))",
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 6,
completion: {
if AlertControllerUtil.actionCancelled {
GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "widgetPanelLink"))
}
}
)
}
}
@objc static var hasChangedTouchMode = false
@objc static func isFirstChangingTouchMode() -> Bool {
guard !hasChangedTouchMode else { return false }
hasChangedTouchMode = true
let key = "hasChangedTouchMode5"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleTouchModeChangingTip(in vc: UIViewController?) {
if isFirstChangingTouchMode() {
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "touchModeStackTip"))",
withCancel: false,
buttonTitle: LocalizationHelper.localizedString(forKey: "Got it!"),
countdown: 6,
completion: {
}
)
}
}
@objc static func isFirstTappingInputAccessoryBar() -> Bool {
let key = "isFirstTappingInputAccessoryBar"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func isFirstStreamingOnMac() -> Bool {
if !isRunningOnMacAsiPadApp {return false}
let key = "hasStreamedOnMac"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func handleLegacyFramePacingTip(in vc: UIViewController?, with selector: UISegmentedControl, passAlert: Bool = false, uiAction: (() -> Void)? = nil) {
if passAlert || selector.selectedSegmentIndex == FramePacingMode.queue.rawValue {
uiAction?()
return
}
if selector.previousSelectedSegmentIndex != FramePacingMode.queue.rawValue {
uiAction?()
return
}
AlertControllerUtil.cancelButtonString = "Cancel"
AlertControllerUtil.showAlert(
in: vc,
title: LocalizationHelper.localizedString(forKey: "Tips"),
message: "\n\(LocalizationHelper.localizedString(forKey: "legacyFramePacingTip"))",
withCancel: true,
buttonTitle: LocalizationHelper.localizedString(forKey: "Confirm"),
countdown: 0,
completion: {
if AlertControllerUtil.actionCancelled {selector.selectedSegmentIndex = selector.previousSelectedSegmentIndex}
uiAction?()
}
)
}
@objc static func gamepadOverlayFeatureTipTitle() -> String {
LocalizationHelper.localizedString(forKey: "Gamepad Overlay")
}
@objc static func gamepadOverlayFeatureTipMessage() -> String {
LocalizationHelper.localizedString(forKey: "gamepadOverlayFeatureTip")
}
@objc static func gamepadOverlayFeatureTipButtonTitle() -> String {
LocalizationHelper.localizedString(forKey: "Got it!")
}
@objc static func isIPhone() -> Bool {
return UIDevice.current.userInterfaceIdiom == .phone
}
@objc static func isIPad() -> Bool {
return UIDevice.current.userInterfaceIdiom == .pad
}
@objc static var isRunningOnMacAsiPadApp: Bool {
if #available(iOS 14.0, *) {
return ProcessInfo.processInfo.isiOSAppOnMac
}
return false
}
@objc static let iOS26Available: Bool = {
if #available(iOS 26.0, tvOS 26.0, *) {
return true
} else {
return false
}
}()
@objc static let liquidGlassEnabled: Bool = {
if #available(iOS 26.0, tvOS 26.0, *) {
let useLegacyUI = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool
return useLegacyUI != true
} else {
return false
}
}()
@objc static let isGUIWidgetPickerAvailable: Bool = {
if #available(iOS 13.0, tvOS 13.0, *) {
return true
} else {
return false
}
}()
@objc static let menuSeparatorWidth: CGFloat = 0.7
@objc static let menuSectionSeparatorWidth: CGFloat = 0.7
@objc static var legacyToolbarHeight: CGFloat {
return 44
}
@objc static var inputAccessoryBarHeight: CGFloat {
if #available(iOS 13.0, *){
return GenericUtils.isIPhone() ? 46 : 55
}
else {return 44}
}
@objc static var hostViewNavigationBarHeight: CGFloat {
switch UIDevice.current.userInterfaceIdiom {
case .phone:
return liquidGlassEnabled ? 54 : 44
case .pad:
return liquidGlassEnabled ? 54 : 50
default:
return liquidGlassEnabled ? 54 : 50
}
}
@objc static var settingsMenuNavigationBarHeight: CGFloat {
// return isIPhone() ? 44 : hostViewNavigationBarHeight
switch UIDevice.current.userInterfaceIdiom {
case .phone:
return liquidGlassEnabled ? hostViewNavigationBarHeight+5 : hostViewNavigationBarHeight
case .pad:
return liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight
default:
return liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight
}
}
@objc static var dockedNavBarTopAnchorOffset: CGFloat {
return liquidGlassEnabled ? 10 : 0
}
@available(iOS 26.0, *)
@objc static func applyOffTintColor(_ view: UIView) {
let name = String(describing: type(of: view))
if name.contains("UISwitchModernVisualElement") {
view.backgroundColor = ThemeManager.liquidGlassSwitchOffTint
view.layer.cornerRadius = view.bounds.height/2
view.clipsToBounds = true
}
for sub in view.subviews {
applyOffTintColor(sub)
}
}
@objc static var autoPopSoftKeyboard: Bool = true
@objc static var textFieldShouldResignAfterReturn: Bool = false
@objc static func getAtrributedPlaceHolder(text:String)-> NSAttributedString {
if #available(iOS 13.0, *) {
return NSAttributedString(
string: text,
attributes: [
.font: UIFont.systemFont(ofSize: 15),
.foregroundColor: UIColor.placeholderText
])
} else {
return NSAttributedString(
string: text,
attributes: [
.font: UIFont.systemFont(ofSize: 15),
.foregroundColor: UIColor.lightText
])
}
}
static var kScaleLayerKey: UInt8 = 0
@objc static func setVerticalScale(view: UIView, show: Bool) {
// 移除旧的
if let oldLayer = objc_getAssociatedObject(view, &kScaleLayerKey) as? CALayer {
oldLayer.removeFromSuperlayer()
objc_setAssociatedObject(view, &kScaleLayerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
guard show else { return }
let container = CALayer()
container.frame = view.bounds
container.contentsScale = UIScreen.main.scale
let step: CGFloat = 0.05
let totalSteps = Int(1.0 / step)
let path = UIBezierPath()
for i in 0...totalSteps {
let value = CGFloat(i) * step
let y = view.bounds.height * (1.0 - value)
let isMajor = i % 2 == 0
let lineLength: CGFloat = isMajor ? 10 : 5
// 刻度线
path.move(to: CGPoint(x: 0, y: y))
path.addLine(to: CGPoint(x: lineLength, y: y))
// 数值
if true {
let t = CATextLayer()
t.contentsScale = UIScreen.main.scale
t.font = UIFont.systemFont(ofSize: 13, weight: .medium)
t.fontSize = 13
t.foregroundColor = UIColor.red.cgColor
t.backgroundColor = ThemeManager.menuBackgroundColor.cgColor
t.alignmentMode = .left
t.string = String(format: "%.2f", value)
t.frame = CGRect(x: lineLength + 2, y: y - 7, width: 30, height: 14)
container.addSublayer(t)
}
}
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.strokeColor = UIColor.red.cgColor
shape.lineWidth = 1
container.addSublayer(shape)
view.layer.addSublayer(container)
objc_setAssociatedObject(view, &kScaleLayerKey, container, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
@objc static func toCGFloat(_ str: String) -> CGFloat {
return CGFloat(Double(str) ?? 0)
}
@objc(openUrl:)
static func openUrl(_ urlString: String) {
guard let url = URL(string: urlString),
UIApplication.shared.canOpenURL(url) else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
@objc static func isLandscape() -> Bool {
if #available(iOS 13.0, *) {
guard let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive })
else {return false}
return windowScene.interfaceOrientation.isLandscape
}
else {return GenericUtils.screenWidth > GenericUtils.screenHeight}
}
@objc static func viewIsLandscape(_ view: UIView?) -> Bool {
guard let view else {return false}
return view.bounds.width > view.bounds.height
}
@objc static var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
@objc static var screenHeight: CGFloat {
return UIScreen.main.bounds.height
}
@objc static var iOS18Available: Bool {
if #available(iOS 18.0, *) {return true}
else {return false}
}
@objc static var globeAsEscape: Bool = false
@objc(parentViewControllerForView:)
static func parentViewController(for view: UIView?) -> UIViewController? {
var responder: UIResponder? = view
while let currentResponder = responder {
if let viewController = currentResponder as? UIViewController {
return viewController
}
responder = currentResponder.next
}
return nil
}
@objc static func rootViewController() -> UIViewController? {
if #available(iOS 13.0, *) {
return UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }?
.windows
.first { $0.isKeyWindow }?
.rootViewController
} else {
return UIApplication.shared.keyWindow?.rootViewController
}
}
@objc static func topViewController() -> UIViewController? {
return topViewController(from: rootViewController())
}
private static func topViewController(from rootViewController: UIViewController?) -> UIViewController? {
if let navigationController = rootViewController as? UINavigationController {
return topViewController(from: navigationController.visibleViewController)
}
if let tabBarController = rootViewController as? UITabBarController {
return topViewController(from: tabBarController.selectedViewController)
}
if let splitViewController = rootViewController as? UISplitViewController,
let lastViewController = splitViewController.viewControllers.last {
return topViewController(from: lastViewController)
}
if let presentedViewController = rootViewController?.presentedViewController {
return topViewController(from: presentedViewController)
}
return rootViewController
}
}
extension UIView {
var parentViewController: UIViewController? {
var responder: UIResponder? = self
while let r = responder {
if let vc = r as? UIViewController {
return vc
}
responder = r.next
}
return nil
}
}
extension CGPoint {
var isValid: Bool {
x.isFinite && y.isFinite
}
}
extension String {
var localized: String {
LocalizationHelper.localizedString(forKey: self)
}
var localizedProfileName: String {
let parts = self.components(separatedBy: " - Restored")
let isRestored = self.contains(" - Restored")
let localized = isRestored ? "\(parts.first?.localized ?? "") - \("Restored".localized)" : self.localized
return localized
}
}
private var previousSelectedSegmentIndexKey: UInt8 = 0
private var lastKnownSelectedSegmentIndexKey: UInt8 = 0
extension UISegmentedControl {
@objc static func installPreviousSelectionTracking() {
_ = enablePreviousSelectionTracking
}
private static let enablePreviousSelectionTracking: Void = {
guard
let originalSetter = class_getInstanceMethod(
UISegmentedControl.self,
#selector(setter: UISegmentedControl.selectedSegmentIndex)
),
let swizzledSetter = class_getInstanceMethod(
UISegmentedControl.self,
#selector(UISegmentedControl.vl_setSelectedSegmentIndex(_:))
),
let originalSendAction = class_getInstanceMethod(
UISegmentedControl.self,
#selector(UISegmentedControl.sendAction(_:to:for:))
),
let swizzledSendAction = class_getInstanceMethod(
UISegmentedControl.self,
#selector(UISegmentedControl.vl_sendAction(_:to:for:))
)
else {
return
}
swizzleMethod(
on: UISegmentedControl.self,
originalMethod: originalSetter,
originalSelector: #selector(setter: UISegmentedControl.selectedSegmentIndex),
swizzledMethod: swizzledSetter,
swizzledSelector: #selector(UISegmentedControl.vl_setSelectedSegmentIndex(_:))
)
swizzleMethod(
on: UISegmentedControl.self,
originalMethod: originalSendAction,
originalSelector: #selector(UISegmentedControl.sendAction(_:to:for:)),
swizzledMethod: swizzledSendAction,
swizzledSelector: #selector(UISegmentedControl.vl_sendAction(_:to:for:))
)
}()
@objc var previousSelectedSegmentIndex: Int {
Self.installPreviousSelectionTracking()
initializeLastKnownIndexIfNeeded()
return (objc_getAssociatedObject(self, &previousSelectedSegmentIndexKey) as? NSNumber)?.intValue
?? selectedSegmentIndex
}
@objc func resetPreviousSelectedSegmentIndex() {
Self.installPreviousSelectionTracking()
storePreviousSelectedSegmentIndex(selectedSegmentIndex)
storeLastKnownSelectedSegmentIndex(selectedSegmentIndex)
}
@objc private func vl_setSelectedSegmentIndex(_ newValue: Int) {
Self.installPreviousSelectionTracking()
initializeLastKnownIndexIfNeeded()
let currentValue = selectedSegmentIndex
if currentValue != newValue {
storePreviousSelectedSegmentIndex(newValue)
storeLastKnownSelectedSegmentIndex(newValue)
}
vl_setSelectedSegmentIndex(newValue)
}
@objc private func vl_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
Self.installPreviousSelectionTracking()
updatePreviousSelectedSegmentIndexIfNeeded()
vl_sendAction(action, to: target, for: event)
}
private func initializeLastKnownIndexIfNeeded() {
guard objc_getAssociatedObject(self, &lastKnownSelectedSegmentIndexKey) == nil else {
return
}
storeLastKnownSelectedSegmentIndex(selectedSegmentIndex)
}
private func updatePreviousSelectedSegmentIndexIfNeeded() {
initializeLastKnownIndexIfNeeded()
let currentValue = selectedSegmentIndex
let lastKnownValue = (objc_getAssociatedObject(self, &lastKnownSelectedSegmentIndexKey) as? NSNumber)?.intValue
?? UISegmentedControl.noSegment
guard currentValue != lastKnownValue else {
return
}
storePreviousSelectedSegmentIndex(lastKnownValue)
storeLastKnownSelectedSegmentIndex(currentValue)
}
private func storePreviousSelectedSegmentIndex(_ value: Int) {
objc_setAssociatedObject(
self,
&previousSelectedSegmentIndexKey,
NSNumber(value: value),
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
private func storeLastKnownSelectedSegmentIndex(_ value: Int) {
objc_setAssociatedObject(
self,
&lastKnownSelectedSegmentIndexKey,
NSNumber(value: value),
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
private static func swizzleMethod(
on cls: AnyClass,
originalMethod: Method,
originalSelector: Selector,
swizzledMethod: Method,
swizzledSelector: Selector
) {
let didAddMethod = class_addMethod(
cls,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)
)
if didAddMethod {
class_replaceMethod(
cls,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod)
)
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}