3

I want to make use of a private framework on iOS from a Swift app. I am well aware why this is a bad idea, but it will be useful during testing, so App Store limitations are not an issue, and I have quite exhaustively looked into alternatives.

What I would like to do is something along the lines of this:

dlopen("/Developer/Library/PrivateFrameworks/UIAutomation.framework/UIAutomation".fileSystemRepresentation,RTLD_LOCAL);

let eventsclass = NSClassFromString("UIASyntheticEvents") as? UIASyntheticEvents.Type
eventGenerator = eventsclass!.sharedEventGenerator() as! UIASyntheticEvents

The problem is that this will cause a linker error:

Undefined symbols for architecture x86_64:
  "_OBJC_CLASS_$_UIASyntheticEvents", referenced from:
      type metadata accessor for __ObjC.UIASyntheticEvents in Test.o

If I link the framework directly rather than use dlopen(), it will work fine on the simulator, but will fail to build for an actual device, as the framework supplied in the SDK is for the simulator only and will not link during a device build.

I have tried doing these calls in Objective-C instead, in case it was just the .Type cast that was causing problems, but this does not help. If I access the returned object from Swift, I still get the same error.

I suppose I could create a wrapper in Objective-C that just passes all the calls to the class straight through, but this seems excessive. Is there a more elegant solution to this problem?

Dag Ågren
  • 1,064
  • 7
  • 18
  • No Swift linkage against static frameworks. – matt Mar 21 '16 at 17:07
  • I'm not sure what you mean here, there is no static framework involved. – Dag Ågren Mar 21 '16 at 22:07
  • OK, just checking. – matt Mar 21 '16 at 23:56
  • I am absolutely not an expert on frameworks (in fact I sort of hate them) but from what I can gather, it sounds like your Objective-C wrapper is what you're going to end up doing. See this discussion: http://stackoverflow.com/questions/35328255/expose-an-interface-of-a-class-loaded-from-a-framework-at-runtime – matt Mar 21 '16 at 23:58
  • Actually, that article contains some very good hints for how to fix this properly without Objective-C. That's actually very helpful! I'll try to write up a proper answer to the question when I've worked through the details. – Dag Ågren Mar 22 '16 at 12:40

1 Answers1

5

I found one solution which requires some manual work, but does work in pure Swift.

The trick is to create an @objc protocol in Swift which matches the Objective-C methods, then do an unsafe cast to that protocol type. In my case, the protocol looks like this:

@objc protocol UIASyntheticEvents {
    static func sharedEventGenerator() -> UIASyntheticEvents

    //@property(readonly) struct __IOHIDEventSystemClient *ioSystemClient; // @synthesize ioSystemClient=_ioSystemClient;
    var voiceOverStyleTouchEventsEnabled: Bool { get set }
    var activePointCount: UInt64 { get set }
    //@property(nonatomic) CDStruct_3eca2549 *activePoints; // @synthesize activePoints=_activePoints;
    var gsScreenScale: Double { get set }
    var gsScreenSize: CGSize { get set }
    var screenSize: CGSize { get set }
    var screen: UIScreen { get set }
    var onScreenRect: CGRect { get set }

    func sendPinchCloseWithStartPoint(_: CGPoint, endPoint: CGPoint, duration: Double, inRect: CGRect)
    func sendPinchOpenWithStartPoint(_: CGPoint, endPoint: CGPoint, duration: Double, inRect: CGRect)
    func sendDragWithStartPoint(_: CGPoint, endPoint: CGPoint, duration: Double, withFlick: Bool, inRect: CGRect)
    func sendRotate(_: CGPoint, withRadius: Double, rotation: Double, duration: Double, touchCount: UInt64)
    func sendMultifingerDragWithPointArray(_: UnsafePointer<CGPoint>, numPoints: Int32, duration: Double, numFingers: Int32)
    func sendPinchCloseWithStartPoint(_: CGPoint, endPoint: CGPoint, duration: Double)
    func sendPinchOpenWithStartPoint(_: CGPoint, endPoint: CGPoint, duration: Double)
    func sendFlickWithStartPoint(_: CGPoint, endPoint: CGPoint, duration: Double)
    func sendDragWithStartPoint(_: CGPoint, endPoint: CGPoint, duration: Double)
    func sendTaps(_: Int, location: CGPoint, withNumberOfTouches: Int, inRect: CGRect)
    func sendDoubleFingerTap(_: CGPoint)
    func sendDoubleTap(_: CGPoint)
    func _sendTap(_: CGPoint, withPressure: Double)
    func sendTap(_: CGPoint)
    func _setMajorRadiusForAllPoints(_: Double)
    func _setPressureForAllPoints(_: Double)
    func moveToPoints(_: UnsafePointer<CGPoint>, touchCount: UInt64, duration: Double)
    func _moveLastTouchPoint(_: CGPoint)
    func liftUp(_: CGPoint)
    func liftUp(_: CGPoint, touchCount: UInt64)
    func liftUpAtPoints(_: UnsafePointer<CGPoint>, touchCount: UInt64)
    func touchDown(_: CGPoint)
    func touchDown(_: CGPoint, touchCount: UInt64)
    func touchDownAtPoints(_: UnsafePointer<CGPoint>, touchCount: UInt64)
    func shake()
    func setRinger(_: Bool)
    func holdVolumeDown(_: Double)
    func clickVolumeDown()
    func holdVolumeUp(_: Double)
    func clickVolumeUp()
    func holdLock(_: Double)
    func clickLock()
    func lockDevice()
    func holdMenu(_: Double)
    func clickMenu()
    func _sendSimpleEvent(_: Int)
    func setOrientation(_: Int32)
    func sendAccelerometerX(_: Double, Y: Double, Z: Double, duration: Double)
    func sendAccelerometerX(_: Double, Y: Double, Z: Double)
    func _updateTouchPoints(_: UnsafePointer<CGPoint>, count: UInt64)
    func _sendHIDVendorDefinedEvent(_: UInt32, usage: UInt32, data: UnsafePointer<UInt8>, dataLength: UInt32) -> Bool
    func _sendHIDScrollEventX(_: Double, Y: Double, Z: Double) -> Bool
    func _sendHIDKeyboardEventPage(_: UInt32, usage: UInt32, duration: Double) -> Bool
    //- (_Bool)_sendHIDEvent:(struct __IOHIDEvent *)arg1;
    //- (struct __IOHIDEvent *)_UIACreateIOHIDEventType:(unsigned int)arg1;    func _isEdgePoint(_: CGPoint) -> Bool
    func _normalizePoint(_: CGPoint) -> CGPoint
    //- (void)dealloc;
    func _initScreenProperties()
    //- (id)init;
}

This was converted by hand from a class-dump output. If anyone knows a quicker way to do it, I'd love to know.

Once you have this protocol, you can simply do the following:

    dlopen("/Developer/Library/PrivateFrameworks/UIAutomation.framework/UIAutomation".fileSystemRepresentation,RTLD_LOCAL)

    let eventsclass = unsafeBitCast(NSClassFromString("UIASyntheticEvents"), UIASyntheticEvents.Type.self)
    eventGenerator = eventsclass.sharedEventGenerator()
Dag Ågren
  • 1,064
  • 7
  • 18