iOS Sizing UITableView During Orientation Change - ios

I have a view that has two tables. In the story board, I have two separate views, one horizontal and the other vertical. When I need to navigate to the view, the code detects the orientation and brings up the appropriate view (and does so on an orientation change.
I have the following code in my method:
-(void)viewDidAppear:(BOOL)animated{
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
if(orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight){
if(tableHeight2 > 324){
tableHeight2 =325;
}
table1.frame = CGRectMake(table1.frame.origin.x, table1.frame.origin.y, table1.frame.size.width, tableHeight1);
table2.frame = CGRectMake(table2.frame.origin.x, table1.frame.origin.y + 20 + tableHeight1, table2.frame.size.width, tableHeight2);
}else {
if(tableHeight2 > 500){
tableHeight2 = 500;
}
table1.frame = CGRectMake(table1.frame.origin.x, table1.frame.origin.y, table1.frame.size.width, tableHeight1);
table2.frame = CGRectMake(table2.frame.origin.x, table1.frame.origin.y + 50 + tableHeight1, table2.frame.size.width, tableHeight2);
}
}
This works wonderfully when I press a button to navigate to the view. It adds up all of the cell heights and makes the first table the appropriate height, then moves the second table 50 pixels below the first table. It also makes sure the second table doesn't extend beyond the visible screen area.
When the orientation changes, I the following code is executed:
-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration {
InitViewController *ini;
ini = [self.storyboard instantiateViewControllerWithIdentifier:#"Init"];
ini.location = MenuName;
[self presentViewController:ini animated:NO completion:nil];
}
This should do the same thing that pressing a barbuttonitem does: change to InitViewController while sending the StoryboardID to it in the ini.location variable. The code for the navigation buttons is pretty much identical to the code in willAnimateRotationToInterfaceOrientation. InitViewController then determines the orientation and sends the app to the correct storyboard UIView.
It does send it to the right view, I can tell based on the table widths. What it doesn't do is change the height of the first (top) table, table1. The first table retains the size it was given in the storyboard.
If there is area of code you think I need to post to get a better picture, let me know I'll be happy to add it. Any help, insight, or even just trial-and-error suggestions would be appreciated.
*Note: I have tried to change willAnimateRotationToInterfaceOrientation to ViewDidLayoutSubviews, to not effect.

Well, it seems a very small change fixed it. I noticed that the code on the navigation buttons had YES under "animate" for the view change, and the willAnimateRotationToInterfaceOrientation "animated:NO". I changed it to "YES" and that fixed it. Not sure why yet, perhaps it affects how the method displays the view or affects the load order, but there it is.

Related

iOS UITextField Outside of SuperView not Responding to Touches

I'm building an iOS 8 app that makes use of the new hidesBarsOnSwipe property on UINavgitationController to hide the nav bar while scrolling. At the same time that the nav bar hides, I'm also programmatically hiding the tab bar. On top of the tab bar, there is a text field which lets users comment on a post (much like Facebook). When the tab bar is hidden (by moving it downward and off the screen), the text field is moved down as well, so that it now sits at the bottom of the screen and so that there's no gap between the bottom of the screen and the text field.
So, things look great. But, turns out that the text field doesn't respond to touch events when it moves to the bottom of the screen. I did some digging and it appears that the reason is because the text field is outside of its superview (the view controller's view), and so touch events will not be sent to the text field.
So I think I've figured out why the issue is occurring, but I haven't yet figured out how to fix it. I've tried messing with hitTest:withEvent: and pointInside:withEvent: but didn't have any luck. Anyone have any solutions?
EDIT: Here is some code to make the question clearer (hopefully). When the nav controller's barHideOnSwipeGestureRecognizer is called, I am running the following code:
- (void)barHideSwipeGestureActivated:(UIPanGestureRecognizer*)gesture
{
[self animateTabBarUpOrDown:self.navigationController.navigationBar.frame.origin.y >= 0 completion:nil];
}
The method above is the following:
- (void)animateTabBarUpOrDown:(BOOL)up completion:(void (^)(void))completionBlock
{
if(!self.animatingTabBar && self.tabbarIsUp != up)
{
self.animatingTabBar = YES;
//to animate the tabbar up, reset the comments bottom constraint to 0 and set the tab bar frame to it's original place
//to animate the tabbar down, move its frame down by its height. set comments bottom constraint to the negative value of that height.
[UIView animateWithDuration:kTabBarAnimationDuration animations:^{
UITabBar *tabBar = self.tabBarController.tabBar;
if(up)
{
tabBar.frame = CGRectMake(tabBar.frame.origin.x, tabBar.frame.origin.y - tabBar.frame.size.height, tabBar.frame.size.width, tabBar.frame.size.height);
self.addCommentViewToBottomConstraint.constant = 0.0f;
}
else
{
tabBar.frame = CGRectMake(tabBar.frame.origin.x, tabBar.frame.origin.y + tabBar.frame.size.height, tabBar.frame.size.width, tabBar.frame.size.height);
self.addCommentViewToBottomConstraint.constant = -tabBar.frame.size.height;
}
} completion:^(BOOL finished) {
self.tabbarIsUp = up;
self.animatingTabBar = NO;
if(completionBlock)
{
completionBlock();
}
}];
}
}
Ok, finally found a solution for this one. I haphazardly messed around with changing the bounds of my view controller's view, but that was too hacky and ultimately didn't accomplish what I wanted it to.
What I ended up doing was changing my view controller's edgesForExtendedLayout property to be equal to UIRectEdgeAll which basically says that the view should take up the entire screen, and extend above top bars / below bottom bars.
I had to hack around a little bit with changing auto layout constraints on my text field so that it appeared in the right place at the right time, but overall, the solution was changing edgesForExtendedLayout to be UIRectEdgeAll - this makes the view take up the entire screen, so the text field is now still in the super view even when it animates downward, thus, allowing it to still receive touch events.

iOS Rotation with UITabBarController - switch controller out completely and smoothly

I have an iOS application and the main entry point into the storyboard is a Tabbar with with tabs. I want to support rotation but in landscape mode, each tab will look drastically different to the point I need to change the views out with others from the storyboard. At first though, I considered just switching out the entire tab bar controller when the user rotates, but I don't see how that might be accomplished. So I have two options that I can't seem to get anywhere with.
Switch each view with a segue somehow into it's landscape alternative and back. I've seen this done easily with modal views, but not with UITabbar before.
Switch out the entire tab bar somehow from the delegate so that I just have two completely separate section of my storyboard that are alike except one path is portrait and the other is landscape.
Does anyone have anything similar they are doing and could throw me a bone?
You can create a custom class for both view controllers, link them together with segoes, and then use didRotateFromInterfaceOrientation: to trigger the segues from code.
Create a class for your VC's. Add this code to the .m file:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
if (fromInterfaceOrientation == UIInterfaceOrientationLandscapeLeft || fromInterfaceOrientation == UIInterfaceOrientationLandscapeRight) {
[self performSegueWithIdentifier:#"rotateToPortrait" sender:nil];
} else {
[self performSegueWithIdentifier:#"rotateToLandscape" sender:nil];
}
}
And in your storyboard, create segues between the two views. Make their identifiers "rotateToPortrait" and "rotateToLandscape", respectively. The code you added will switch the views whenever the device is rotated.
EDIT: I think I misunderstood the question. If you want to move views around, change their size, etc. when the screen is rotated, while keeping the same navigation/tab bar state, then you can do something like this in the didRotateFromInterfaceOrientation method:
if (fromInterfaceOrientation == UIInterfaceOrientationLandscapeLeft || fromInterfaceOrientation == UIInterfaceOrientationLandscapeRight) {
myView.frame.origin.x = aNumber;
myView.frame.size.width = anotherNumber;
// Changing the layout of the views here by resetting their origins and sizes.
// This code is called when rotated from portrait to landscape.
} else {
// Do the same thing as above, but this one handles rotation from landscape to portrait.
}

topLayoutGuide in child view controller

I have a UIPageViewController with translucent status bar and navigation bar. Its topLayoutGuide is 64 pixels, as expected.
However, the child view controllers of the UIPageViewController report a topLayoutGuide of 0 pixels, even if they're shown under the status bar and navigation bar.
Is this the expected behavior? If so, what's the best way to position a view of a child view controller under the real topLayoutGuide?
(short of using parentViewController.topLayoutGuide, which I'd consider a hack)
While this answer might be correct, I still found myself having to travel the containment tree up to find the right parent view controller and get what you describe as the "real topLayoutGuide". This way I can manually implement automaticallyAdjustsScrollViewInsets.
This is how I'm doing it:
In my table view controller (a subclass of UIViewController actually), I have this:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
_tableView.frame = self.view.bounds;
const UIEdgeInsets insets = (self.automaticallyAdjustsScrollViewInsets) ? UIEdgeInsetsMake(self.ms_navigationBarTopLayoutGuide.length,
0.0,
self.ms_navigationBarBottomLayoutGuide.length,
0.0) : UIEdgeInsetsZero;
_tableView.contentInset = _tableView.scrollIndicatorInsets = insets;
}
Notice the category methods in UIViewController, this is how I implemented them:
#implementation UIViewController (MSLayoutSupport)
- (id<UILayoutSupport>)ms_navigationBarTopLayoutGuide {
if (self.parentViewController &&
![self.parentViewController isKindOfClass:UINavigationController.class]) {
return self.parentViewController.ms_navigationBarTopLayoutGuide;
} else {
return self.topLayoutGuide;
}
}
- (id<UILayoutSupport>)ms_navigationBarBottomLayoutGuide {
if (self.parentViewController &&
![self.parentViewController isKindOfClass:UINavigationController.class]) {
return self.parentViewController.ms_navigationBarBottomLayoutGuide;
} else {
return self.bottomLayoutGuide;
}
}
#end
Hope this helps :)
I might be wrong, but in my opinion the behaviour is correct. The topLayout value can be used by the container view controller to layout its view's subviews.
The reference says:
To use a top layout guide without using constraints, obtain the guide’s position relative to the top bound of the containing view.
In the parent, relative to the containing view, the value will be 64.
In the child, relative to the containing view (the parent), the value will be 0.
In the container View Controller you could use the property this way:
- (void) viewWillLayoutSubviews {
CGRect viewBounds = self.view.bounds;
CGFloat topBarOffset = self.topLayoutGuide.length;
for (UIView *view in [self.view subviews]){
view.frame = CGRectMake(viewBounds.origin.x, viewBounds.origin.y+topBarOffset, viewBounds.size.width, viewBounds.size.height-topBarOffset);
}
}
The Child view controller does not need to know that there are a Navigation and a Status bar : its parent will have already laid out its subviews taking that into account.
If I create a new page based project, embed it in a navigation controller, and add this code to the parent view controllers it seems to be working fine:
you can add a constraint in the storyboard and change it in viewWillLayoutSubviews
something like this:
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
self.topGuideConstraint.constant = [self.parentViewController.topLayoutGuide length];
}
The documentation says to use topLayoutGuide in viewDidLayoutSubviews if you are using a UIViewController subclass, or layoutSubviews if you are using a UIView subclass.
If you use it in those methods you should get an appropriate non-zero value.
Documentation link:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/topLayoutGuide
In case if you have UIPageViewController like OP does and you have for example collection view controllers as children. Turns out the fix for content inset is simple and it works on iOS 8:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
UIEdgeInsets insets = self.collectionView.contentInset;
insets.top = self.parentViewController.topLayoutGuide.length;
self.collectionView.contentInset = insets;
self.collectionView.scrollIndicatorInsets = insets;
}
This has been addressed in iOS 8.
How to set topLayoutGuide position for child view controller
Essentially, the container view controller should constrain the child view controller's (top|bottom|left|right)LayoutGuide as it would any other view. (In iOS 7, it was already fully constrained at a required priority, so this didn't work.)
I think the guides are definitely meant to be set for nested child controllers. For example, suppose you have:
A 100x50 screen, with a 20 pixel status bar at the top.
A top-level view controller, covering the whole window. Its topLayoutGuide is 20.
A nested view controller inside the top view covering the bottom 95 pixels, eg. 5 pixels down from the top of the screen. This view should have a topLayoutGuide of 15, since its top 15 pixels are covered by the status bar.
That would make sense: it means that the nested view controller can set constraints to prevent unwanted overlap, just like a top-level one. It doesn't have to care that it's nested, or where on the screen its parent is displaying it, and the parent view controller doesn't need to know how the child wants to interact with the status bar.
That also seems to be what the documentation--or some of the documentation, at least--says:
The top layout guide indicates the distance, in points, between the top of a view controller’s view and the bottom of the bottommost bar that overlays the view
(https://developer.apple.com/library/ios/documentation/UIKit/Reference/UILayoutSupport_Protocol/Reference/Reference.html)
That doesn't say anything about only working for top-level view controllers.
But, I don't know if this is what actually happens. I've definitely seen child view controllers with nonzero topLayoutGuides, but I'm still figuring out the quirks. (In my case the top guide should be zero, since the view isn't at the top of the screen, which is what I'm banging my head against at the moment...)
This is the approach for the known guide length. Create constrains not to guides, but to view's top with fixed constants assuming guide distance will be.
Swifty implementation of #NachoSoto answer:
extension UIViewController {
func navigationBarTopLayoutGuide() -> UILayoutSupport {
if let parentViewController = self.parentViewController {
if !parentViewController.isKindOfClass(UINavigationController) {
return parentViewController.navigationBarTopLayoutGuide()
}
}
return self.topLayoutGuide
}
func navigationBarBottomLayoutGuide() -> UILayoutSupport {
if let parentViewController = self.parentViewController {
if !parentViewController.isKindOfClass(UINavigationController) {
return parentViewController.navigationBarBottomLayoutGuide()
}
}
return self.bottomLayoutGuide
}
}
Not sure if anyone still got problem with this, as I still did a few minutes ago.
My problem is like this (source gif from https://knuspermagier.de/2014-fixing-uipageviewcontrollers-top-layout-guide-problems.html).
For short, my pageViewController has 3 child viewcontrollers. First viewcontroller is fine, but when I slide to the next one, the whole view is incorrectly offset to the top (~20 pixel, I guess), but will return to normal after my finger is off the screen.
I stayed up all night looking for solution for this but still no luck finding one.
Then suddenly I came up with this crazy idea:
[pageViewController setViewControllers:#[listViewControllers[1]] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) {
}];
[pageViewController setViewControllers:#[listViewControllers[0]] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished) {
}];
My listViewControllers has 3 child viewcontrollers. The one at index 0 has problem, so I firstly set it as root of pageviewcontroller, and right after that set it back to the first view controller (as I expected).
Voila, it worked!
Hope it helps!
This is an unfortunate behavior that appears to have been rectified in iOS 11 with the safe-area API revamp. That said, you will always get the correct value off the root view controller. For example, if you want the upper safe area height pre-iOS 11:
Swift 4
let root = UIApplication.shared.keyWindow!.rootViewController!
let topLayoutGuideLength = root.topLayoutGuide.length

