iOS: AutoRotating between NIBs - iphone

My universal app is a single full screen view. Pressing a button flips to reveal a settings page:
- (void) showSettings
{
FlipsideViewController * flipsideVC = [FlipsideViewController alloc];
NSString * settingsNib;
if ( isIPad() )
settingsNib = isCurrentlyPortrait() ? #"settings_iPad_portrait" : #"settings_iPad_landscape";
else
settingsNib = #"settings_iPhone";
[flipsideVC initWithNibName: settingsNib
bundle: nil ];
flipsideVC.delegatePointingToMainVC = self;
flipsideVC.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController: flipsideVC
animated: YES ];
[flipsideVC release];
}
and the settings page invokes the delegate method: I recreate the main view in light of the changed settings, and flip back.
- (void) settingsDidQuit:(FlipsideViewController *) flipsideVC
{
[self createOrRecreateWheelView];
[self dismissModalViewControllerAnimated: YES];
}
But what if the user rotates the iPad on the settings page? Apple decrees that my app must handle this. But how to do this? can I dynamically load a new XIB for the settings page?
I can't see a way to do that, so my attempted solution is to catch the rotation within the settings view, ...
- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation) oldInterfaceOrientation
{
[self.delegatePointingToMainVC settingsOrientationChanged];
}
...and call back to the main view controller, which dissolves the settings view controller and recreates it in light of the current orientation.
- (void) settingsOrientationChanged
{
[self dismissModalViewControllerAnimated: YES];
[self showSettings];
}
There is a trivial problem straight away -- didRotateFromInterfaceOrientation gets triggered automatically when the settings page loads. I can prevent this by setting a boolean to false in init, and modifying thus:
- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation) oldInterfaceOrientation
{
if (initialized)
[self.delegatePointingToMainVC settingsOrientationChanged];
initialized = true;
}
problem with this approach is that I navigate to the settings page, rotate the device, and it momentarily shows the correct settings page, before flicking back to my main view.
I think there is a threading problem here. But maybe my whole approach is wrong. Can somebody suggest a better solution?

I'm not sure I understand the problem. You want the settings view (loaded from NIB) to autorotate? You should just return YES for the orientation you want the autorotation to be performed in the shouldAutorotateToInterfaceOrientation: and set the autoresizing mask of the views inside the XIB accordingly to your needs.
There's no need to call back the main view controller and tell him to push a new settings view controller. The rotation behavior of the views is determined by the autoresizing mask properties of each view and the implementation of shouldAutorotateToInterfaceOrientation: method of the associated view controller and just that. If want to do more advanced animations, though, you can set up and manage them in the willRotateToInterfaceOrientation:duration: and didRotateFromInterfaceOrientation: methods.

Related

iOS7 / iOS6 Conditional Rotation Portrait / Landscape for different sections of App

