Friday 11 March 2011

Binding to Objective-C from MonoTouch

MonoTouch is a managed layer that binds onto the Objective-C CocoaTouch libraries using a binding layer. There’s a Mono tool called btouch that will generate a binding layer (or at least do much of the work) of binding an Objective-C API.

In a circuitous way, I recently got vaguely intimate with the MonoTouch binding mechanism.

A few weeks ago I was working on a MonoTouch iPhone sample and came across a need that I couldn’t find a way to satisfy.

I was calling a web service method passing in the data for a bunch of points on a graph, some text and dimensions of an image. The web service rendered the graph into an image of the specified size with legend data as passed in and returned a URL to the generated image, which was to be displayed full screen.

The text on the chart forced its orientation and since by default an iPhone is held so the screen has a portrait aspect, the chart was sized for portrait display. However… the chart data didn’t really work well in a portrait oriented chart. It worked better when drawn on an image that was wide and short, rather than narrow and tall.

In other words, if the user rotated the phone so it was in landscape mode, then I could request a landscape oriented image, and displaying that would look sensible with the text written across the landscape chart.

However, there’s a problem here, because the user wouldn’t be rotating the phone ahead of my chart request. So I thought maybe I could force the phone’s display to rotate as if the user had done it, then when they saw the chart with the text at 90 degrees, they’d be forced to rotate it and could see the landscape oriented chart as it was intended.

The trouble is, forcing a rotation appears not to be allowed.

To clarify, here’s what you can do. You are allowed to specify up front what orientation your program should start up in by setting an entry in your info.plist file (Angry Birds, for example, starts up in landscape). You can also respond to the user rotating the device mid-program by overriding ShouldAutorotateToInterfaceOrientation(). If you want you can pop up a UIAlertView and tell the user to rotate the device at any point.

But you can’t force a new view to be a different orientation part way through a program.

Not so far as I could find.

Not officially, anyway.

After some searching I bumped into comment 7 on this thread, which explains that while UIDevice’s orientation property is documented to be read-only, you can in fact write to it with something like this Objective-C statement (which should be on one line were it not for the narrow width of this blog text area).

[[UIDevice currentDevice] setOrientation:
    UIDeviceOrientationLandscapeLeft];

Of course, Objective-C is all fine and dandy, but of little use when coding with MonoTouch, but we’ll come back to that in a bit.

This statement works fine in the simulator. You can pass whatever orientation to it you like and the screen will rotate as you dictate.

However it is using a private setter method, one which is not documented, and so would be immediately rejected if you submitted an app containing this to the Apple AppStore.

So, you can use this sort of hackery while testing, but best to kill it off quickly and find a better (documented) way of doing things.

If you try and write the equivalent statement in C# in a MonoTouch app:

UIDevice.CurrentDevice.Orientation =
    UIInterfaceOrientation.LandscapeLeft;

it will fail to compile. This is because the MonoTouch library has been generated from all the documented iOS library content.

But given that setOrientation clearly exists, private as it maybe, there is a way of getting at it. And I followed the trail to find out how to access it, to get a handle on how binding operates from C# to Objective-C. if it comes to pass that i want to use another Objective-C library then I’ll need to be familiar with binding from one to the other.

Fortunately there is a good amount of information on the binding layer, which is implemented in the MonoTouch.ObjCRuntime namespace. The mechanism to call from C# to Objective-C is designed to mimic the internal Objective-C messaging mechanism. Objective-C uses a function called obj_msgSend() to send a message (the name of a method) to some receiving object. The message name is referred to as a selector.

The MonoTouch.ObjCRuntime.Selector class allows you to map onto any target method name, so long as you use the correct Objective-C form, including all the appropriate colon suffix characters.

The MonoTouch.ObjCRuntime.Messaging class has many overloaded versions of various methods that do the same job as obj_msgSend() in Objective-C. You look at the required input parameters and return type to choose an appropriate one. In this case, setOrientation: does not return anything and takes one integer sized input parameter and so void_objc_msgSend_int() is the one we need.

using MonoTouch.ObjCRuntime;
...
Selector orientationSetter;
...
private void SetOrientation (UIInterfaceOrientation toOrientation)
{
    if (orientationSetter == null)
        orientationSetter = new Selector ("setOrientation:");
    Messaging.void_objc_msgSend_int (UIDevice.CurrentDevice.Handle,
        orientationSetter.Handle, (int)toOrientation);
}

Now you can switch orientations at will in the iPhone Simulator, or on a test deployed app.

But of course screen rotation is not the real point underlying this story. The summary is that binding to any Objective-C types and methods is feasible with the MonoTouch binding layer.

You should also be aware of the MonoTouch btouch tool that helps do most of the work of binding an Objective-C API. More information on the MonoTouch API design, binding Objective-C types, other binding details and selector details can be found in the MonoTouch documentation.

No comments:

Post a Comment