Cannot resize AdMob's view upon orientation change

What I tried so far is, in viewDidLoad, I called
self.bannerView.autoresizingMask=UIViewAutoresizingFlexibleWidth;
and
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)newInterfaceOrientation duration:(NSTimeInterval)duration {
if (newInterfaceOrientation == UIInterfaceOrientationLandscapeLeft || newInterfaceOrientation == UIInterfaceOrientationLandscapeRight) {
self.bannerView.frame=CGRectMake(0.0,
0.0,
480.0,
GAD_SIZE_320x50.height);
}
// Position the UI elements for portrait mode
else {
self.bannerView.frame=CGRectMake(0.0,
0.0,
GAD_SIZE_320x50.width,
GAD_SIZE_320x50.height);
}
}
Both of these didn't work for me.
Hmm, I don't think that AdMob's creatives can stretch to fit the size of the screen when in landscape. So despite the fact that you're stretching the frame of the view to fit, the ad itself I think will stay the same size.
This means you should still see an ad come in on orientation changes, it will just look like it's the same size (make sure to make another request for an ad in the willAnimateRotationToInterfaceOrientation: method to see this).
You don't need to do any moves, but you must set correct rootViewController for adMovView.
If you use view controller model please add line in each custom view controller
adMobView.rootViewController = viewController;
where viewController - root view controller of your app.
Do not code like this
adMobView.rootViewController = self;
in custom view!

