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)
Related
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
I'm having some issues of figuring out when the auto-contraints setup on a XIB are applied in the view setup process.
For more explanation:
I've setup a XIB for a view
I set the "Simulated Metrics" Size to iPhone 3.5-Inch
I've added auto-constraints to the subviews inside this view
In the View Controller I perform certain operations dependent on the subview (IBOutlet) frames/bounds in the viewDidLoad method
In the View I perform certain operations dependent on the subview (IBOutlet) frames/bounds in the awakeFromNib method
In those 2 methods (ViewController::viewDidLoad and View::awakeFromNib) the IBOutlet views HAVE been loaded but the constraints have no yet been applied. The actual frame is still set to the iPhone 3.5" size (width 320) when using a larger simulator (such as the iPhone 6 simulator).
When are these auto-constraints applied and when should any necessary operations that would need the ACTUAL frame/bounds of the subviews take place?
I'm using XCode 6.3, Swift (1.2)
The constraints are applied in the layoutSubviews method. So if you want to do something after they are applied in a UIView subclass, override the method:
override func layoutSubviews() {
super.layoutSubviews()
//the frames have now their final values, after applying constraints
}
In a UIViewController subclass, use viewDidLayoutSubviews for the same purpose:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//the frames have now their final values, after applying constraints
}
Please note that you shouldn't set frame / bounds for a view if you added auto layout constraints to this view.
Note for Xcode 8:
If you use auto layout, you may have a problem with new feature: view as [any device]. When you test your app on another device(simulator) and have inferred screen, screen bounds for this device gonna be applied in viewWillLayoutSubviews.
you can use method
override func layoutSubviews() {
super.layoutSubviews()
//the frames have now their final values, after applying constraints
}
But this method will be called multiple times in a view's life cycle, such as orientation change or moving in and out views, so you need to handle this.
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 relatively new in the XCode/iOS world; I've done some decent sized storyboard based apps, but I didn't ever cut me teeth on the whole nib/xib thing. I want to use the same tools for scenes to design/layout a reusable view/control. So I created my first ever xib for my view subclass and painted it up:
I have my outlets connected and constraints setup, just like I'm used to doing in the storyboard. I set the class of my File Owner to that of my custom UIView subclass. So I assume I can instantiate this view subclass with some API, and it will configured/connected as shown.
Now back in my storyboard, I want to embed/reuse this. I'm doing so in a table view prototype cell:
I've got a view. I've set the class of it to my subclass. I've created an outlet for it so I can manipulate it.
The $64 question is where/how do I indicate that it's not enough to just put an empty/unconfigured instance of my view subclass there, but to use the .xib I created to configure/instantiate it? It would be really cool, if in XCode6, I could just enter the XIB file to use for a given UIView, but I don't see a field for doing that, so I assume I have to do something in code somewhere.
(I do see other questions like this on SO, but haven't found any asking for just this part of the puzzle, or up to date with XCode6/2015)
Update
I am able to get this to kind of work by implementing my table cell's awakeFromNib as follows:
- (void)awakeFromNib
{
// gather all of the constraints pointing to the uncofigured instance
NSArray* progressConstraints = [self.contentView.constraints filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(id each, NSDictionary *_) {
return (((NSLayoutConstraint*)each).firstItem == self.progressControl) || (((NSLayoutConstraint*)each).secondItem == self.progressControl);
}]];
// fetch the fleshed out variant
ProgramProgressControl *fromXIB = [[[NSBundle mainBundle] loadNibNamed:#"ProgramProgressControl" owner:self options:nil] objectAtIndex:0];
// ape the current placeholder's frame
fromXIB.frame = self.progressControl.frame;
// now swap them
[UIView transitionFromView: self.progressControl toView: fromXIB duration: 0 options: 0 completion: nil];
// recreate all of the constraints, but for the new guy
for (NSLayoutConstraint *each in progressConstraints) {
id firstItem = each.firstItem == self.progressControl ? fromXIB : each.firstItem;
id secondItem = each.secondItem == self.progressControl ? fromXIB : each.secondItem;
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem: firstItem attribute: each.firstAttribute relatedBy: each.relation toItem: secondItem attribute: each.secondAttribute multiplier: each.multiplier constant: each.constant];
[self.contentView addConstraint: constraint];
}
// update our outlet
self.progressControl = fromXIB;
}
Is this as easy as it gets then? Or am I working too hard for this?
You're almost there. You need to override initWithCoder in your custom class you assigned the view to.
- (id)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super initWithCoder:aDecoder])) {
[self addSubview:[[[NSBundle mainBundle] loadNibNamed:#"ViewYouCreated" owner:self options:nil] objectAtIndex:0]];
}
return self; }
Once that's done the StoryBoard will know to load the xib inside that UIView.
Here's a more detailed explanation:
This is how your UIViewController looks like on your story board:
The blue space is basically a UIView that will "hold" your xib.
This is your xib:
There's an Action connected to a button on it that will print some text.
and this is the final result:
The difference between the first clickMe and the second is that the first was added to the UIViewController using the StoryBoard. The second was added using code.
You need to implement awakeAfterUsingCoder: in your custom UIView subclass. This method allows you to exchange the decoded object (from the storyboard) with a different object (from your reusable xib), like so:
- (id) awakeAfterUsingCoder: (NSCoder *) aDecoder
{
// without this check you'll end up with a recursive loop - we need to know that we were loaded from our view xib vs the storyboard.
// set the view tag in the MyView xib to be -999 and anything else in the storyboard.
if ( self.tag == -999 )
{
return self;
}
// make sure your custom view is the first object in the nib
MyView* v = [[[UINib nibWithNibName: #"MyView" bundle: nil] instantiateWithOwner: nil options: nil] firstObject];
// copy properties forward from the storyboard-decoded object (self)
v.frame = self.frame;
v.autoresizingMask = self.autoresizingMask;
v.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
v.tag = self.tag;
// copy any other attribtues you want to set in the storyboard
// possibly copy any child constraints for width/height
return v;
}
There's a pretty good writeup here discussing this technique and a few alternatives.
Furthermore, if you add IB_DESIGNABLE to your #interface declaration, and provide an initWithFrame: method you can get design-time preview to work in IB (Xcode 6 required!):
IB_DESIGNABLE #interface MyView : UIView
#end
#implementation MyView
- (id) initWithFrame: (CGRect) frame
{
self = [[[UINib nibWithNibName: #"MyView"
bundle: [NSBundle bundleForClass: [MyView class]]]
instantiateWithOwner: nil
options: nil] firstObject];
self.frame = frame;
return self;
}
A pretty cool and reusable way of doing this Interface Builder and Swift 4:
Create a new class like so:
import Foundation
import UIKit
#IBDesignable class XibView: UIView {
#IBInspectable var xibName: String?
override func awakeFromNib() {
guard let name = self.xibName,
let xib = Bundle.main.loadNibNamed(name, owner: self),
let view = xib.first as? UIView else { return }
self.addSubview(view)
}
}
In your storyboard, add a UIView that will act as the container for the Xib. Give it a class name of XibView:
In the property inspector of this new XibView, set the name of your .xib (without the file extension) in the IBInspectable field:
Add a new Xib view to your project, and in the property inspector, set the Xib's "File's Owner" to XibView (ensure you've only set the "File's Owner" to your custom class, DO NOT subclass the content view, or it will crash), and again, set the IBInspectable field:
One thing to note: This assumes that you're matching the .xib frame to its container. If you do not, or need it to be resizable, you'll need to add in some programmatic constraints or modify the subview's frame to fit. I use snapkit to make things easy:
xibView.snp_makeConstraints(closure: { (make) -> Void in
make.edges.equalTo(self)
})
Bonus points
Allegedly you can use prepareForInterfaceBuilder() to make these reusable views visible in Interface Builder, but I haven't had much luck. This blog suggests adding a contentView property, and calling the following:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
xibSetup()
contentView?.prepareForInterfaceBuilder()
}
You just have to drag and drop UIView in your IB and outlet it and set
yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:#"yourUIViewClass" owner:self options:nil] firstObject];
[self.view addSubview:yourView]
Step
Add New File => User Interface => UIView
Set Custom Class - yourUIViewClass
Set Restoration ID - yourUIViewClass
yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:#"yourUIViewClass" owner:self options:nil] firstObject];
[self.view addSubview:yourView]
Now you can customize view as you want.
I've been using this code snippet for years. If you plan on having custom class views in your XIB just drop this in the .m file of your custom class.
As a side effect it results in awakeFromNib being called so you can leave all your init/setup code in there.
- (id)awakeAfterUsingCoder:(NSCoder*)aDecoder {
if ([[self subviews] count] == 0) {
UIView *view = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil][0];
view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.alpha = self.alpha;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
return view;
}
return self;
}
Create xib file
File > New > New File > iOS > User Interface > View
Create custom UIView class
File > New > New File > iOS > Source > CocoaTouch
Assign the xib file's identity to the custom view class
In viewDidLoad of the view controller initialize the xib and its associated file using loadNibNamed: on NSBundle.mainBundle and the first view returned can be added as a subview of self.view.
The custom view loaded from the nib can be saved to a property for setting the frame in viewDidLayoutSubviews. Just set the frame to self.view's frame unless you need to make it smaller than self.view.
class ViewController: UIViewController {
weak var customView: MyView!
override func viewDidLoad() {
super.viewDidLoad()
self.customView = NSBundle.mainBundle().loadNibNamed("MyView", owner: self, options: nil)[0] as! MyView
self.view.addSubview(customView)
addButtonHandlerForCustomView()
}
private func addButtonHandlerForCustomView() {
customView.buttonHandler = {
[weak self] (sender:UIButton) in
guard let welf = self else {
return
}
welf.buttonTapped(sender)
}
}
override func viewDidLayoutSubviews() {
self.customView.frame = self.view.frame
}
private func buttonTapped(button:UIButton) {
}
}
Also, if you want to talk back from the xib to your UIViewController instance then create a weak property on the custom view's class.
class MyView: UIView {
var buttonHandler:((sender:UIButton)->())!
#IBAction func buttonTapped(sender: UIButton) {
buttonHandler(sender:sender)
}
}
Here's the project on GitHub
A little bit more swifty version of #brandonscript 's idea with early return:
override func awakeFromNib() {
guard let xibName = xibName,
let xib = Bundle.main.loadNibNamed(xibName, owner: self, options: nil),
let views = xib as? [UIView] else {
return
}
if views.count > 0 {
self.addSubview(views[0])
}
}
While I don't recommend the path you're going down it can be done by placing an "embedded view controller view" where you want the view to appear.
Embed a view controller that contains a single view -- the view you want to be reused.
It's been a while on this one, and I've seen a number of answers go by. I recently revisited it because I had just been using UIViewController embedding. Which works, until you want to put something in "an element repeated at runtime" (e.g. a UICollectionViewCell or a UITableViewCell). The link provided by #TomSwift led me to follow the pattern of
A) Rather than make the parent view class be the custom class type, make the FileOwner be the target class (in my example, CycleControlsBar)
B) Any outlet/action linking of the nested widgets goes to that
C) Implement this simple method on CycleControlsBar:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if let container = (Bundle.main.loadNibNamed("CycleControlsBar", owner: self, options: nil) as? [UIView])?.first {
self.addSubview(container)
container.constrainToSuperview() // left as an excise for the student
}
}
Works like a charm and so much simpler than the other approaches.
Here's the answer you've wanted all along. You can just create your CustomView class, have the master instance of it in a xib with all the subviews and outlets. Then you can apply that class to any instances in your storyboards or other xibs.
No need to fiddle with File's Owner, or connect outlets to a proxy or modify the xib in a peculiar way, or add an instance of your custom view as a subview of itself.
Just do this:
Import BFWControls framework
Change your superclass from UIView to NibView (or from UITableViewCell to NibTableViewCell)
That's it!
It even works with IBDesignable to render your custom view (including the subviews from the xib) at design time in the storyboard.
You can read more about it here:
https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155
And you can get the open source BFWControls framework here:
https://github.com/BareFeetWare/BFWControls
And here's a simple extract of the code that drives it, in case you're curious:
https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4
Tom 👣
The "correct" answer is that you are not meant to make re-usable views with corresponding nibs. If a view subclass is valuable as a reusable object it rarely will need a nib to go with it. Take for example every view subclass provided by UIKit. Part of this thinking is a view subclass that is actually valuable wont be implemented using a nib, which is the general view at Apple.
Usually when you use a view in nib or storyboard you will want to tweak it graphically for the given use case anyway.
You might consider using "copy paste" for recreating same or similar views instead of making separate nibs. I this believe accomplishes the same requirements and it will keep you more or less in line with what Apple is doing.