Problem: A have an App that uses both Landscape mode (locked) and Portrait Mode (locked) for different parts of the app. Now I have a working solution however it doesn't seem correct and does have it's own problems.
Optimally I would love to force a orientation change. Thinking even about doing a view transformation if needed.
Basic flow of App:
HomeView (Portrait) (which has a few sub pushed views that are also portrait and locked to that).
LandscapeView (Landscape) (which has 5 pushed subviews that are also landscape)
Note:
HomeView has a link to LandscapeView
LandscapeView can go back to HomeView
At the end of the LandscapeView subviews it returns to the HomeView
Basic Image showing how this looks with the different view orientations. (The lines indicate flow of app, orientation of the images indicate how each screen should be )
Currently using the below implementation to call / set if the view is in portrait mode or landscape mode by [setLockedToPortait:YES] (for portrait view) etc.
This in term makes the query for what interface orientation to use from iOS if the device is rotated.
Now for the case of going to the LandscapeView, I show a temporary view over the top of the normal view asking to use to rotate their phone to landscape. (A temporary view is also shown when returning to the HomeView from a landscape view)
So once the user has rotated their device, it will trigger the correct orientation and then the temporary view will hide.
If the user then rotates their phone back to portrait at this point it will still be locked to landscape so will not trigger another view rotation (also no temp view will appear or anything)
Current Implementation Code::
// ---------------------- NavigationController (subclass of UINavigationController)
#interface NavigationController () {
BOOL isOrientationPortrait;
}
#end
#implementation NavigationController {
UIDeviceOrientation lastAccepted;
UIDeviceOrientation lastKnown;
}
-(void)setLockedToPortait:(BOOL)isLocked {
isOrientationPortrait = isLocked;
}
-(UIDeviceOrientation) getCurrentOrientation {
UIDeviceOrientation orientate = [[UIDevice currentDevice] orientation];
if(orientate == 0) { // needed for simulator
orientate = (UIDeviceOrientation)[UIApplication sharedApplication].statusBarOrientation;
}
return orientate;
}
// Deprecated in iOS6, still needed for iOS5 support.
- (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)toInterfaceOrientation
{
UIDeviceOrientation orientation = [self getCurrentOrientation];
[self setLastKnownOrientation:orientation];
if(isOrientationPortrait == YES) {
if([self isLastKnownPortrait] == YES) {
[self setLastAcceptedOrientation:orientation];
return YES;
} else {
return NO;
}
} else {
if([self isLastKnownLandscape] == YES) {
[self setLastAcceptedOrientation:orientation];
return YES;
} else {
return NO;
}
}
}
// iOS6/7 support
- (BOOL)shouldAutorotate
{
// find out the current device orientation
UIDeviceOrientation orientation = [self getCurrentOrientation];
[self setLastKnownOrientation:orientation];
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
if(isOrientationPortrait == YES) {
if([self isLastKnownPortrait] == YES)
{
UIDeviceOrientation orientation = [self getCurrentOrientation];
[self setLastAcceptedOrientation:orientation];
}
return (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown);
} else {
if([self isLastKnownLandscape] == YES)
{
UIDeviceOrientation orientation = [self getCurrentOrientation];
[self setLastAcceptedOrientation:orientation];
}
return (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight );
}
}
-(void)setLastAcceptedOrientation:(UIDeviceOrientation)orient {
lastAccepted = orient;
}
-(void)setLastKnownOrientation:(UIDeviceOrientation)orient {
lastKnown = orient;
}
-(BOOL)isLastKnownPortrait {
return UIDeviceOrientationIsPortrait(lastKnown);
}
-(BOOL)isLastKnownLandscape {
return UIDeviceOrientationIsLandscape(lastKnown);
}
-(BOOL)isLastAcceptedPortrait {
return UIDeviceOrientationIsPortrait(lastAccepted);
}
-(BOOL)isLastAcceptedLandscape {
return UIDeviceOrientationIsLandscape(lastAccepted);
}
Current Problems:
Device rotations are always required after a view has loaded for the user going to Landscape mode from Portrait and vice versa.
If the user has the device orientation locked, this will not work at all.
When transitioning back from Landscape mode, and the user has already rotated their device to Portrait (in the last landscape view), the Portrait view's interface will be locked to a 'Landscape' layout until the user re-rotates their device (so currently I am just showing the overlay to rotate the device, but it is already rotated… very annoying for the user). Massive issue right now with the above implementation.
Would love to be able to:
Force an orientation change on the phone for the current view.
Set a preferred layout for a view which is forced between push/pops of views.
I've looked a lot at the other solutions on here and on the Apple Dev forums, however none seem to cover this problem, or still this orientation bug between the two views exists as well.
Thanks for any help or pointers! No advice will be discounted :D
--
Edit::
Solution Found thanks to #leo-natan!!
So instead of trying to force a change of orientation on the views. Just push a new modal view. This forces a change. You still need to above orientation code for managing rotations.
So what I have now in my HomeViewController:
LandscapeViewController * viewController = [[[LandscapeViewController ViewController alloc] init] autorelease];
UINib * nib = [UINib nibWithNibName:#"NavigationController" bundle:nil];
NavigationController *navController = [[nib instantiateWithOwner:nil options:nil] objectAtIndex:0];
[navController initWithRootViewController:viewController];
[self presentViewController:navController animated:YES completion:^{
// completion
}];
So it is necessary to re-add a new navigation controller for this modal view. Also note above 'presentViewController' is the new way of pushing Modal views.
Implemented this overloaded method for the managing of the view controller:
-(id)initWithRootViewController:(UIViewController *)rootViewController {
self = [super initWithRootViewController:rootViewController];
if(self){
}
return self;
}
Note: The above is not using storyboards. The problem may be solved by using storyboards and modally showing a view in the same fashion.
See my answer here, including a test project.
Basically, orientation can only be forced to change when presenting a view controller modally. For example, media playback in some apps. If you wish to transition from a view controller that can only be presented in portrait to a view controller that is only presented in landscape, you will need a modal presentation. Push will not work.

understanding orientation in tabBar application iOS 4.2

I am currently working on a tabBar application. My tabBar controller is the rootViewController and I have 3 tabs. Each tab will have several UIWebView's on them which I would like to be able to support orientation once the webview has loaded. Once the webview has closed then orientation is no longer supported. How can I tell my other viewControllers to handle orientation instead of the rootViewController?
I hope I explained this to where it's clear.
As always TIA,
T
Ok this is a shot in the dark - This might not be what you are asking.
To support multiple orientations you need to do 2 things.
Check the orientation when the ViewController is first loaded.
Respond to orientation changed notifications / override orientation changed methods.
Here is how I do it:
- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
// Call my method to handle orientations
[self layoutViewsForOrientation:toInterfaceOrientation];
return YES;
}
- (void) viewWillAppear:(BOOL)animated
{
// ...
// Some code .....
// ...
// Call my method to handle orientations
[self layoutViewsForOrientation:self.interfaceOrientation];
}
- (void) layoutViewsForOrientation:(UIInterfaceOrientation) orientation
{
UIInterfaceOrientationIsLandscape(orientation) {
// Move views about if needed
} else {
// ....
}
}

