I'm trying to recreate an effect similar to the iOS Music app in that I want a completely new view to appear in my app when the phone flips from vertical to horizontal (the Music app shows album covers). And I can't quite seem to get it working. So far I've tried variations of the following:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
TMFHorizontalFavoritesViewController *horizontalVC = [[TMFHorizontalFavoritesViewController alloc] init];
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
if (UIDeviceOrientationIsLandscape(deviceOrientation)) {
[self presentViewController:horizontalVC animated:YES completion:nil];
}
}
But at best all I seem to get is a black screen. I've tried googling and searching here on Stackoverflow for similar questions, but I can't seem to piece together the right answer. If anyone has any input it would be much appreciated!
If there's one piece of advise I could give new iOS programmers, it would be never ever instantiate a view controller with alloc init (there are times when it's legit, but newbies almost always get it wrong).
This is how you do it with a storyboard defined controller,
TMFHorizontalFavoritesViewController *horizontalVC = [self.storyboard instantiateViewControllerWithIdentifier:#"MyIdentifier"];
Related
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 need to work out how to identify what storyboard is active at any given time. I have a specialised nativation in which I need to identify the storyboard (UIView) then change things programmatically depending on what the user presses.
All storyboards have Identifiers.
in the viewDidLoad of the root view I have the following.
- (void)viewDidLoad
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"View1"];
{
What I would like to do is identify which storyboard the user is on and depending on the press do the following sudo-code
- (void)viewDidLoad
if (storyboard.name != RootView)
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"View1"];
{
else if (storyboard.name = View2){
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"View2"];
}
etc....
I have step through the code and seen the StoryboardID however it's which I'm pretty sure your not meant to use....
Thanks in advance
Jeremy
UPDATE: Explanation to Navigation
I'm trying to implement the ECSlideViewController, but It's doing my head in. Effectively adding in the slide to the right function to reveal more options. SO, this thinking was going to be easy turned out icky. I have the master UIViewController<title:HomeView> I then have 4 buttons on the screen which segueway to other UIViewControllers<View1>, UIViewController<View2> etc.
In order to produce the effect on View1,View2,View3,View4 I need to bring the class (ECSlideViewController as per the example) into the UIViewController<HomeView>. However If I change the below code to represent this...
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"HomeView"];
It crashes because it calls itself. Not good, circular coding is a no no.
but if I set it to what was originally there
self.topViewController =
[storyboard instantiateViewControllerWithIdentifier:#"FirstTop"];
( btw firstTop is the title of the view used with the example)
It works but then disregards the UIViewController<HomeView>
This is why I asked if there was a way to identify the self.title of the UIViewController(said storyboard...my bad) as I was going to put conditional logic around it in order to not put the code in if it's on the UIViewController<HomeView>.
It really is hard to explain unless you download the ECSlideViewController and start playing with it. But effectively I just want to test the self.title.....I think...
The other idea was to bring the logic into the UIViewControllers of the four and get it to work there...but It freaks out passing nil as it's expecting an identifier...
Hope this makes sense....
J.
Okay Guys,
Totally ditched ECSlideViewController. I found a few articles explaining that it had issues when you had multiple UiViewControllers not passing data correctly. I ended up using Andrews suggestion. http://www.youtube.com/feed/UCJA_puohXgnze8gPaerTeig It worked for easier for me.
Although I take note of what the design guidelines Apple have an this is usually a no no, but I'm hoping that they won't mind.
Thanks everyone again!
J.
I'm not sure if this helps, but it sounds like you will have to compare instances to get the results you are looking for. I haven't tried this, but I would create properties of each storyboard in your app delegate, then reference the app delegate in your view controller to compare. This might not be the best coding practices, but I'll leave that into your hands.
Something like (untested code):
- (void)testStoryBoard //After awake from nib or viewDidLoad
{
NXAppDelegate *appDelegate = (NXAppDelegate *)[[UIApplication sharedApplication] delegate];
if ([self.storyBoard isEqual:appDelegate.view1StoryBoard])
NSLog(#"View1 Storyboard");
else
NSLog(#"View 2 Storyboard");
}
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.
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.