UISplitViewController Multiple Detail Views Segmented Control

I'm working on an iPad app in a split view controller where the app will remain in landscape the entire time. I would like the root view controller to remain a list and the detail view controller to swap out 4 different views controlled by a UISegmentedControl.
I'm following this post here UISegmentedControl Best Practice, however when I swap in my view controllers, they don't properly fit in the detailview controller, they are cut off as if they are trying to draw for ipad portrait orientation.
If I completely ignore the segmented control approach and have a detail view, the view size properly in the detail view, but once i try to swap them in with a segmented control is where I run into trouble.
Is there a way to tell the swapped in views to draw correctly?
Have you tried:
swappedInView.frame = detailController.view.bounds;
when you call
[detailedController.view addSubview:swappedInView];
?
Their contents need to have their resizing behaviors (most easily in xcode/IB) set appropriately.
I am using a UISegmentControl as well, but adding my views programmatically. I have my default view (segment 0) loaded first in the viewDidLoad of the rootController. Then based on which segment is pressed, I check if the view has been initialized, if not, initialize, then add it as a subview. Then remove the other view. I had a similar post on this on how to keep track of it that might help you out, and has the code from Beginning iPhone 4 Development book that I used for my own app. Here's the code snippet to get you started if you want to go this approach:
if (self.yellowViewController.view.superview == nil)
{
if (self.yellowViewController == nil)
{
YellowViewController *yellowController =
[[YellowViewController alloc] initWithNibName:#"YellowView"
bundle:nil];
self.yellowViewController = yellowController;
[yellowController release];
}
[blueViewController.view removeFromSuperview];
[self.view insertSubview:yellowViewController.view atIndex:0];
}
else
{
if (self.blueViewController == nil)
{
BlueViewController *blueController =
[[BlueViewController alloc] initWithNibName:#"BlueView"
bundle:nil];
self.blueViewController = blueController;
[blueController release];
}
[yellowViewController.view removeFromSuperview];
[self.view insertSubview:blueViewController.view atIndex:0];
}
In my own, I add as a subview, instead of inserting it behind the other views (they had a toolbar in the front in their example). So if say segment 3 was pressed, then I would check the other views if their superviews were present, remove that view, add my view. Hope that helps.

Resources