If you haven’t already read @olebegemann’s blog post about _UIRemoteView’s and remote view controllers new in iOS 6 then please do so now and come back to this after you have finished it.
http://oleb.net/blog/2012/10/remote-view-controllers-in-ios-6/
When I first read this post about iOS 6 and _UIRemoteView’s which allowed apple to move things like email into different processes and still display them as if they where in the app there was one point I that caught my attention more then it would most peoples.
<_UIRemoteView: 0x1e05c300; frame = (0 0; 320 480); transform = [0.5, -0, 0, 0.5, -0, 0]; userInteractionEnabled = NO; layer = <CALayerHost: 0x1e05c460>>
CALayerHost, the instant I sure that I thought, “wow, Apple finally put it to good use’. From my experiments with CardSwitcher I have come across CALayerHost’s before. They are actually how CardSwitcher’s live views work. More then displaying part of an app, however, they also allow interaction through to that app (although that is buggy iOS 5, haven’t tested 6 yet). For CardSwitcher to use them as live views into apps it actually has to block interaction, if you remove that you get this http://www.youtube.com/watch?v=1D1NyXITjI0.
An individual CALayerHost will display 1 UIWindow so to display an entire app you need to have multiple CALayerHost objects and keep them in track with the apps windows, if it creates one so do you. This is a lot of, very simple, but, effectively all, boiler plate code. And so, in SpringBoard there is a class called SBAppContextHostView which houses them all as sublayers and lets you just deal with one object. (actually tracking the contexts used to be done by it itself but is now done by a separate SBAppContextHostManager object in iOS 5+). This is used for things such as the zoom effect when you close an app, and the when you open the app switcher and it slides the "app” up (in reality the context view is activated and moved instead).
But, despite the the fact it is only used by SpringBoard it is actually part of QuartzCore, which all apps link against. From my experiments I believe they have changed it's behaviour slightly in iOS 6 (to get this working well) but it should still be possible to replicate on iOS 5.
iOS 5 does have XPC I believe (it has a private XPCObjects.framework) but I haven’t used it before and as I was trying to hack this together quickly didn’t use it. Another thing I would do if writing this properly is what apple has done, create a proper UIRemoteViewController to handle it all and create a few protocols and helper objects either side.
To build my example app I used CPDistributedMessagingCenter to communicate between different processes. The different parts involved in my test setup are:
- a library which houses my KHRemoteView (so multiple apps potentially link against it);
- the client app, this displays the remote view after 5 seconds (so you see a change);
- the server app, this is what is displayed inside the other app;
- a tweak running inside SpringBoard, to launch the app;
A few more notes on CALayerHost, it doesn’t respect…. most things. If you set it’s frame it’ll take into account it’s position but ignore the set size. It will always display full size. It ignores contentSize and displays 1 pixel per point (massive version of the target app). One of the few things it does respect though is it’s transform, so you can use this to scale it done by the devices contentScale.
self.layerHost = [[objc_getClass("CALayerHost") alloc] init];
self.layerHost.anchorPoint = CGPointMake(0,0);
self.layerHost.transform = CATransform3DMakeScale(1/[UIScreen mainScreen].scale, 1/[UIScreen mainScreen].scale, 1);
self.layerHost.bounds = self.bounds;
Another annoying thing is that it seems to fight with SpringBoard. All windows have a corresponding SBContext and CALayerHost in SpringBoard. What I think is happening is that when we set the contextId for the layerHost it becomes the host layer for that context. However the SpringBoard one then takes back control of the context the next render, possibly because SB is the render server (below iOS 6) and it is automatically given priority or it updates each pump of the runloop. Even though it loses the link with the context very quickly it still sends it touch events, you just don’t see the results. To get around this I set the contextId to 0 and then instantly back to what it was 60 times a second (the devices screen refresh rate).
//... KHRemoteView.m
-(void)updateDisplay{
if (update) {
unsigned int contextID = self.layerHost.contextId;
self.layerHost.contextId = 0;
self.layerHost.contextId = contextID;
[self performSelector:@selector(updateDisplay) withObject:nil afterDelay:(1/60)];
}
}
//...
In my demo server (Service?) app I have it display an MFMailComposeViewController (as a reference back to the original article). A few odd things with this are due to my using only one CALayerHost (I should use 2, or possible 3 for you not to notice anything. 1 for the app, one for the UITextEffectsWindow (keyboard) and another for alerts/action sheets). The copy & paste menu is in a separate UIWindow and so is not shown when you select text, but the text range selection blue bars are. The keyboard, alerts and action sheets and not that but you can still type and interact with them. The layers send touch events through not to the window but to the app itself, so if you can type without looking on iOS you should still be able to write an email with this app.
// Send messages to launch the server app and get the servers contextId...
[[objc_getClass("CPDistributedMessagingCenter") centerNamed:@"com.iky1e.remoteView.sb.messaging.center"] sendMessageAndReceiveReplyName:@"kh_launch_remote_app" userInfo:nil];
// Lazy loading the library (makefiles aren't my strong point)...
dlopen("/usr/lib/LibRemoteView.dylib", RTLD_NOW);
The server itself is very basic, it just displays an MFMailComposeViewController. The original one had a UIButton and a view that animated up and down, but I switched to this to show a more interactive example.
Here’s a video of the basic demo application. You can see some of the weird effects, typing without a keyboard for example.
Finally here is the code: it includes the tweak running in SpringBoard, the client app, the server app, and the library that the client links against to access the KHRemoteView class. Have a look and tell me what you think, here on HackerNews or on twitter?