UIViewController does not auto rotate

As the title says. My UIViewController will not rotate no matter what. When it loads shouldAutorotateToInterfaceOrientation is being called but after that it doesnt.
UPDATE 1:
It's a really really wierd problem. At least for me. And i ll try to explain everything.
It's a navigation based app. Every controller has
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{
return YES;
}
Xcontroller is a child of Acontroller and it doesn't auto rotate. If Xcontroller become a child of Bcontroller then it will autorotate. So something is wrong with Acontroller. But Acontroller is identical (except its data) to Bcontroller.
Whats Wrong?
UPDATE 2:
I decided to recreate Acontroller. And it worked.I believe I was missing something stupid.
I am not sure whether it's the same reason as your case. But I experienced the same thing. the shouldAutorotateToInterfaceOrientation was only called once in the beginning.
After some serious debugging by taking code apart, I found that the reason is in my overridden init method.
I had this before:
- (id)initWithAlbum:(PhotoAlbum *)theAlbum {
if (self) {
self.photoAlbum = theAlbum;
}
return self;
}
And then I changed to this
- (id)initWithAlbum:(PhotoAlbum *)theAlbum {
if (self = [super init]) {
self.photoAlbum = theAlbum;
}
return self;
}
Note: the only difference is I added [super init] to call the parent init.
After this change, the rotation works well and the shouldAutorotateToInterfaceOrientation is being called everytime I rotate the screen.
Hope this help.
There can be several possible reasons your view controller does not rotate.
See Apple's official Q&A on this issue:
Why won't my UIViewController rotate with the device?
http://developer.apple.com/library/ios/#qa/qa2010/qa1688.html
Apple Q&A has the detailed solution for the problem.
Why won't my UIViewController rotate with the device?
http://developer.apple.com/library/ios/#qa/qa1688/_index.html
If you add a viewcontroller.view to uiwindow, you should set this viewcontroller as rootviewcontroller.
[self.window addSubview: mainViewcontroller.view];
self.window.rootViewController=mainViewcontroller;
Also, make sure you don't have rotation lock on. I spent a good hour trying to figure out why my views stopped rotating. shouldAutorotateToInterfaceOrientation was being called only once at start up and when Game Center leaderboards/achievements were presented.
I had the same issue - the reason was, that it was my first UIViewController, that i created on the fly in my ApplicationDelegate, added it's View to my UIWindow and immediately released it.
That's of course not correct as I just added the UIView of the UIViewController (retaining it) and than released the whole controller.
You should add your first UIViewController as an instance variable in Your ApplicationDelegate instead, and release it in Your ApplicationDelegate's dealloc-method.
In my case, the ViewController was inside a NavigationController which was used by a "parent" viewControlled that received the orientation changes.
What I did in this parent was:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation{
if(_navigationController){
return [_navigationController.topViewController shouldAutorotateToInterfaceOrientation: toInterfaceOrientation];
}
return toInterfaceOrientation == UIInterfaceOrientationPortrait;
}
This way you can implement your own orientation change logic depending on the currently visible controller.
(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{ return YES; }
The above method if u using, you will able to call many time if u want with out any error.
I think there is no strange behavior here, it is called only one which is right. There is no need to call more than one to decide if the device should rotate to a direction or not.
This method just ask if the device should rotate to a direction or not.
If you want to handle the orientation change, you should register for the notification from the UIDeviceDidChangeOrientationNotification and override the following method:
- (void)orientationChanged:(NSNotification *)notification
{
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
if (UIDeviceOrientationIsLandscape(deviceOrientation) &&
!isShowingLandscapeView)
{
[self presentModalViewController:self.landscapeViewController
animated:YES];
isShowingLandscapeView = YES;
}
else if (deviceOrientation == UIDeviceOrientationPortrait &&
isShowingLandscapeView)
{
[self dismissModalViewControllerAnimated:YES];
isShowingLandscapeView = NO;
}
}
See more here.
I have the same problem but with two view controllers added to the application's UIWindow. The reason
is The view controller's UIView property is embedded inside UIWindow but alongside an additional view controller
From Apple Technical Q&A
http://developer.apple.com/library/ios/#qa/qa1688/_index.html

Detecting when camera's iris is open on iPhone

For a cutom camera overlay I need to find out, when the iris is opened, because my overlay will allways shown while the iris is close (and then animating to open).
Any ideas ?
You can listen for the PLCameraViewIrisAnimationDidEndNotification notification. Since this is not officially documented, you might be in violation of the Apple TOS, but I think so long as you write your code so that it's defensive against the possibility that the name or contract of this notification might change (so in the future you might not get the event) you'll probably be ok. In other words, use a timer or other technique to ensure that the thing you want done when the iris is open will definitely happen eventually even if you never get the notification...
Trivial example without the defensive programming. (Of course, you can register an interest only for this specific notification as well, see the docs for the notification center.)
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(notificationCallback:)
name:nil
object:nil
];
- (void) notificationCallback:(NSNotification *) notification {
if ([[notification name] isEqualToString:#"PLCameraViewIrisAnimationDidEndNotification"]) {
NSLog(#"Iris open");
// we don't need to listen any more
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
}
It seems that PLCameraViewIrisAnimationDidEndNotification no longer gets notified in iOS5.
I can't figure out what is a suitable solution when the iris has finished opening, there must another option rather than using a 3 second timer.
Check here: https://devforums.apple.com/message/561008#561008
I have a ViewController (ALImagePickerController) which holds, initializes and presents the UIImagePickerController as a child view controller (I have another child view controller for presenting the taken image which is not shown here) and I present (as a modal) the ALImagePickerController when I want to use the camera. So during this the viewDidAppear of the ViewContoller I add an animation to bring in the camera overlay gracefully as the shutter animation disappears.
#interface ALImagePickerController ()
#property (nonatomic) UIImagePickerController *cameraController;
#property (nonatomic) CameraOverlayView *overlayView;
....
#end
#implementation ALImagePickerController
....
- (void)viewDidLoad {
[super viewDidLoad];
[UIApplication sharedApplication].statusBarHidden = YES;
self.cameraController = [UIImagePickerController new];
self.cameraController.sourceType = UIImagePickerControllerSourceTypeCamera;
self.cameraController.delegate = self;
self.cameraController.allowsEditing = NO;
self.cameraController.showsCameraControls = NO;
....
self.overlayView = [CameraOverlayView new];
....
self.overlayView.alpha = 0;
self.cameraController.cameraOverlayView = self.overlayView;
....
// add as child view controller
[self addChildViewController:self.cameraController];
[self.view addSubview:self.cameraController.view];
[self.cameraController didMoveToParentViewController:self];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[UIApplication sharedApplication].statusBarHidden = NO;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// smoothly bring in the overlay as the native camera shutter animation opens.
[UIView animateWithDuration:0.2 delay:0.3 options:UIViewAnimationCurveEaseOut animations:^{
self.overlayView.alpha = 1.f;
} completion:nil];
}
....
#end
The way I solved this problem is I initialize all the elements with the hidden property set to YES, then call a 3-second delayed selector after I call the camera, where I set all the elements to hidden = NO. It's not an ideal solution but it seems to work, and any lag after the iris is opened is negligible.
You should already know when the camera is ready to take a picture. At least the way I use a custom camera overlay, I init the view with something like self.sourceType = UIImagePickerControllerSourceTypeCamera; and the other usual setup, and the camera is ready (or "iris is open") at that point.
In summary, if one is using a custom camera overlay the way I am used to using it, one will know when the iris is open because it is under your control.

iPhone - Splash Screen with progress bar

I tried to create a SplashView which display the Default.png in the background and a UIProgressBar in front. But the splash screen is not being updated...
Inside my view controller I load first the splash view with a parameter how many steps my initialisation has and then I start a second thread via NSTimer and after each initialisation step I tell the SplashView to display the new progress value.
All looks good in theory, but when running this app the progress bar is not being updated (the method of the splash screen receives the values, I can see it in the logs). I also tried to add usleep(10000); in between to give the view updates a bit time and also instead of using the progress bar I drew directly on the view and called [self setNeedsDisplay]; but all didn't work :/
What am I doing wrong?
Thanks for your help!
Tom
Here is some code:
SPLASHSCREEN:
- (id)initWithFrame:(CGRect)frame withStepCount:(int)stepCount {
if (self = [super initWithFrame:frame]) {
// Initialization code
background = [[UIImageView alloc] initWithFrame: [self bounds]];
[background setImage: [UIImage imageWithContentsOfFile: [NSString stringWithFormat:#"%#/%#", [[NSBundle mainBundle] resourcePath], #"Default.png"]]];
[self addSubview: background];
progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar];
[progressView setFrame:CGRectMake(60.0f, 222.0f, 200.0f, 20.0f)];
[progressView setProgress: 0.0f];
stepValue = 1.0f / (float)stepCount;
[self addSubview:progressView];
}
return self;
}
- (void)tick {
value += stepValue;
[progressView setProgress: value];
}
VIEWCONTROLLER:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
splashView = [[SplashView alloc] initWithFrame: CGRectMake(0.0f, 0.0f, 320.0f, 480.0f) withStepCount:9];
[self setView: splashView];
NSTimer* delayTimer;
delayTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(finishInitialization) userInfo:nil repeats:NO];
}
return self;
}
- (void)finishInitialization {
// do some stuff, like allocation, opening a db, creating views, heavy stuff...
[splashView tick]; // this should update the progress bar...
// do some stuff, like allocation, opening a db, creating views, heavy stuff...
[splashView tick]; // this should update the progress bar...
// init done... set the right view and release the SplashView
}
As mentioned in another answer, for some finite amount of time, as your app is being launched, Default.png is displayed and you have no control over it. However, if in your AppDelegate, you create a new view that displays the same Default.png, you can create a seamless transition from the original Default.png to a view that you can add a progress bar to.
Now, presumably, you have created a view or similar and you are updating a progress bar every so often in order to give the user some feedback. The challenge here is that your view is only drawn when it gets called to do a drawRect. If, however, you go from AppDelegate to some initialization code to a viewcontroller's viewDidLoad, without the run loop getting a chance to figure out which views need to have drawRect called on, then your view will never display its status bar.
Therefore in order to accomplish what you want, you have to either make sure that drawRect gets called, such as by pushing off a lot of your initialization code into other threads or timer tasks, or you can force the drawing by calling drawRect yourself, after setting up contexts and such.
If you go with the background tasks, then make sure your initialization code is thread-safe.
Default.png is just a graphic, a static image shown while the application is launching. If you want to show further progress, you'll have to show everything at the applicationDidLaunch phase. Show your modal "Splash Screen" there first (Create a view controller, add its view as a subview of your main window) and dismiss it when you are done whatever additional loading you needed to do.
Also, you need to do update your progress bar in a seperate thread. Updating your GUI in the same thread where a lot of business is going on is (in my opinion, but I could be wrong) a bad idea.
The main thread is, as far as I know, the only one that can safely do GUI things, and its event loop (that is, the main application thread's) is the one that does the actual displaying after you've called -setNeedsDisplay. Spawn a new thread to do your loading, and update the progress on the main thread.