-
-
Save Lessica/5971f3cae4722dcbbe4f2c5837a3e362 to your computer and use it in GitHub Desktop.
UIKit Touch Synthesis (Hacks! Hacks hacks! Hacks!)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment