I want to have a UIView take over the whole screen to display an image from time to time, but most of the time I'm using a UISplitViewController for the main app functionality. Is there a recommended/best practice way to do this? Right now, I have a pointer to the main window and am just adding the UIView as a sub view and bringing it to front, but it won't display in the proper orientation. I was just wondering if there is a better way/something simple I'm missing.
Thanks.
I struggled with this same issue for some time and have come up with a clunky workaround. Before my solution, some more background on the obstacles:
In iPhone OS, the root view's coordinates are always in the literal fixed coordinates of the physical screen. "Up" is always device up. Subviews have to get coordinates which have been converted to a given orientation so that "Up" is what we expect to be: gravitational up.
Placing a UIView at the top of the view hierarchy (above the UIScrollView) would therefore require that you make these conversions yourself during orientation events. You get fullscreen, but you lose the benefit of the scrollView managing orientation coordinates for your subviews.
Keeping your view within the scrollView, there is no easy method as in Mac OS to simply collapse a pane of a splitView. Reviewing the header file reveals some private instance methods used to manage width.
So, I think that what you can do is maintain two "orientation-aware" view controllers (one splitView, one fullscreen), and switch them out and move your view between them when you toggle fullscreen. I did not go this route as it is obviously a touchy situation memory- and view-hierarchy- wise.
What I do is switch between an "almost fullscreen" width for the main view and regular split width. This works great except that the splitView's rounded corners are actually hard-coded images that get draw regardless of the splitView's dimensions. You will see these tiny black round corners 100% of the time. Here is the effect:
- (IBAction)toggleFullscreen:(id)sender; {
id appDelegate = [[UIApplication sharedApplication] delegate];
UISplitViewController *split = [appDelegate splitViewController];
//get master and detail view controller
UIViewController *master = [split.viewControllers objectAtIndex:0];
UIViewController *detail = [split.viewControllers objectAtIndex:1];
//In landscape permit fullscreen
if(self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft ||
self.interfaceOrientation == UIInterfaceOrientationLandscapeRight) {
CGRect f = detail.view.frame;
if ( f.origin.x == 0 ) { //exiting fullscreen
[sender setImage:[UIImage imageNamed:#"SlideLeft.png"]];
//adjust detail view
f.size.width = 703;
f.origin.x = 321;
[detail.view setFrame:f];
//adjust master view
f = master.view.frame;
f.size.width = 320;
f.origin.x = 0;
[master.view setFrame:f];
} else { //entering fullscreen
[sender setImage:[UIImage imageNamed:#"SlideRight.png"]];
//adjust detail view
f.size.width = 1024;
f.origin.x = 0;
[detail.view setFrame:f];
//adjust master view
f = master.view.frame;
f.size.width = 1;
f.origin.x = -1;
[master.view setFrame:f];
}
}
}
I got lots of this code from here and other sources, but as far as I know this implementation is unique. Probably because it is imperfect.
I ended up using a modal view controller which I present from the split view controller. I then specify the allowed orientations in -shouldAutorotateToInterfaceOrientation. I also hide the status bar. This works for my needs.
Related
I am trying to adjust the size of a background static UIImageView (from Nib file) for iPhone5 users. Unfortunately, the following code does not seem to make any difference on the background view's size.
Does anyone know why? Thanks in advance for any help you can provide.
ViewController.m:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
device = appDelegate.deviceType;
NSLog(#"The device platform is: %#", device);
if ([[device substringToIndex:8] caseInsensitiveCompare: #"iPhone 5"] == NSOrderedSame) {
[background sizeThatFits:CGSizeMake(320, 504)];
}
else {
[background sizeThatFits:CGSizeMake(320, 416)];
}
...
//Note: 'background' is declared in `ViewController.h` as, `IBOutlet` `UIImageView` *background, and is linked to the image view in ViewController_iPhone.xib
A few thoughts:
As demosten and shabzco suggested, I wouldn't use a device name/description to determine coordinates (if nothing else, what about the iPhone 6, etc.);
If you're going to set the frame programmatically, I would suggest setting the background.frame property based upon the view controller's view's bounds rather than hard coding the size of the image view. That way, the background is adjusted to the appropriate size of the view controller's view, not only regardless of device, but also regardless if that view controller is, at a later date, embedded as a child view controller of another container controller, etc. (E.g., what if you put your view in a navigation controller and tab bar controller, your own custom container controller, etc.). Also, don't make assumptions about the size of the status bar or other graphical elements. Let iOS figure all of this out for you with simply:
background.frame = self.view.bounds;
Or better yet, if you've added the background image view to the NIB itself, set the autosizing mask and don't change the frame programmatically at all. If you have autolayout turned off, just set the autosizing properties of your image view like so:
Bottom line, if you can, avoid explicit device name references in your code and avoid hard coded coordinates. To have hardcoded dimensions in your code will just make your app more fragile, susceptible to problems with new devices, new versions of iOS, embedding your view controller in additional container controllers, etc., and limits the reuse opportunities for your code.
Here's a better way to check between iPhone 5 and previous sized devices
if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
CGSize result = [[UIScreen mainScreen] bounds].size;
if(result.height == 480)
{
[background sizeThatFits:CGSizeMake(320, 416)];
}
if(result.height == 568)
{
[background sizeThatFits:CGSizeMake(320, 504)];
}
}
sizeThatFits does not change size. You should modify background.frame. Something like:
background.frame = CGRectMake(background.frame.origin.x, background.frame.origin.y, 320, 416);
and
background.frame = CGRectMake(background.frame.origin.x, background.frame.origin.y, 320, 504);
Also make sure your UIImageView does not have flexible width or height in Size inspector tab while editing your Nib file.
The width of a UITabBarItem varies, depending on how many there are.
How do I determine how wide my tab bar items are?
I'm looking for a property if possible, as opposed to a mathematical formula, since on the iPad, there is also an issue of padding on either side of the tab bar. Consider these screenshots. Notice the padding on either side of the tab bar items on the iPad (highlighted with the red boxes). This padding does not exist on the iPhone.
The iPad:
The iPhone:
Edit: It has been noted in the comments below that this solution did not work in a beta version of iOS 5, so be prepared to modify it to meet your needs.
I think in order to do this the right way, you have to be able to get to the frame of each tab bar item. Fortunately, this is possible:
CGFloat tabBarTop = [[[self tabBarController] tabBar] frame].origin.y;
NSInteger index = [[self tabBarController] selectedIndex];
CGFloat tabMiddle = CGRectGetMidX([[[[[self tabBarController] tabBar] subviews] objectAtIndex:index] frame]);
The above code gives you the y coordinate of the top of the tab bar and the x coordinate of the middle of the selected tab item. From here, you can add your indicator view to the tab bar controller's view relative to this point.
Disclaimer: The danger with this approach is that it peeks under the hood of the UITabBar class by accessing its view hierarchy and the frame property of instances of the private UITabBarButton class. It is possible (however unlikely) that this approach could be affected/broken by a future iOS update. However, you're not accessing any private APIs, so this shouldn't get your app rejected from the App Store.
I made a simple iPad project demonstrating how it works, complete with animations.
Swift 3
I created a shared instance in UIApplication then pulled my subviews width
tabBarController?.tabBar.subviews[1].frame.width
UITabBarItem inherits from UIBarItem, which inherits from NSObject. Since it doesn't inherit from UIView, I don't know that you're going to be able to get the information you want just with a property. I know you don't want a mathematical way of doing it, but it would seem that getting the frame of the tab bar and dividing by the # of tabs would be a reasonable option that should work regardless of the hardware (iphone vs ipad). Padding shouldn't affect this, right? So you'd have:
tabSize = tabbar.frame.size.width / [tabbar.items count];
tabBarStart = tabbar.frame.origin.x;
// Assume index of tabs starts at 0, then the tab in your pic would be tab 4
// targetX would be the center point of the target tab.
targetX = tabBarStart + (tabSize * targetTabIndex) + (tabSize / 2);
I, as well, am interested to know if someone finds a simple property to give this information.
Looking at the answer above:
Items in the UITabBar work different in the iPhone and the iPad. In the iPhone they fill whole width, in the iPad they're centered horizontally.
There is a spacing between the items in the iPad.
You can set the values for both width and spacing using tabBar.itemWidth or tabBar.itemSpacing. You are not able to read system spacing or width from it - you'll receive 0 unless you set it.
So here's my sample how to get frames of all UITabBarItems:
// get all UITabBarButton views
NSMutableArray *tabViews = [NSMutableArray new];
for (UIView *view in self.tabBar.subviews) {
if ([view isKindOfClass:[UIControl class]]) {
[tabViews addObject:[NSValue valueWithCGRect:view.frame]];
}
}
// sort them from left to right
NSArray *sortedArray = [tabViews sortedArrayUsingComparator:^NSComparisonResult(NSValue *firstValue, NSValue *secondValue) {
CGRect firstRect = [firstValue CGRectValue];
CGRect secondRect = [secondValue CGRectValue];
return CGRectGetMinX(firstRect) > CGRectGetMinX(secondRect);
}];
Now you have a table of frames, corresponding to your tabbar items. You can eg. place an imageView on selected index button frame.
CGRect frame = self.tabBar.bounds;
CGSize imageSize = CGSizeMake(CGRectGetWidth(frame) / self.tabBar.items.count, self.imageView.image.size.height);
CGRect selectedRect = sortedArray.count > self.selectedIndex ? [sortedArray[self.selectedIndex] CGRectValue] : CGRectZero;
[self.imageView setFrame:CGRectIntegral(CGRectMake(CGRectGetMinX(selectedRect), CGRectGetMaxY(frame) - imageSize.height,
CGRectGetWidth(selectedRect), imageSize.height))];
I tried these 2 properties of UITabBar:
#property(nonatomic) CGFloat itemWidth NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR;
#property(nonatomic) CGFloat itemSpacing NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR;
But get zero value as Vive said. So I wrote some codes to get the right width:
CGFloat tabBarItemWidth = 0;
for (UIView *view in [self.tabBarController.tabBar subviews]) {
if ([view isKindOfClass:NSClassFromString(#"UITabBarButton")]) {
if (tabBarItemWidth == 0) {
tabBarItemWidth = view.frame.origin.x;
} else {
tabBarItemWidth = view.frame.origin.x - tabBarItemWidth;
break;
}
}
}
Why not use the width of first UITabBarButton? Because there are spaces between tab bar buttons. :)
You can get tabBarItem view using private property view. Then just get view frame.
- (CGRect)rectForTabBarItem:(UITabBarItem *)tabBarItem {
UIView *itemView = [tabBarItem valueForKey:#"view"];
return itemView.frame;
}
Loop through your tabBar's subviews and find the views with the class "UITabBarButton". These are the views for each tab item.
for (UIView *view in self.tabBar.subviews) {
if ([view isKindOfClass:NSClassFromString(#"UITabBarButton")]) {
// view.frame contains the frame for each item's view
// view.center contains the center of each item's view
}
}
I have an iPhone application that's using Navigation Controller to display the top bar (with title and back button, and such...).
I added a UITabBar to the application window, that enables to switch between the parts of it. Instead of adding the tab bar to each of ViewController's view I added the bar to app window.
(When I had it in the ViewController, switching between controllers made the tab bar to swipe left/right, when animated pop/push occured, together with whole view).
So, I added the UITabBar to the MainWindow.xib, and tied it to the app delegate's variable. In my didFinishLaunchingWithOptions method, I added the following code:
[self.window addSubview:navigationController.view];
CGRect frame = navigationController.view.frame;
frame.size.height -= tabbar.frame.size.height;
navigationController.view.frame = frame;
tabbar.selectedItem = [tabbar.items objectAtIndex:0];
to resize the main (navigationController's) view, in order to make the TabBar visible.
The problem shows up when I rotate the device -- my view gets stretched to full window and I loose the ability to show the TabBar.
I added a - (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation) fromInterfaceOrientation method to my ViewController, with the following code:
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
CGRect frame = self.view.frame;
frame.size.height -= [AppState shared].tabBar.frame.size.height;
//frame.origin.y = [AppState shared].tabBar.frame.size.height;
//frame.origin.x = 100;
self.view.frame = frame;
frame = [AppState shared].tabBar.frame;
frame.origin.y = [UIScreen mainScreen].bounds.size.height - frame.origin.y - frame.size.height;
[AppState shared].tabBar.frame = frame;
}
It resizes the view, and moves the tab bar to up/down part of the view (I allow only Portrait/Portrait upside down orientations here). The problem is, my TabBar is turned upside down as well, and also, it's no longer clickable.
It looks like the image below:
Anyone knows how to handle this kind of situation? Or, how to make the tab bar not tied to view controller, but also able to handle interface's rotation smoothly?
You are using the tabbar in an unintended way. You seem to be using the UITabBarView as an uncontrolled element of other views. That is not it's function.
The UITabBarView should be controlled directly by a UITabBarController which in turn should be controlling all the view controllers for the views displayed in the tabbar i.e. the tabbar controller is a type of navigation controller that controls subcontrollers.
Suppose you have three tabs and the third one is a navigation controller. Your controller hierarchy would look like this:
TabbarController:
-->tab1ViewController
-->tab2ViewController
-->tab3ViewController(UINavigationController):
-->rootViewController-->secondViewController
You are trying to move and manage the tabbar view without its controller and the proper controller hierarchy. That isn't going to work.
I have a bunch of 'rowviews' that I want to put in a vertical scroll view. I have created this rowView view as a separate nib in IB. They are sized at 1024/200 (ipad). Now I want to put them one by one in my parent UIScrollView. I tried a simple [vScroll addSubview:rowView] but this puts them overtop of eachother (I made the rowview transparent to check this). So then I started fooling with the bounds of each rowview to no avail. This is my code. Note 'self.yExtentSoFar' is initialised to 0. Imagine the code below called for each row:
MyRowView *rowView = [[MyRowView alloc] init];
float calculatedWidth = 0;
// minus nav bar
float calculatedHeight = 0;
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
UIInterfaceOrientation orientation = [UIDevice currentDevice].orientation;
if (orientation == UIInterfaceOrientationPortrait){
// iPad
calculatedWidth = 768.0;
calculatedHeight = 960.0;
}else{
// iPad
calculatedWidth = 1024.0;
calculatedHeight = 704.0;
}
[self.vScroll addSubview: rowView.view];
[rowView.view setBounds:CGRectMake(0, self.yExtentSoFar, calculatedWidth,200)];
[self.vScroll setContentSize:CGSizeMake(calculatedWidth, yExtentSoFar+200)];
self.yExtentSoFar += 200;
So before I tried settings bounds the rowviews appeared overtop of each other. Understandable I guess. When I set the bounds, the 2nd row view hasn't appeared under the 1st as expected, instead I have to pull down the vScroll and the 2nd has appeared ABOVE the first off screen!
Could someone point to where I'm going wrong? Thanks a lot,
Mike
You want to layout your subviews by setting their frame.
Specifically you're confusing the reference co-ordinates.. bounds refers to how much of that view to show. Whereas the frame is where (& what size) should the view be placed in it's superview.
See "The Relationship of the Frame, Bounds, and Center" View Programming Guide for iPhone
You're doing it wrong ;-) What you have explained here is more or less a re-implementation of what you get using a UITableView. Use a UITableView and a custom table view cell. It will make your life much easier.
I'm trying to create an iPhone application that is always in landscape mode, using the Utility application template. Here's what I did:
Create a new iPhone application project, using the Utility Application template
In Interface Builder, rotate all the views 90 degrees.
In Interface Builder, add a label to the middle of the MainView. Stretch it all the way across the view, set the alignment to centered, and set the autosizing springs so that it can stretch horizontally.
In Info.plist, add the key "UIInterfaceOrientation" with value "UIInterfaceOrientationLandscapeRight"
In the controller classes, change the shouldAutorotateToInterfaceOrientation methods to "return (interfaceOrientation == UIInterfaceOrientationLandscapeRight) || (interfaceOrientation == UIInterfaceOrientationLandscapeLeft);"
Run the app.
When I launch my app, it comes up in landscape orientation, but the main view only covers the top half of the display, and it is stretched horizontally. I get the same results in both the simulator and on an actual device. I've seen it with versions 2.2 and 2.2.1 of the SDK.
I have been able to work around the problem by adding the following step to the above:
Add "self.view.autoresizesSubviews = NO;" to RootViewController's viewDidLoad method after "[super viewDidLoad];".
If I do this, then it works as expected. But this feels like a hack. Why should this be necessary?
I don't think it is a transformation issue. All elements are drawn in the proper orientation and with the proper scaling. The problem seems to be that the bounds rectangles of the main view gets funky. It looks like the height of the main view is being cut by a little more than half, and the width is being increased by about 50%.
If I do the exact same set of steps using the View-based Application template instead of Utility, then everything works as expected. So I'm pretty sure the problem is specific to how a Utility application manages its views.
Anybody understand what's going on here?
I was going to say that setting this key does not rotate your interface; you still need to lay out your content in landscape mode and do the appropriate rotation using CFAffineTransform - see "Launching in Landscape Mode" in iPhone OS Programming Guide. Going to find the reference for you, I found this comment: "To launch a view controller–based application in landscape mode in versions of iPhone OS prior to v2.1, you need to apply a 90 degree rotation to the transform of the application’s root view in addition to all the preceding steps. Prior to iPhone OS 2.1, view controllers did not automatically rotate their views based on the value of the UIInterfaceOrientation key. This step is not necessary in iPhone OS 2.1 and later, however."
So if you're running pre-2.1, you need to add this code to your viewDidLoad method in your view controller. (Otherwise, can you post some code?)
-(void)viewDidLoad
// After loading the view, transform the view so that the co-ordinates are right for landscape
// As described in iPhone Application Programming Guide
// Weird, I'm sure this used to be needed, but it doesn't now. The one in CardScrollViewController is needed though.
{
[super viewDidLoad];
CGAffineTransform transform = self.view.transform;
CGPoint center = CGPointMake(kScreenHeight / 2.0, kScreenWidth / 2.0);
// Set the center point of the view to the center point of the window's content area.
self.view.center = center;
// Rotate the view 90 degrees around its new center point.
transform = CGAffineTransformRotate(transform, (M_PI / 2.0));
self.view.transform = transform;
}
Jane describes the setting of UIInterfaceOrientation to UIInterfaceOrientationLandscapeRight (or UIInterfaceOrientationLandscapeLeft), and the rotation settings recommended in the documentation, but I used a slightly different block of code (to the same end) in my root view controller:
- (void)loadView
{
UIView *primaryView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
primaryView.backgroundColor = [UIColor clearColor];
// Start in landscape orientation, and stay that way
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if (orientation == UIInterfaceOrientationLandscapeRight)
{
CGAffineTransform transform = primaryView.transform;
// Use the status bar frame to determine the center point of the window's content area.
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
CGRect bounds = CGRectMake(0, 0, statusBarFrame.size.height, statusBarFrame.origin.x);
CGPoint center = CGPointMake(60.0, bounds.size.height / 2.0);
// Set the center point of the view to the center point of the window's content area.
primaryView.center = center;
// Rotate the view 90 degrees around its new center point.
transform = CGAffineTransformRotate(transform, (M_PI / 2.0));
primaryView.transform = transform;
}
self.view = primaryView;
[primaryView release];
}
In addition to that, I implemented the following delegate method in my root view controller:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return ( (interfaceOrientation == UIInterfaceOrientationLandscapeLeft) || (interfaceOrientation == UIInterfaceOrientationLandscapeRight));
}
Finally, I was encountering weird glitches with the Simulator not auto-rotating properly, so I needed to implement the following delegate method in my UIApplicationDelegate:
- (void)application:(UIApplication *)application willChangeStatusBarOrientation:(UIInterfaceOrientation)newStatusBarOrientation duration:(NSTimeInterval)duration;
{
// This prevents the view from autorotating to portrait in the simulator
if ((newStatusBarOrientation == UIInterfaceOrientationPortrait) || (newStatusBarOrientation == UIInterfaceOrientationPortraitUpsideDown))
[application setStatusBarOrientation:UIInterfaceOrientationLandscapeRight animated:NO];
}
After all that, my application was able to start in landscape (right) and stay in that orientation under the 2.0 firmware and in the Simulator.
Try setting the orientation property of the view to Landscape in the nib. This property can be found in 4th tab[Attributes Inspector] of Info View of the UIView under Simulated Metrices.