import UIKit import ObjectiveC.runtime // MARK: - IOKit @objc private protocol IOHIDEvent: NSObjectProtocol {} private struct IOHIDDigitizerEventMask: OptionSet { let rawValue: UInt32 init(rawValue: UInt32) { self.rawValue = rawValue } static let range = IOHIDDigitizerEventMask(rawValue: 1 << 0) static let touch = IOHIDDigitizerEventMask(rawValue: 1 << 1) static let position = IOHIDDigitizerEventMask(rawValue: 1 << 2) static let cancel = IOHIDDigitizerEventMask(rawValue: 1 << 7) } private enum IOHIDEventField: UInt32 { case digitizerX = 0xB0000 case digitizerY = 0xB0001 case digitizerMajorRadius = 0xB0014 case digitizerMinorRadius = 0xB0015 case digitizerIsDisplayIntegrated = 0xB0019 } private enum IOHIDDigitizerTransducerType: UInt32 { case finger = 2 } private struct IOKit { typealias CHIDEventCreateDigitizerEvent = @convention(c) (_ allocator: CFAllocator?, _ timestamp: UInt64, _ transducer_type: IOHIDDigitizerTransducerType.RawValue, _ index: UInt32, _ identifier: UInt32, _ eventMask: IOHIDDigitizerEventMask.RawValue, _ buttonEvent: UInt32, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat, _ pressure: CGFloat, _ twist: CGFloat, _ isRange: DarwinBoolean, _ isTouch: DarwinBoolean, _ options: CFOptionFlags) -> IOHIDEvent typealias CHIDEventCreateDigitizerFingerEvent = @convention(c) (_ allocator: CFAllocator?, _ timestamp: UInt64, _ identifier: UInt32, _ fingerIndex: UInt32, _ eventMask: IOHIDDigitizerEventMask.RawValue, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat, _ pressure: CGFloat, _ twist: CGFloat, _ isRange: DarwinBoolean, _ isTouch: DarwinBoolean, _ options: CFOptionFlags) -> IOHIDEvent typealias CHIDEventGetIntegerValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue) -> Int typealias CHIDEventSetIntegerValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue, _ value: Int) -> Void typealias CHIDEventGetFloatValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue) -> CGFloat typealias CHIDEventSetFloatValue = @convention(c) (_ event: IOHIDEvent, _ field: IOHIDEventField.RawValue, _ value: CGFloat) -> Void typealias CHIDEventAppendEvent = @convention(c) (_ event: IOHIDEvent, _ subevent: IOHIDEvent, _ options: CFOptionFlags) -> Void let hidCreateDigitizerEvent: CHIDEventCreateDigitizerEvent let hidCreateDigitizerFingerEvent: CHIDEventCreateDigitizerFingerEvent let hidEventGetIntegerValue: CHIDEventGetIntegerValue let hidEventSetIntegerValue: CHIDEventSetIntegerValue let hidEventGetFloatValue: CHIDEventGetFloatValue let hidEventSetFloatValue: CHIDEventSetFloatValue let hidEventAppend: CHIDEventAppendEvent static let shared: IOKit = { let handle = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW) return IOKit( hidCreateDigitizerEvent: unsafeBitCast(dlsym(handle, "IOHIDEventCreateDigitizerEvent"), to: CHIDEventCreateDigitizerEvent.self), hidCreateDigitizerFingerEvent: unsafeBitCast(dlsym(handle, "IOHIDEventCreateDigitizerFingerEvent"), to: CHIDEventCreateDigitizerFingerEvent.self), hidEventGetIntegerValue: unsafeBitCast(dlsym(handle, "IOHIDEventGetIntegerValue"), to: CHIDEventGetIntegerValue.self), hidEventSetIntegerValue: unsafeBitCast(dlsym(handle, "IOHIDEventSetIntegerValue"), to: CHIDEventSetIntegerValue.self), hidEventGetFloatValue: unsafeBitCast(dlsym(handle, "IOHIDEventGetFloatValue"), to: CHIDEventGetFloatValue.self), hidEventSetFloatValue: unsafeBitCast(dlsym(handle, "IOHIDEventSetFloatValue"), to: CHIDEventSetFloatValue.self), hidEventAppend: unsafeBitCast(dlsym(handle, "IOHIDEventAppendEvent"), to: CHIDEventAppendEvent.self)) }() } // MARK: - private struct BackBoardServices { typealias CHIDEventSetDigitizerInfo = @convention(c) (_ digitizerEvent: IOHIDEvent, _ contextID: UInt32, _ systemGestureIsPossible: DarwinBoolean, _ isSystemGestureStateChangeEvent: DarwinBoolean, _ displayUUID: CFString?, _ initialTouchTimestamp: CFTimeInterval, _ maxForce: Float) -> Void let hidEventSetDigitizerInfo: CHIDEventSetDigitizerInfo static let shared: BackBoardServices = { let handle = dlopen("/System/Library/PrivateFrameworks/BackBoardServices.framework/BackBoardServices", RTLD_NOW) return BackBoardServices( hidEventSetDigitizerInfo: unsafeBitCast(dlsym(handle, "BKSHIDEventSetDigitizerInfo"), to: CHIDEventSetDigitizerInfo.self)) }() } // MARK: - @objc private protocol UIApplicationSPI: NSObjectProtocol { @objc(_enqueueHIDEvent:) func enqueue(_ event: IOHIDEvent) } @objc private protocol UIWindowSPI: NSObjectProtocol { @objc(_contextId) var contextID: UInt32 { get } } private struct UIKit { init() {} static let shared: UIKit = { class_addProtocol(UIApplication.self, UIApplicationSPI.self) class_addProtocol(UIWindow.self, UIWindowSPI.self) return UIKit() }() @discardableResult func send(_ event: IOHIDEvent, in window: UIWindow?) -> Bool { guard let window = window as? UIWindow & UIWindowSPI, let app = window.target(forAction: #selector(UIApplicationSPI.enqueue), withSender: window) as? UIApplication & UIApplicationSPI else { return false } BackBoardServices.shared.hidEventSetDigitizerInfo(event, window.contextID, false, false, nil, 0, 0) app.enqueue(event) return true } } // MARK: - struct EventGenerator { struct Touch { var point = CGPoint.zero var phase = UITouch.Phase.stationary } struct Hand { var touches = [Touch]() var phase = UITouch.Phase.stationary } private let window: UIWindow? init(window: UIWindow?) { self.window = window } } // MARK: - private extension EventGenerator { static var callbackID = UInt32(0) func nextEventCallbackID() -> UInt32 { EventGenerator.callbackID &+= 1 return EventGenerator.callbackID } func send(_ event: IOHIDEvent) { UIKit.shared.send(event, in: window) } func eventMask(from info: Hand) -> IOHIDDigitizerEventMask { for touch in info.touches { switch touch.phase { case .began, .ended, .cancelled: return .touch case .moved, .stationary: break @unknown default: break } } return [] } func isRangeAndTouch(in info: Hand) -> DarwinBoolean { for touch in info.touches { switch touch.phase { case .began, .moved, .stationary: return true default: break } } return false } func eventMask(from info: Touch) -> IOHIDDigitizerEventMask { switch info.phase { case .began, .ended: return [ .touch, .range ] case .cancelled: return [ .touch, .range, .cancel ] case .moved: return .position case .stationary: return [] @unknown default: return [] } } func createEvent(_ info: Hand) -> IOHIDEvent { let machTime = mach_absolute_time() let touch = isRangeAndTouch(in: info) let event = IOKit.shared.hidCreateDigitizerEvent(nil, machTime, IOHIDDigitizerTransducerType.finger.rawValue, 0, 0, eventMask(from: info).rawValue, 0, 0, 0, 0, 0, 0, false, touch, 0) IOKit.shared.hidEventSetIntegerValue(event, IOHIDEventField.digitizerIsDisplayIntegrated.rawValue, 1) for info in info.touches { let subevent = IOKit.shared.hidCreateDigitizerFingerEvent(nil, machTime, nextEventCallbackID(), 2, eventMask(from: info).rawValue, info.point.x, info.point.y, 0, 0, 0, touch, touch, 0) IOKit.shared.hidEventAppend(event, subevent, 0) } return event } } // MARK: - extension EventGenerator { private enum Constants { static let fingerLiftDelay = TimeInterval(0.05) static let longPressHoldDelay = TimeInterval(2) static let multiTapInterval = TimeInterval(0.15) } func send(_ info: Hand) { let event = createEvent(info) send(event) } func touchDown(at point: CGPoint, count: Int = 1) { let touches = (0 ..< count).prefix(5).map { _ in Touch(point: point, phase: .began) } send(Hand(touches: touches, phase: .began)) } func liftUp(at point: CGPoint, count: Int = 1) { let touches = (0 ..< count).prefix(5).map { _ in Touch(point: point, phase: .ended) } send(Hand(touches: touches, phase: .ended)) } func tap(at point: CGPoint) { touchDown(at: point) DispatchQueue.main.asyncAfter(deadline: .now() + Constants.fingerLiftDelay) { self.liftUp(at: point) } } func longPress(at point: CGPoint) { touchDown(at: point) DispatchQueue.main.asyncAfter(deadline: .now() + Constants.longPressHoldDelay) { self.liftUp(at: point) } } func sendTaps(_ count: Int, at point: CGPoint, numberOfTouches: Int = 1) { func handleNext(_ remaining: Range<Int>) { tap(at: point) guard !remaining.isEmpty else { return } DispatchQueue.main.asyncAfter(deadline: .now() + Constants.multiTapInterval) { handleNext(remaining.dropFirst()) } } handleNext(0 ..< count) } }