Skip to content

Instantly share code, notes, and snippets.

@zwaldowski
Created April 26, 2019 05:35
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save zwaldowski/ca33e586306e4e248b278da70c568e1c to your computer and use it in GitHub Desktop.
Save zwaldowski/ca33e586306e4e248b278da70c568e1c to your computer and use it in GitHub Desktop.
UIKit Touch Synthesis (Hacks! Hacks hacks! Hacks!)
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)
}
}
@phillyboii72
Copy link

whats this for

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment