I'm trying to create custom reusable view, lets say QuestionView. Now I use this class to be extended by my QuestionView, so it loads view from my xib, then this view added as subview to self. It works ok in case if my view has constant height and width, but I need kind of this layout
This view's file's owner set to QuestionView.
I have label on top which connected with top, left and right via constraints but it's flexible in terms of height - label is multiline. Yes/No buttons view connected to bottom of label, left and right of superview and has constant height. Details view connected to bottom of buttons view, to left and to right, has constant height. So my QuestionView has flexible height. If I change text of label to 2 lines for example, my view should be stretched.
I have ViewController xib, where I put generic view and set its class to QuestionView.
I just add this view as subview of QuestionView so I think there is a problem with constraints between view and subview, I should add them? I tried to add left, right, top, bottom constraints between them with translatesAutoresizingMaskIntoConstraints set to false but anyway got strange (similar to height from xibs) superview(QuestionView) height, subview height is ok in runtime.
So what am I doing wrong here? Do I need to bind subview height to superview height somehow differently?
UPD. Here is screenshot in runtime, gray view is it's size in runtime, should be stretched to TextField bottom. Now it looks like it was false effect of ok subview height in runtime.
Here is my code now
import UIKit
protocol NibDefinable {
var nibName: String { get }
}
#IBDesignable
class NibLoadingView: UIView, NibDefinable {
var containerView: UIView!
var nibName: String {
return String(self.dynamicType)
}
override init(frame: CGRect) {
super.init(frame: frame)
nibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
nibSetup()
}
private func nibSetup() {
//clipsToBounds = true
containerView = loadViewFromNib()
containerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(containerView)
addConstraint(.Top)
addConstraint(.Left)
addConstraint(.Bottom)
addConstraint(.Right)
}
private func addConstraint(attribute: NSLayoutAttribute) {
self.addConstraint(NSLayoutConstraint(item: self,
attribute: attribute,
relatedBy: .Equal,
toItem: containerView,
attribute: attribute,
multiplier: 1,
constant: 0.0
))
}
private func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let nibView = nib.instantiateWithOwner(self, options: nil).first as! UIView
return nibView
}
}
First, you have to add a constraint between QuestionView and bottom of it's superview
Second, looks like problem might be with QuestionView's superview and its parent constraints.
UPD
the best tool to nail those kind of bugs - Reveal
Related
I have a custom view and xib. I use this custom view in one of my storyboard's controller views.
I have a use case where I want to be able to hide the custom view (and bring its height to zero). Right now, I set the height in the interface builder and set constraints to the superview's edges:
As you can see, I want its height to be 84 everywhere.
Now here is my custom view's class:
import UIKit
#IBDesignable
class BannerView: UIView {
#IBOutlet var contentView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override func awakeFromNib() {
super.awakeFromNib()
initialize()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
initialize()
contentView?.prepareForInterfaceBuilder()
}
func initialize() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "BannerView", bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
func hide() {
// Hide the view and set its height to zero here
}
}
But, now I'm confused... should I also be setting a height constraint on the custom view when I load it into one of my storyboards? Or should its height be 84 everywhere and I shouldn't have to specify it any further?
Also, how would I hide the custom view and set its height to zero in the above hide() function?
There are several ways to do this... here's one.
Give the content of your xib constraints to make its height 84-pts. You haven't shown your xib's layout, but I'll assume you know how to do that.
Then, when you add BannerView to your main view (I'm guessing that's what you're doing), embed it in a Vertical UIStackView with these properties:
Now, when you set bannerView.isHidden = true, the stack view automatically removes it from the height calculations, resulting in the stack view having a height of Zero.
Setting bannerView.isHidden = false will then re-display the banner view along with its height.
As you want the view's height to be 84 everywhere I think you should add a height constraint outlet and set the value 84.
Set the constraint value to 0 to hide the view (I highly suggest not to hide some view by doing this).
I have a custom view. I am trying to add this custom view and center it in my ViewController. I created the view in storyboard but am adding it to my ViewController programmatically. The init function requires that I give a frame. I don't want to specify a frame because I want the view to be autosized based on what content is in the view controller and then I just want to use my constraints to center the view.
This is the code within my viewcontroller that I use to add my custom view
let reportWindow = ReportWindow(user: self.uid!, frame: CGRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(reportWindow)
let centerX = NSLayoutConstraint(item: reportWindow, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let centerY = NSLayoutConstraint(item: reportWindow, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([centerX, centerY])
This is the code for my custom view class
class ReportWindow: UIView{
var uid: String
#IBOutlet weak var title: UILabel?
#IBOutlet weak var textView: UITextView!
#IBOutlet weak var contentView: UIView!
init(user uid: String, frame: CGRect) {
self.uid = uid
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#IBAction func cancel(_ sender: UIButton) {
self.removeFromSuperview()
}
#IBAction func confirm(_ sender: UIButton) {
self.removeFromSuperview()
}
}
extension ReportWindow{
func commonInit() {
let view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.RawValue(UInt8(UIView.AutoresizingMask.flexibleWidth.rawValue) | UInt8(UIView.AutoresizingMask.flexibleHeight.rawValue)))
self.addSubview(view)
textView.delegate = self
textView.textAlignment = .center
self.translatesAutoresizingMaskIntoConstraints = false
}
func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "ReportWindow", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
}
This is what my custom view looks like in storyboard
And this is what my custom view looks like while the application is running, the background is missing, and I cannot interact with the buttons or the text view. I've read that this is because my content is outside of my frame. I want the frame to auto place itself around all my content though, I don't want to have to specify the size and position of the frame.
You're going to have a much easier time with this if you simply present the view inside a modal view controller programatically or using a segue.
Copy your view to a new view controller and present that view controller modally. If you do so using a segue, you will need to set the uid of the view controller inside your preformSegue function. Otherwise, you would set it where you present the view controller. You can handle the button tap inside that view controller.
If that doesn't achieve the look you're going for (a floating view) by default, you can set the new view controller's view to be clear and have a smaller view (your already defined view) anchored to its center.
As a sidenote, I would recommend renaming your class to ReportUserView as it is not a UIWindow. It is a UIView, which is what you want. Windows are a bit different in iOS. You usually only have one and just deal with views and controllers inside it.
I have a collectionView Cell with a StackView in it.
class OuterCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var stackView: UIStackView!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
for i in 1...30 {
let tick = UIView()
if i % 2 == 0 {
tick.backgroundColor = UIColor.red
} else {
tick.backgroundColor = UIColor.clear
}
tick.tag = i
tick.heightAnchor.constraint(equalTo: self.heightAnchor, constant: 0.0).isActive = true
self.stackView?.insertArrangedSubview(tick, at:i)
}
}
I want to draw 30 cells on the stackView alternating between red and clear . Right now I am setting the index of the subview as well as the height with a auto layout anchor constraint and hoping that the .fillEqually distribution will compute the width and X locations of each of the subviews. However right now, it crashes when I try and set the height anchor and simply doesn't show anything when I remove the height anchor. How do I lay these out such that they are full height, and right next to each other with no margins?
I think that you shouldn't be adding a height anchor here to items within your stackview. Automatically, things contained in the stack view are equal heights (or widths depending on stackview orientation). You shouldn't need to be setting sizes within the stackview directly. It defeats the purpose of the stackview in the first place.
A custom view is located in IB by dragging UIView object from library and it's class name is set to the custom view class name. This custom view has a subview, which is added in init?(coder aDecoder: NSCoder).
How should anchor type constraints be composed, and where should they be located so the _centralLabel subview is set centrally in the custom view, and it's dimensions are 25% of the custom's view dimensions?
If the code is written like this:
override func updateConstraints() {
if !_subviewsConstraintsAdded {
_centralLabel.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.25).isActive = true
_centralLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.25).isActive = true
_centralLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
_centralLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
_subviewsConstraintsAdded = true
}
super.updateConstraints()
}
The result of the code is, that instead of setting the size of _centralLabel to be 25% of the custom view, the custom view is shrunk to (0,0) which is the size of _centralLabel while updateConstraints() is being called.
The missing bit was that:
_centralLabel.translatesAutoresizingMaskIntoConstraints = false
which should be located somewhere after programmatically instantiating the subview with this line:
_centralLabel = UILabel()
After this correction the _centralLabel subview has enlarged and has moved to the center of the view, while the view itself remained in its original size, shape and position.
I am trying to create a UIView(A) containing 2 custom Views (B) in it
View B is setup using Autolayout Constraints and made in Interface Builder, including the constraints. A is added in the Nib of the viewController
B
- UIImageView(Leading = 10, Trailing = 10, AlignVertically)
- UITextField(Leading = 10, Trailing = 10, AlignVertically)
ViewController
A(300x300, AlignHorizontally, AlignVertically)
In the ViewController I have A to be fixed at 300x300 and
B1 and B2 has its Leading, Trailing, Top and Bottom pinned at 0. (which should make B1 and B2 to be 300x150, forgive me if I miss something out)
When loading View B I use the following code to load its Nib:
override func awakeAfterUsingCoder(aDecoder: NSCoder!) -> AnyObject! {
if self.subviews.count == 0 {
let bundle = NSBundle(forClass: self.dynamicType)
var view = bundle.loadNibNamed("B", owner: nil, options: nil)[0] as B
view.setTranslatesAutoresizingMaskIntoConstraints(false)
let constraints = self.constraints()
self.removeConstraints(constraints)
view.addConstraints(constraints)
return view
}
return self
}
But when I try to run this I get the following warning including a crash:
The view hierarchy is not prepared for the constraint: <NSLayoutConstraint:0x7f897ad1acc0 V:[TestProject.B:0x7f897af73840(300)]>
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView _viewHierarchyUnpreparedForConstraint:] to debug.
I have also tried adding the view as a property of View B and use the following code to add it to B
NSBundle.mainBundle().loadNibNamed("B", owner: self, options: nil)
self.addSubview(self.viewOfB);
the result of this is the view is added to the viewController, but it is not adopting any of the AutoLayoutConstraints from its own Nib.
Right now I have no idea how to add this view to the viewController's view including the constraints. What am I doing wrong? Is there a better way to do this?
PS: View A used to be custom too.
PPS: I am using Swift to do this, but I'm sure solutions in Objective-C works as well.
First of all the error you are getting is because
you can't swap or move constraints between your views
the views must already be added in the hierarchy before adding new constraints
the constraints are usually added on the parent view that holds the views (or common ancestor).
(for more details see AutoLayout Guide)
I would suggest having view A (CustomView : UIView) loading the contents (B Views) from the nib file and adding them as it's subviews.
The trick is that you need to layout you B views inside a content view so that your nib file has only one root object in it.
This way, when you add the content view as the view A subview, all constraints will transfer over(similar with how UITableViewCells use their contentView).
Important Note: The content view should NOT be of the class type CustomView. If you want to drag outlets and actions just declare the File's Owner object of the class CustomView and link them there.
Your final view hierarchy should look like this:
MainView (ViewController's view)
View A
Content View
B1 View
B2 View
This way view A has it's layout configured in the ViewController's storyboard/xib and you B views will use the constraints from the nib file related to the content view.
So the only thing let is to make sure is that the content view will always have the same size with view A.
class CustomView: UIView
{
#IBOutlet var _b1Label: UILabel!
#IBOutlet var _b2Button: UIButton!
func loadContentView()
{
let contentNib = UINib(nibName: "CustomView", bundle: nil)
if let contentView = contentNib.instantiateWithOwner(self, options: nil).first as? UIView
{
self.addSubview(contentView)
// We could use autoresizing or manually setting some constraints here for the content view
contentView.translatesAutoresizingMaskIntoConstraints = true
contentView.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
contentView.frame = self.bounds
}
}
override init(frame: CGRect)
{
super.init(frame: frame)
loadContentView()
}
required init(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder);
loadContentView()
}
}
The problem is: constraints retrieved from self are referring to self.
Instead, you have to make new constraints referring to view and with same other properties. like this:
NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in self.constraints) {
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
if(firstItem == self) firstItem = view;
if(secondItem == self) secondItem = view;
[constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant]];
}
/* if you also have to move subviews into `view`.
for(UIView *subview in self.subviews) {
[view addSubview:subview];
}
*/
[view addConstraints:constraints];
And, I think you should copy a few more properties from self.
view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
Sorry for Obj-C code, I copied from this answer :)