Our project runs for all iPAD's and we came across a problem as an example we have 8 buttons vertically sitting on a screen with constraints being added as vertical spacing they look fine on a iPAD 9.7 inch, but they look really big on iPAD 12.9, so the question is, is there any good way to actually use the screen space for something better, as in add an extra UIView if it is iPAD 12.9. I have looked into working with size classes, but I believe there is one size class for all iPADs, what I want is if there is a way to have different UI for different iPAD sizes using the Interface builder
The way I imagine your situation is that this specific ViewController has a lot of shared stuff, (like a common top bar, or a navigation bar) but just the middle content doesn't seem to fit properly.
In cases like this it is recommended to have a custom UIView that will load a different xib file based on the height or width of the current device.
The key point here is that, you actually only need one UIView subclass.
For this you can use #IBDesignable as well to preview it in real time inside the interface builder.
To achieve this you have to follow the next steps.
1) Create a .xib file for each of your "UIViews" based on the sizes.
2) Create an UIView subclass.
3) Hook the properties from the interface builder to this subclass. Note that you have to repeat this process for each of the xib files you want to use. IMPORTANT: Even though they are different xibs, they all get hooked into the same class.
4) Mark the created class as #IBDesignable like this.
#IBDesignable class CustomView: UIView {
...
}
5) Add the following code inside your class that will load a different xib based on whatever criteria you chose.
////////// View Logic //////////
// Our custom view from the XIB file
var view: UIView!
func xibSetup() {
view = loadViewFromNib()
// use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(view)
}
func setup()
{
// Extra setup goes here
}
func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
let nib : UINib
let screenRect : CGRect = UIScreen.main.bounds;
// Use a different Nib based on the current screen height
if screenRect.size.height == 1024 {
// iPad Air & Pro
nib = UINib(nibName: "iPadNormal", bundle: bundle)
}
else if screenRect.size.height == 1366 {
// Large iPad Pro
nib = UINib(nibName: "iPadLarge", bundle: bundle)
}
else {
// Fall back
nib = UINib(nibName: "iPadFallback", bundle: bundle)
}
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
override init(frame: CGRect) {
// 1. setup any properties here
// 2. call super.init(frame:)
super.init(frame: frame)
// 3. Setup view from .xib file
xibSetup()
// 4. Other Setup
setup()
}
required init?(coder aDecoder: NSCoder) {
// 1. setup any properties here
// 2. call super.init(coder:)
super.init(coder: aDecoder)
// 3. Setup view from .xib file
xibSetup()
// 4. Other Setup
setup()
}
////////////////
IMPORTANT:
This approach is ONLY recommended when the contents of the custom view are the same, or with very minimal changes (layout doesn't matter). If the contents change a lot between sizes (like you actually wanna display different things) then you should create a "BaseViewController" with your shared logic, and make a subclass per iPad Size, so that each of them has it's own ViewController + Interface Builder Screen. Then just load the required screen just as if it was a completely different screen.
Just put constraints as i suggested below Have a look on the Sample i have created for you.
Give your Button Aspect ratio.
Give width equal to the parent View's width.
Add Constraint Center Horizontally in container and Center Vertically in container.
Select Your Button and then go to size inspector and set the multiplier for the width (like 0.3 or 0.4 etc).
Then set the multiplier for "align center X to.." (if you want your button to be left of center then set multiplier below 1 like 0.8, 0.7,0.5 etc And if you want your button to be in the right of the center then set the multiplier value bigger than 1 like 1.2 etc).
Set the multiplier value for "Align center y to.." as like in Step 5.
Its all done.. Just run and check.
Hope this will help you.
You can use AutoLayout for this. You can assign aspect ratio base height to your buttons. Remember there is an option of setting ratio relationship to be 'equal to' or 'less than equal to.' You could use these instead of giving exact constraints. So this would cause button to adapt heights. Then in the code you could simply check if there is enough space remaining to draw an extra uiview or not. This will solve the 'Looking really big' problem also.
Check IB screenshot here to see what i am referring to:
Click on 'Relation' to see these settings
Assume your nibs are Foo_pro.xib and Foo_ipad.xib.
#implementation Util
+(BOOL) isIpad_1024
{
if ([UIScreen mainScreen].bounds.size.height == 1024) {
return YES;
}
return NO;
}
+(BOOL) isIpadPro_1366
{
if ([UIScreen mainScreen].bounds.size.height == 1366) {
return YES;
}
return NO;
}
+(NSString *)convertXib:(NSString *)nibName {
if([Util isiPadPro_1366]) {
return [NSString stringWithFormat:#"%#_pro", nibName];
} else {
return [NSString stringWithFormat:#"%#_ipad", nibName];
}
}
Foo *foo = [[Foo alloc] initWithNibName:[Util convertXib:#"Foo"] bundle:nil]
Try This
1.Remove constrains from all of your buttons
Add all of your buttons into a stackview
Put vertical constrains on your stackview
set same height same width for stackview buttons
Remove height if you set for any button
Don't set height for stackview if you want it to run on different devices.
You can use this
All buttons embeded into view and set to its constraints to superview.
make sure add a aspect ratio constraints to it.
Select all buttons and add constraints
Equal width ,Equal Height, Aspect ratio
Related
I have a view that I assign as the inputAccessoryView of my view controller. I adjusted the constraints so that it's above the home indicator on iPhone X (X, XR, XS, XS Max). I keep that inputAccessoryView pinned to the bottom of the screen when the keyboard is hidden by setting canBecomeFirstResponder to true in my view controller.
It works on all phones except for the X family. On all of the X phones, my view is not getting any touch events - they are hitting the view below it, as if inputAccessoryView wasn't there.
If I constrain the bottom to superview instead of safeArea, it works, but then it's too close to the home indicator, which I don't want:
In this radar ("inputAccessoryViews should respect safe area inset with external keyboard on iPhone X"), Apple engineers say that this is not a bug and that developers should constrain one of the views to its parent's safeAreaLayoutGuide:
Engineering has provided the following feedback regarding this issue: It’s your responsibility to respect the input accessory view’s safeAreaInsets. We designed it this way so developers could provide a background view (i.e., see Safari’s Find on Page input accessory view) and lay out the content view with respect to safeAreaInsets. This is fairly straightforward to accomplish. Have a view hierarchy where you have a container view and a content view. The container view can have a background color or a background view that encompasses its entire bounds, and it lays out it’s content view based on safeAreaInsets. If you’re using autolayout, this is as simple as setting the content view’s bottomAnchor to be equal to it’s superview’s safeAreaLayoutGuide.
I believe that's exactly what I'm doing, but I'm clearly missing something since it breaks on X.
GitHub project is here: https://github.com/nambatee/HorizontallyScrollableToolbarAccessoryView/tree/master/Horizontally%20Scrollable%20Toolbar%20Accessory%20View
Your problem is actually is that you are not setting a height for inputAccessoryView and your scrollView is out of bounds that's why you are not getting the touch events and just to prove that if you go to your top level view and set it to clips to bounds and run your project this is what you get
So why is this happening? it's because you view is not having an intrinsicContentSize you can give it an intrinsicContentSize by subclassing UIView and override intrinsicContentSize and return what ever width and height you want just like this
class CustomView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize.zero
}
}
Don't forget to set your view to be CustomView in code and in the nib file
private lazy var horizontallyScrollableToolbarAccessoryView: CustomView? = {
let nibName = "HorizontallyScrollableToolbarAccessoryView"
let view = Bundle.main.loadNibNamed(nibName, owner: nil, options: nil)?.first as? CustomView
return view
}()
you can also provide the height through a height constraint like this
override func viewDidLoad() {
super.viewDidLoad()
self.horizontallyScrollableToolbarAccessoryView?.heightAnchor.constraint(equalToConstant: 80).isActive = true
}
but you will still need to set your intrinsicContentSize to be CGSize.Zero
class CustomView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize.zero
}
}
and that's what you get at the end a working scrollView
I would like to use the same nib file for both iPad and iPhone.
I found there are two black bars in the top and bottom when I run the app on iPhone 6 plus. So I found there is a fixed size defined in my xib file, and they are not editable.
How should I make it adapt to different screen size?
Note:
* The project doesn't use autolayout and storyboard
On viewWillAppear set frame of your xib file.
swift
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.frame = UIScreen.main.bounds
}
Objective-C
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[self view] setFrame:[[UIScreen mainScreen] bounds]];
}
There is no way around it, you need to use autolayout and size classes:
https://developer.apple.com/library/ios/recipes/xcode_help-IB_adaptive_sizes/chapters/AboutAdaptiveSizeDesign.html
You can use nib files, that's fine, but autolayout and size classes are unavoidable if you're going to make an app for all devices.
You have to unable all the Auto Resizing masks at XIB
In ur picture, u also need to set Width & Height Auto Resizing masks..So that the xib/view will take the width & Height as per super view or fill the screen.
For more info about Auto Resizing Masks refer below link..
Autoresizing masks programmatically vs Interface Builder / xib / nib
Hope it helps you..
I want to create simple custom view and use it for navigationItem.titleView. My custom view is very simple, has only image view and label: [image]-[label]. I have xib file where I have defined all constraints (both subviews has constraint to superview - I want to height and width of subviews determine height and width of entire custom view). The problem is that when I instantiate view from xib like this:
class func titleViewWithTitle(title: String, icon: UIImage?) -> TitleView {
let titleView = NSBundle.mainBundle().loadNibNamed("TitleView", owner: self, options: nil).first as TitleView
titleView.title.text = title
titleView.icon.image = icon
titleView.layoutSubviews()
return titleView
}
the size of this view is not determined by it's children. It has the size that was manually set in Interface Builder. What can I do to force the view to dynamically calculate size?
In your custom view, override the layoutSubviews method and add this:
CGSize targetSize = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
Create a CGRectFrame from this and set this frame as a new frame for self (which is the custom view).
This will basically find the smallest possible frame that still encompasses its content in a way that all content is shown.
Note: All subviews of this custom view must have autolayout compression resistance set properly. I.e. they must resist shrinking.
I have a nib that has an init method:
override init() {
super.init();
self.view = NSBundle.mainBundle().loadNibNamed("myNib", owner: self, options: nil).first as? UIView;
self.addSubview(self.view);
}
In a UITableView cell, I'm loading the nib like this:
override func awakeFromNib() {
self.myInnerNib = myNib();
self.nibContainerView?.addSubview(self.myInnerNib!);
}
The nib is freeform, and its constraints are set to fill the parent view's container. However, when I load it on a bigger device, it is clear that the view doesn't fill it's parent.
When nibs are loaded, you receive an array of views, and you generally take the top level view and add it as a subview. Is it possible that there is another view between the container and my nib that is preventing the nib from resizing correctly?
EDIT
I was overriding my init with frame method where i was forcing the subviews in the nib to set their frames instead of letting them be implicitly inferred
override init(frame: CGRect) {
super.init(frame: frame);
self.view = NSBundle.mainBundle().loadNibNamed("DayLineGraphView", owner: self, options: nil).first as? UIView;
self.view.frame = frame; <- problem
self.innerView.frame = frame; <-problem
self.addSubview(self.view);
setupGraph()
}
You have identified the problematic lines properly:
self.view.frame = frame; <- problem
self.innerView.frame = frame; <-problem
What you are doing here is setting the view frame and setting the innerView frame to the same thing. The reason why this is wrong is that the origin point on the innerView most likely will be incorrect (not always, but generally). If the innerView is to be filling the view you must make sure the origin is {0, 0}. An easy way to do this is use the view's bound property:
self.view.frame = frame;
self.innerView.frame = self.view.bounds;
This should take care of your issue.
On another note about constraints. In this case the "container" view may have the proper constraints in IB, but you are adding a subview to it, which needs constraints of its own. So the "container" view will need to establish constraints for the "subview". Since you are adding the subview programmatically the constraints must be set programmatically as well. The reason why you probably suspect this is incorrect is because of the convenient translatesAutoresizingMaskIntoConstraints boolean. This will allow iOS to make educated guesses on what it believes the constraints should be. Since you are setting the sizes to be equivalent it sets up constraints to fill the parent... But constraints are still being establishes at runtime (i.e. Not in IB).
I'm loading a view from a xib using Autolayout and Size Classes. Inside that view there's a subview, viewWithSizeClasses, with a constraint on its height that depends on the size class.
What I'm trying to do is to load constraints right after loadNibNamed in order to get the proper height according to the current Size Class.
I tried various combinations of layoutSubviews(), updateConstraints() but no matter what I do I'm always getting the default Any, Any height.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let xibView = NSBundle.mainBundle().loadNibNamed("View", owner: self, options: nil).first as View
self.view.addSubview(xibView)
height = xibView.viewWithSizeClasses.frame.size.height // <- Any, Any height
}
I'm deploying on iOS8 or newer.
I ran into the same trouble trying to use the same xib with separate constraints for iPhone vs iPad. My solution was to create a view for each size class within the same xib. Then when loading the xib, access the proper view by index. In my case, my Any-Any size class is at index 0 and Regular-Regular at index 1.
NSArray *views = [NSBundle.mainBundle loadNibNamed:NSStringFromClass(self.class) owner:self options:nil];
UIView *view = views[IS_IPAD];
I'm sure you can translate that to Swift. I've also defined a global macro
#define IS_IPAD (UI_USER_INTERFACE_IDIOM()==UIUserInterfaceIdiomPad)