I don’t think that I’ve mentioned this before, but WaveShaper is a mixture of openFrameworks, UIKit, and Minim for C++. One of the major difficulties I’ve had in developing the app is getting the UIKit views and controllers to play nice. It’s not hard to render UIKit widgets on top of the EAGLView, you simply add your UIKit widget as a subview of the UIWindow or the EAGLView that openFrameworks creates, but it does not work to directly use openFrameworks coordinates to set the position and size of UIKit widgets.
The reason it doesn’t work is because (I’m pretty sure) openGL’s coordinate system is not the same as Apple’s. Part of what openFrameworks does is translate the coordinates of incoming touches from Apple’s coordinate system to openGL’s before sending touch events. This allows you to always think about the drawing surface in the “natural” way for doing openGL apps: upper left corner is always (0,0), positive X is to the right, positive Y is down. You can use this knowledge to simply transform your openGL coordinates to Apple coordinates when setting the position and size of UIKit widgets, but if you are using ofxiPhoneSetOrientation to reorient your openGL surface, the mapping between openGL coordinates and UIKit coordinates changes depending on the orientation. This means you have to reposition all of your UIKit objects every time the device is reoriented. Even worse, what I discovered is that in one of the landscape modes, even though the UIKit widgets looked correct, I actually had the device orientation set to the opposite of what it should have been, so when I displayed popovers, they were upside-down. When I presented modal view controllers, they would appear in the correct orientation, but then not rotate when device orientation changed.
After much hair-pulling, I finally came up with a solution that I think is pretty solid. Essentially, what I have done is embed the EAGLView in a view hierarchy that is well behaved with respect to Apple’s system and I do not call ofxiPhoneSetOrientation to change the orientation of the app. What this means is that from openFrameworks’ perspective the app never changes orientation, which is true in some sense because the EAGLView is added as a subview to a vanilla UIView that is strictly responsible for handling the rotation.
Ok, onto some actual code. Before setting up any UI, I set the openFrameworks orientation to landscape:
|
1 |
ofxiPhoneSetOrientation( OF_ORIENTATION_90_RIGHT ); |
Then I rearrange the view hierarchy that openFrameworks creates so that I can set the position and size of UIKit widgets using openFrameworks coordinates. Included in this is creating a root view controller for the window, so that I can take advantage of the auto-rotation behavior they provide (and because Apple expects your app to have a root view controller):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// remove the glview from the UIWindow, which is where openFrameworks puts it [ofxiPhoneGetGLView() removeFromSuperview]; // create a plain old UIView that is the same size as the app // just use OF sizes as our reference, everything should work out CGRect mainFrame = CGRectMake(0, 0, ofGetWidth(), ofGetHeight()); mMainView = [[UIView alloc] initWithFrame:mainFrame]; // grab the glView and rotate/position it so it looks correct UIView* glView = ofxiPhoneGetGLView(); glView.transform = CGAffineTransformRotate(CGAffineTransformIdentity, M_PI_2); glView.center = mMainView.center; // glView goes on our main view to prevent wonky resizing [mMainView addSubview:glView]; // root controller has main view as its view mRootViewController = [[LandscapeViewController alloc] init]; mRootViewController.view = mMainView; // and finally we set the root view controller // which will make mMainView the window's primary view // all other views are then added as subviews of mMainView // NOT as subviews of the UIWindow or the EAGLView ofxiPhoneGetUIWindow().rootViewController = mRootViewController; |
Here’s the LandscapeViewController class, which limits supported orientations to landscape orientations and also turns off animations during rotation so that the orientation snaps. If you like the animated orientation change, you can simply remove those lines.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// barebones UIViewController so we can control allowed orientations of SoundCloud interface. @interface LandscapeViewController : UIViewController { } @end @implementation LandscapeViewController - (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { if ( UIInterfaceOrientationIsLandscape(toInterfaceOrientation) ) { return YES; } return NO; } - (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { // snap to new rotation [UIView setAnimationsEnabled:NO]; } - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { // animations back on so we get the nice slide-ons for the keyboard and so forth [UIView setAnimationsEnabled:YES]; } @end |
Finally, the class that registers to receive ofxiPhoneAlerts needs to handle device orientation changes like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void App::deviceOrientationChanged(int newOrientation) { UIApplication * app = [UIApplication sharedApplication]; // status bar orientations are reversed from device orientations // so that we can use OF coordinates to layout UIKit widgets. if ( newOrientation == UIDeviceOrientationLandscapeLeft ) { [app setStatusBarOrientation: UIInterfaceOrientationLandscapeRight animated:NO]; } else if ( newOrientation == UIDeviceOrientationLandscapeRight ) { [app setStatusBarOrientation: UIInterfaceOrientationLandscapeLeft animated:NO]; } } |
And again, WaveShaper only displays in landscape, which is why those are the only orientations I handle.
Once all of this is in place, you can happily position buttons, sliders, popovers, and whatever else using the openFrameworks coordinate system and they will all look correct and rotate with the screen.
Hello and thanks for those tips.
I know we can mix objective C with C++/OF, but how and where did you put these rows in your application ?
Do you have special classes for that ?
I’m using ofxUI as the GUI toolkit.
Do you know if I can also use your tips here with it ?
My App class sub-classes ofxiPhoneApp and then I do all of my implementation in an .mm file. Simply setting the file extension to .mm will make XCode treat is as Objective-C++ which means you can mix Objective-C and C++ in the same file. I’ve never used ofxUI so I’m not sure how it works, but I assume it uses the same coordinate system as openFrameworks. This probably means that if you don’t use any UIKit classes in your app, you don’t even need to use this auto-rotation trick, but it should also be the case that it will all look fine if you do and then if you add UIKit classes later, you won’t have to worry about it.
Thanks for sharing this! I was curious if this method can also work in all orientation though. From my tests, it seems to only work in landscape because when rotated to portrait, the openGL view then gets cut off. In my other app (which isn’t using this code) generally I aim for the OF view to stay the same, but the UIKit views to be able to rotate to all directions. It’s a bit tricky which is why I was wondering about your version since it seams more streamline – plus not having a rootViewController has caused some issues.
Yes, I think you are right on that. If you want to be able to support all orientations, then you probably need to tell openFrameworks when you go into portrait orientation so it will resize the openGL surface. You’d also need to relayout your elements to fit into portrait. I’m sure there is a way to do portrait so that you can still use OF coordinates to position and size the UIKit classes.
ddf, thanks for your answer (read very lately here)
I finally (almost) decided to use UIKit.
I don’t want to bother with exotic UI btw..
Thanks for this! It fixes the rotation issues I was having with my previous method of calling
[ofxIphoneGetGLView() addSubView: myViewController.view];
However, this method has introduced a touch-distribution problem. Previously, transparent areas of my view allowed touches to go through to the oF app. With this method, the area of my screen which has a subView on a higher level than the oF view seems to cancel all touches. I had to send the oF view to the back of the view order using sendSubViewToBack:glView. My view stack looks like this:
MainView
——-MenuView
————–SubMenuView
———————oF View
The touches are getting through MainView fine, but when I touch a transparent area in MenuView (which takes up the right quarter of the screen), they never make it to oF. Any ideas?
I’m pretty sure that views have a properly that allow you to set whether they should handle touches or not. It could be that your MenuView is trying to handle all touches and somehow the responder chain is messed up such that they aren’t getting passed downwards.
I double-checked all the properties I could think of for the views – they’re all set to non-opaque, user interaction enabled, multi-touch enabled, non-exclusive. Since touch events don’t naturally propagate through subviews, there must be something in the oF setup that adds the propagation and breaks when doing things this way. However, I was able to fix both of my problems with a combination of your technique and mine. I’ll write up a post about it since it’s a little long for a comment.
Pingback: Fixing Orientation Issues with OpenFrameworks and iOS — Momo the Monster
I’ve got the solution for my issue at http://mmmlabs.com/b/2012/04/fixing-orientation-issues-with-openframeworks-and-ios/
Thanks for the help!
Hi,
I’m having problems in having this situation solved! The project builds and the app is running but I get a black screen!!!
What I’ve Done.
1) Created the LandscapeViewController.h
//—————————————————————–
#import
@interface LandscapeViewController : UIViewController
{
}
@end
@implementation LandscapeViewController
- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
if ( UIInterfaceOrientationIsLandscape(toInterfaceOrientation) )
{
return YES;
}
return NO;
}
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
// snap to new rotation
[UIView setAnimationsEnabled:NO];
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
// animations back on so we get the nice slide-ons for the keyboard and so forth
[UIView setAnimationsEnabled:YES];
}
@end
//—————————————————————–
2) Added to my testApp setup() function the following code:
//—————————————————————–
#include “testApp.h”
UIView * mMainView;
UIView * glView;
#include “LandscapeViewController.h”
LandscapeViewController * mRootViewController;
#include “MyFileBrowserViewController.h”
MyFileBrowserViewController *fileBrowser;
void testApp::setup(){
// initialize the accelerometer
ofxAccelerometer.setup();
ofxiPhoneSetOrientation( OF_ORIENTATION_90_RIGHT );
[ofxiPhoneGetGLView() removeFromSuperview];
CGRect mainFrame = CGRectMake(0, 0, ofGetWidth(), ofGetHeight());
mMainView = [[UIView alloc] initWithFrame:mainFrame];
UIView* glView = ofxiPhoneGetGLView();
glView.transform = CGAffineTransformRotate(CGAffineTransformIdentity, M_PI_2);
glView.center = mMainView.center;
[mMainView addSubview:glView];
mRootViewController = [[LandscapeViewController alloc] init];
mRootViewController.view = mMainView;
ofxiPhoneGetUIWindow().rootViewController = mRootViewController;
fileBrowser = [[MyFileBrowserViewController alloc] initWithNibName : @”MyFileBrowserView”
bundle : nil];
[ofxiPhoneGetGLView() addSubview:fileBrowser.view];
myOFimage.loadImage(“UI_view1.png”);
ofSetFrameRate(60);
}
//—————————————————————–
3) Updated testApp deviceOrientationChanged function to:
//————————————————————–
void testApp::deviceOrientationChanged(int newOrientation){
UIApplication * app = [UIApplication sharedApplication];
// status bar orientations are reversed from device orientations
// so that we can use OF coordinates to layout UIKit widgets.
if ( newOrientation == UIDeviceOrientationLandscapeLeft )
{
[app setStatusBarOrientation: UIInterfaceOrientationLandscapeRight
animated:NO];
}
else if ( newOrientation == UIDeviceOrientationLandscapeRight )
{
[app setStatusBarOrientation: UIInterfaceOrientationLandscapeLeft
animated:NO];
}
}
//—————————————————————–
What I’m making wrong?
Best Regards
Oigrés
Hey Oigrés, I’m not sure, really. Things that jump out are that maybe the nib is not loading properly for your MyFileBrowserViewController and that you are adding the view owned by that same controller as a subview of the GLView. Generally speaking, I don’t think you usually want to add the view of a Controller as a subview somewhere else. Instead, you want to present MyFileBrowserViewController using the root view controller you create. But I haven’t look at this stuff in a while, so you might want to google around about it.
Hi there, after a whole day of hair-pulling too trying to make my UIView responding to orientation change after closing an iAd banner tap action, i realize that the problem had to do with OF. Finally i found your post and put on the path to solve it. For me the line:
[ofxiPhoneGetUIWindow().rootViewController addChildViewController:self];
did the trick. Added somewhere along viewDidLoad of myViewController makes my GUI to respond according to device orientation. In my case i have a fixed portrait orientation which iAd classes rotates to landscape and after closing the tapped ad the view autorotates to portrait.
Happy coding