Apple's NIBs don't support rotation natively, so I'm trying to use two different NIBs for the same view - one for each orientation.
I tried the following, and it works, except ... it breaks some subviews (e.g. Apple's OpenGL / GLKit views).
It ONLY breaks the very first rotation - all subsequent rotations work exactly as expected. So, I'm assuming there's a subtlety in Apple's stack of "viewWill/Did" calls that I'm missing here. Problem is, I've never been able to find real documentation on that - just vague references here and there inside Apple docs to "some of" the calls that happen, and when/why.
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if( UIInterfaceOrientationIsLandscape( toInterfaceOrientation) )
{
[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:#"%#-landscape", NSStringFromClass([self class])] owner:self options:nil];
[self viewDidAppear:FALSE];
[self viewWillDisappear:FALSE];
[self viewDidReload];
[self viewWillAppear:FALSE];
[self viewDidAppear:FALSE];
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:#"navbar-landscape"] forBarMetrics:UIBarMetricsDefault];
}
else
{
[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:#"%#", NSStringFromClass([self class])] owner:self options:nil];
[self viewDidAppear:FALSE];
[self viewWillDisappear:FALSE];
[self viewDidReload];
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:#"navbar-portrait"] forBarMetrics:UIBarMetricsDefault];
[self viewWillAppear:FALSE];
[self viewDidAppear:FALSE];
}
}
According to Apple, in your case you should not only load a different nib, but change the view controller:
If you want to present the same data differently based on whether a
device is in a portrait or landscape orientation, the way to do so is
using two separate view controllers.
Read the View Controller Programming Guide to find out how exactly.
Hope this helps!
If you search Stack Overflow for "[ios] rotate different nibs" you'll get a ton of hits. There are some, shall we say, "creative" solutions out there.
To Levi's point, as far back as iOS 4.3 at the very least, Apple's counsel has been "load a new controller with the orientation-specific NIB". In Xcode, you can install old iOS documentation sets, and if you look at the iOS 4.3 View Controller Programming Guide, and go to the "Creating an Alternate Landscape Interface" section, you'll see that their counsel back then is very similar to what we see now (though it's using NIBs and not storyboards). But the concept is the same. Fire up a new view controller if a different orientation. It's going to be the most robust approach, IMHO, as when I researched this a long time ago, it appeared that all of the techniques that involved loading the NIB in the background seemed to somewhat kludgy feel to them. Doing this modal to the landscape controller ensures that no critical system notifications or other operations slip through the cracks.
Personally, I find that enough of a hassle, that I have generally given up on separate NIBs for different orientations and stay with a single NIB/scene and rely on springs and struts (a.k.a. auto sizing masks) to do the vast majority of layout changes. And when I have a view for whose changes require more significant relocation of controls with respect to each other, changing of background images, etc, I'll programmatically change the layout in viewWillLayoutSubviews (which, if iOS version is less than 5.0, I invoke from willAnimateRotationToInterfaceOrientation ... but for iOS 5 and later, viewWillLayoutSubviews is superior).
I found the problem: this appears to be a bug specifically in Apple's OpenGL code. It's only projects using GLKit that are affected:
Apple's GLKViewController has to be the sole view controller, taking the full area of the Container VC (e.g. UINavigationController/UITabController/window's rootViewcontroller) and must be managing precisely one GLKView - if there are any others, and/or the GLKViewController isn't full screen, it breaks autorotation. (there appears to be some tricks Apple is doing when the GLKVC is hooked up as the one running the show, which don't fire otherwise)
In theory, if you create a custom Container View Controller (the new feature from iOS 5 that's currently only partially documented by Apple), then it will work correctly. But until Apple publishes full docs for that (maybe WWDC next year?) that's very hard to do.
Related
I am well versed in android but somewhat new to iOS.
I have been working with the storyboard layout system of xcode to create my initial pages, but I am much more interested in dynamic page creation.
My requirements are really quite simple.
I have a thread in which the user selects a layout, then I perform a segue to that page.
However I am tired of the complications of the storyboard and figure there must be a simplier way.
I have functions that I can call which will layout a particular page to my specifications.
In android I do it like this:
I have an layout on the screen with a object defined in a particular location on the screen.
I then just connect the layout I want to the particular object.
Rather than redrawing the whole page with segues between storyboard pages which creates much duplication I would prefer something simplier.
here is pseudo code:
(ui thread)
display root layout (with hook/frame/object to contain subview)
user pushed button in root window to change layout
popup dialog asking which layout
send msg to render code
(render thread)
switch (whichLayout)
case A: connect layout A to subview; break
case B: connect layout B to subview; break
...
I have constructors for the views, the root layout, and the popups. In the iOS world I have a tableCollection or a gridCollection as a class which can be invoked to do the work, however, as I mentioned above I am recreating the entire display page with seques and would prefer not to.
I suspect I am missing some simple concept which is buried under storyboard.
My trouble came from introducing myself to iOS development through the tutorials on the Apple site which lead me down the path of stories boards, navigation controllers, and segues.
When I finally closed my eyes and looked past the pretty doo-dads and blinky-lights and instead understood the programatic way of doing things all became clear.
To answer my own question: the solution is quite simple (though I am sure all sane iOS developers will use the navigation contoller).
I can plop a new view on top of an existing view and then remove it or hide it.
Say 'case A' is my example above is to display a table view:
case A:
CGRect tableFrame = CGRectMake(0.0,0.0,self.window.frame.size.width,self.window.frame.size.height);
myTableView = [[UITableView alloc]initWithFrame:tableFrame style:UITableViewStylePlain];
... populate the table view ...
[self.window addSubview:myTableView];
break;
When switching to another case I remove the case A tableview and then create my case B view and so on:
[myTableView removeFromSuperview];
...
Alternately rather than create / destroy I could hide views so I do not always have to recreate them using
[self.window sendSubviewToBack:myTableView];
and then later:
[self.window bringSubviewToFront:myTableView];
Ok StackOverflow People...I've got a very interesting problem that I've been trying to solve for days and can't figure out so I need some major help. This will most likely be a very lengthy description but please bear with me and thank you deeply in advance for reading all of this because the more words I have, the clearer I can describe the full picture to you all. I will do my absolute best to be as terse and coherent as I can possibly be. Please let me know wherever I fall short.
Here's the context of my problem: I'm using Storyboards for my iOS app and for a particular nav tab in my app, I had to create two separate scenes for both the Portrait and Landscape orientations. The reason for doing this (instead of say, using Autolayout), is because within this said tab, there are visual elements (table views, web views, etc.) that are laid out differently depending on the orientation and it was a lot easier to create a separate orientation scene to handle this change in the UI instead of doing it programmatically -- (it's also just a lot easier to understand and cleaner code-wise). So the take away to keep in mind from all of this is that these two separate Portrait and Landscape scenes represent the SAME TAB in my app. (Side Note: these scenes were made in the IB of course)
Now the visual elements that I mentioned in the UI earlier -- going deeper, they are all containers for different UIViewControllers. I sandboxed everything in the app and pretty much have a 1-to-1 relationship for all things so these containers will map to my subclassed UIViewControllers that I've created for their specific purposes -- but it's here that the first caveat of my problem arises. Here's a practical example for a clearer picture, I have one UIViewController that contains a UITableView called MXSAnnouncementsViewController and this same view controller exists in both the Landscape and Portrait scenes. I did not create an explicit Portrait or Landscape VERSION of that view controller but instead, have the controller keep track of two IBOutlet properties (tableViewLandscape and tableViewPortrait) that point to the orientation-specific UITableViews -- and this approach works perfectly fine. Moreover in my MXSAnnouncementsViewController, I have a local property called tableView that abstracts the orientation-specific table views. It gets set within viewDidLoad which you can see below:
- (void)viewDidLoad
{
[super viewDidLoad];
if (self.tableViewPortrait) {
self.tableView = self.tableViewPortrait;
} else {
self.tableView = self.tableViewLandscape;
}
[self.tableView setDelegate:self];
[self.tableView setDataSource:self];
if (![MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements) {
[MXSAnnouncementManager loadModel:#"MXSAnnouncementGroupAllAnnouncements" withBlock:^(id model, NSError *error) {
if (!error) {
self.arrayLatestAnnouncements = [MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements;
[self.tableView reloadData];
} else {
// show some error msg
}
}];
} else {
self.arrayLatestAnnouncements = [MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements;
}
[self setupPullToRefresh];
}
Whenever I'm in the tab, one of the two orientation-specific IBOutlets is always active and has an address in memory while the other is nil. Whenever I rotate, the roles reverse -- whatever had an address in memory previously is now nil and the other has been initialized and allocated which is why I do what I did with the tableView property in the snippet above. Here is where caveat #2 comes into the picture and it's a doozy -- it has to do with the view lifecycle. Here's a practical example for clarity sake: Say I load the app up in Landscape orientation. When I do, my tableViewLandscape outlet has an address in memory and my tableViewPortrait outlet is nil. That's the expected and desired behavior. Now, when I rotate the app, the crazy stuff begins. Here's one place where I need clarity from all of you with regards to instances of UIViewControllers and what's normal vs. what's not so read the following VERY slowly and carefully.
Rotating the app immediately causes the opposite orientation scene (another INSTANCE of MXSAnnouncementsViewController???) to call its viewDidLoad method (in this example, we're in Landscape so the Portrait scene invokes that method). In that method, my local tableView property gets set to the currently active table view for that orientation (see snippet above). When that method finishes, the previous LANDSCAPE instance of MXSAnnouncementsViewController invokes its viewWillDisappear method which is then followed by the PORTRAIT instance's invocation of its viewWillAppear method which then lastly ends with the LANDSCAPE instance calling its willRotateToInterfaceOrientation callback -- that's the order of operation that I'm seeing from the breakpoints. I really do hope you got all of that because my mind just blew up from it all.
If you're still with me at this point, thank you because we're finally at the home stretch. As the title of this post suggests, the problem I'm trying to solve is my app freezing on rotation. If you haven't noticed on the viewDidLoad snippet, the last instruction to get executed is the setupPullToRefresh method which is the following:
- (void)setupPullToRefresh
{
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(refreshTableView:) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:refreshControl];
}
Since I already explained the whole view lifecycle order of operations on rotation earlier, to make a very long story short, if I comment out that last setupPullToRefresh instruction at the end of viewDidLoad for MXSAnnouncementsViewController, my app works fine. If I include that instruction, my app becomes totally unresponsive on the first rotation and I cannot for the life of me figure out why. Not sure if I'm dealing with an edge case here or something. Any and all insights are welcome and THANK YOU SO MUCH for reading all of this!
Your best approach is probably to abandon your current design of having two separate controllers for portrait and landscape. On iOS, you should always relayout the views for the orientation you want to be in, not destroying and recreating everything. By trying to handle it by recreating everything, you're just going to get yourself in trouble I think.
You can use auto layout to do complex reorderings of views upon rotation if you know it well, but probably your best bet is to scrap your current code to do landscape, and write code to simply rearrange the views yourself upon rotating. You'll have far fewer issues down the road, and your code will be easier for others to understand and maintain as well.
When you remove that one bit of code, your app may appear to be working just fine, but there is probably something going on behind the scenes that isn't quite correct that could come back to bite you in the future. That's probably why adding the line of code breaks it.
Try to add it after rotation
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
[self setupPullToRefresh];
}
If that doesn't help, create UIRefreshControl only once and set it to the right table on rotation.
If that doesn't help too, follow the first given answer (#Gavin's answer) and create only 1 table on viewDidLoad and relayout things in -(void)viewWillLayoutSubviews
I've been using autolayout for a couple of weeks now.
Currently, I'm using a 3rd party library called FLKAutoLayout which makes the process easier.
I'm at the point where I can construct views the way I want, usually without problem.
However, over the past 4 days at work I've been struggling with autolayout once viewcontrollers are involved.
I'm fine with all sorts of UIViews... but for some reason, every viewcontroller.view is a total demon.
I've had nothing but problems getting a viewcontroller.view to size the way I want and an ever deeper problem is that UIViews of child viewcontrollers do not receive events properly when using autolayout.
child viewcontrollers work just fine when designating frames manually, but everything breaks down with autolayout.
I don't understand what's so different about a viewcontroller's UIView that makes it different than all the others... My mind is melting in frustration. Is ios messing with my viewcontroller views behind the scenes or something?
In the image, the red area belongs to a child view controller. This area should not be going past the bottom most subview (the card that says three). This should be easy and I can get it to work just fine with a bunch of normal UIViews but because this is a viewcontroller, everything breaks...
Can anyone shed light on what it is I don't know. Any leads on potential issues is much appreciated.
Thanks for reading.
Update: The problem may be related to ambiguous constraints
UIView *box = [[UIView alloc]init];
[box addSubview:imageView];
[box addSubview:nameLabel];
imageView constrainWidth:#"32" height:#"32"];
[imageView alignTop:#">=0" leading:#"0" bottom:#"<=0" trailing:#"<=0" toView:box];
[imageView alignCenterYWithView:box predicate:#"0"];
[nameLabel constrainLeadingSpaceToView:imageView predicate:#"5"];
[nameLabel alignTop:#">=0" leading:#">=0" bottom:#"<=0" trailing:#"<=0" toView:box];
[nameLabel alignCenterYWithView:box predicate:#"0"];
[self addSubview:box];
[box alignTop:#"5" leading:#"5" bottom:#"-5" trailing:#"-5" toView:self];
The example above is an ambiguous layout but I can't figure out what's wrong with it...
This should probably be a comment, but comments suck for code. ;-)
Did you check for ambiguous constraints? To me this view looks like it could can be caused by ambiguous constraints.
Add this code to your app delegate:
// before #implementation AppDelegate
#interface UIWindow (AutoLayoutDebug)
+ (UIWindow *)keyWindow;
- (NSString *)_autolayoutTrace;
#end
// inside of #implementation
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
NSString *autolayoutTrace = [[UIWindow keyWindow] _autolayoutTrace];
if ([autolayoutTrace rangeOfString:#"AMBIGUOUS"].location != NSNotFound) {
NSLog(#"%#", autolayoutTrace);
}
else {
NSLog(#"No Ambiguous autolayout found");
}
}
and now shake your simulator. You'll find the shake gesture in the hardware menu.
If it doesn't show "No Ambiguous autolayout found" check for the ambiguous ui element(s) in the trace that was printed. They are marked with "AMBIGUOUS".
Then start to add constraints so there is no more ambiguity.
You can call exerciseAmbiguityInLayout on an UI-Element that has an ambiguous layout to get a hint which constraints are missing.
And make sure to remove the debugging code in your shipping product. You might put those two parts inside of #if DEBUG and #endif
At a certain point in your code you need to add the child view controller to the view controller hierarchy: [topViewController addChildViewController:childViewController];. And don't forget to add -didMoveToParentViewController: afterwards.
This will ensure that your rotation and touch events are forwarded the way you are expecting.
As for your autolayout problems: Erica Sadun has written some very useful autolayout debugging tools. I use it mostly to look at the viewLayoutDescription that she has written in a category on UIView, which prints a very readable lists of the constraints, tells you about ambiguity etc. Give it a try, it really helped me figure out how my constraints were messed up.
Second: always make sure that all of your views have translateAutoresizingMasksIntoConstraints set to NO.
I am using one xib file as my first screen in my iPad app.In that xib file I am using one image view in background and one button,if I click on that button then it will go to another page.My application supports both orientation.Now the problem is , suppose I click on that button and enter to the second page and then change the orientation of the device then go back to the main page, now in main page button's frame is changed. so I am not able to understand where to click to go to second page for second time.
Please help me to get rid of this problem.
I found a much better way that works independent of the nav controller. Right now, I have this working when embedded in a nav controller, and when NOT embedded (although I'm not using the nav controller right now, so there may be some bug I've not seen - e.g. the PUSH transition animation might go funny, or something)
Two NIBs, using Apple's naming convention. I suspect that in iOS 6 or 7, Apple might add this as a "feature". I'm using it in my apps and it works perfectly:
triggers on WILL rotate, not SHOULD rotate (waits until the rotate anim is about to start)
uses the Apple naming convention for landscape/portrait files (Default.png is Default-landscape.png if you want Apple to auto-load a landscape version)
reloads the new NIB
which resets the self.view - this will AUTOMATICALLY update the display
and then it calls viewDidLoad (Apple will NOT call this for you, if you manually reload a NIB)
(NB stackoverflow.com requires this sentence here - there's a bug in the code formatter)
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if( UIInterfaceOrientationIsLandscape(toInterfaceOrientation) )
{
[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:#"%#-landscape", NSStringFromClass([self class])] owner:self options:nil];
[self viewDidLoad];
}
else
{
[[NSBundle mainBundle] loadNibNamed:[NSString stringWithFormat:#"%#", NSStringFromClass([self class])] owner:self options:nil];
[self viewDidLoad];
}
}
My iPad app makes heavy use of autorotation. This is great. However, I've noticed that if a hidden view is released by the default implementation of didReceiveMemoryWarning (as described here), when the view is re-loaded from the nib and I happen to be in landscape, it loads it in portrait. This wreaks havoc with the interface until I rotate the iPad manually and force it to go to the proper orientation.
I had assumed that iOS would load the view in the current orientation; that's what it does when the app launches. But it no, not after being unloaded by didReceiveMemoryWarning. Why not? And how can I get it to do that?
The answer, determined thanks to pointers from dbarker, is that the view controller's rotation methods, including -willRotateToInterfaceOrientation:duration: and -willAnimateRotationToInterfaceOrientation:duration:, will not be called when a view is reloaded after a the default implementation of didReceiveMemoryWarning has unloaded the view. I've no idea why it would be different on app launch, but I do have a workaround
What I did was to set a boolean ivar, named unloadedByMemoryWarning, to YES in didReceiveMemoryWarning, like so:
- (void) didReceiveMemoryWarning {
unloadedByMemoryWarning = YES;
[super didReceiveMemoryWarning];
}
Then, in viewDidLoad, if that flag is true, I set it to NO and then call the rotation methods myself:
if (unloadedByMemoryWarning) {
unloadedByMemoryWarning = NO;
[self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
[self willAnimateRotationToInterfaceOrientation:self.interfaceOrientation duration:0];
[self didRotateFromInterfaceOrientation:self.interfaceOrientation];
}
Kinda sucks that I have to do this, but it does work, and now I'm less concerned about getting killed by iOS for using too much memory.
I think iOS 5 might fix this.
For iOS 4.3, I've had good luck with another fix. After the load from nib:
[parent.view addSubview:nibView];
nibView.frame = parent.view.frame;
[nibView setNeedsLayout];
If that worked, you could jettison the unloadedByMemoryWarning logic, since it's safe to do every load. Got the tip & code (basically) from here.