(Failing to) Make iOS6 Remote View Controllers
I’ve been following with great interest Ole Begemann’s research into remote view controllers in iOS6. I wanted to look at the problem from the other end. I want to make my own remote view controllers. Clearly, we’ll be using private APIs and therefore, none of this can make it into your apps for the store. I’ll say up front I was not able to get this working, but I’ve found some interesting things out.
As you’d expect there are two ends to the problem. At one end a service which exports some stuff, at the other end a client. The client requests a remote view controller, from a service provider, and is given an instance of a _UIRemoteViewController which it can present. It would appear that Apple have wrapped up all the XPC heavy lifting into the _UIRemoteViewController and associated _UIViewService classes.
I think I’ve got the client end working, in as much as I can bring up Apple built in remote controllers using the following technique:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
This will bring up the standard mail composer. All good. Now let’s look at making our own service. As Ole pointed out, Apple provide hidden applications/services (via SBAppTags in the Info.plist) which are started when a remote view controller is needed. So if you run the above code you’ll see a process called MailCompositionService is started (if it’s not already there). If we look inside the app bundle for MailCompositionService you’ll see some interesting keys:
It would appear as though SBMachServices defines the name of the XPC service we are offering. The budle indetifier matches the name used in the call to [UIRemoteViewController requestViewController:connectionHandler:]. We can now create an application of our own and add these keys to our Info.plist. I created an app called TestRemote and used com.electriclabs.TestRemote as the bundle name. The app has nothing but a blank view to start with. If you start the app and look in the system console you’ll see this error:
1
|
|
More on this later… I now wanted to understand what MailCompositionService does when it’s started by iOS. To do this I used dtrace with the following program:
1 2 3 4 |
|
As you may know dtrace is very chatty, but after spending a while I could see a rough pattern of _UIViewService method calls:
- Create an intance of _UIViewServiceSessionManager
- Call startListener on the instance of UIViewServiceSessionManager
- Create an instance of _UIViewServiceXPCListener with constructor initWithName:connectionHandler:
There were other XPC calls but they appeared to sit underneath the UIViewService calls. In my TestApp I added the following to didFinishLaunchingWithOptions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Now I needed the parameters to the method call [_UIViewServiceXPCListener initWithName:connectionHandler:]. To do this I invoked the services of lldb. Get MailComposistionService started by running the code at the start and then attach lldb:
1 2 |
|
To set a breakpoint in a private method you need to get its address. You can dump the symbols as follows:
1
|
|
From the output search for ‘[_UIViewServiceXPCListener initWithName:connectionHandler’, you should see a line like this:
1
|
|
The important bit is the address - 0x00ac9ae3. We can now set a breakpoint:
1
|
|
Now bring up the mail composer again, and you should see our breakpoint is hit:
1 2 3 4 5 6 7 8 |
|
I found that there were no variables defined, using frame variable, so I had to fall back to registers. There is a great article here on how to do this. Basically on i386 you can get at self using ‘po (id)($ebp+8)’ and the first and second paramters using po (id)($ebp+16) and po (id)($ebp+20) respectively. A peek at self showed we are not quite far enough in:
1 2 |
|
We want self to be an intance of UIViewServiceXPCListener. So I stepped in a few more levels using ‘thread step-in’, until self was UIViewServiceXPCListener. I could now look at the paramters:
1 2 3 4 |
|
So no massive suprises, _UIViewServiceXPCListener expects a name which in the case of MailComposistionService is “com.apple.uikit.viewservice.com.apple.MailCompositionService” and a callback block. I’m not sure of the structure of the block, but I’m going to take a punt on it having two parameters. The first being a connection and the second an NSError.
I now extended my didFinishLaunchingWithOptions method of the service TestRemote to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
So I’m expecting that when my service is needed I’ll get a callback on my connectionHandler and from there I can create an instance of our view controller. Communication between service and host will be via XPCObjects.
I then created a simple client app called TestRemoteClient. We’ll need a _UIRemoteViewController subclass which I called ELTestRemoteViewController. For simplicity I created a simple view controller with a button which when pressed tries to create an instance of our remote view controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Unfortunately, I’ve never got this to work. I always get an error:
2012-10-08 15:15:05.357 RemoteViewControllerTest[5286:c07] Arg 1 (null), Arg 2 Error Domain=_UIViewServiceInterfaceErrorDomain Code=2 "The operation couldn’t be completed. (_UIViewServiceInterfaceErrorDomain error 2.)"
Early I said I got an error when running the service app TestRemote - ‘Ignoring info dictionary key SBMachServices since com.electriclabs.TestRemote is not a system app’. As a last ditch I tried moving TestRemote into the iOS simulator’s main Application folder. Hoping it would be blessed as a system app. Note I had to reset the simulator after moving it so it appeared. Alas, this did not work however, the error did change when running the client app:
2012-10-08 14:31:58.635 RemoteViewControllerTest[2224:c07] Arg 1 (null), Arg 2 Error Domain=NSCocoaErrorDomain Code=581952 "The operation couldn’t be completed. (Cocoa error 581952.)"
So there we are. I’m a bit stuck. Clearly, there are things missing. I don’t know where I am supposed to tell UIKit the name of my view controller on the service side, or the host interface. I expect i’d have to this in the connection handler block like thus:
1 2 3 4 5 6 7 8 9 10 11 |
|
I’d love to get this working.