Custom View - self.frame is not correct? - ios

So I have a custom UIView class
class MessageBox: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
createSubViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createSubViews()
}
func createSubViews() {
let testView = UIView(frame: self.frame)
testView.backgroundColor = UIColor.brown
self.addSubview(testView)
}
}
I added a UIView inside the storyboard and gave it some constraints:
100 from the top (superview), 0 from the left and right, height is 180
But when I run the app the brown subview I created in the code is way to big. I printed self.frame in my custom view and it turns out that the frame is (0,0,1000,1000). But why? I set constraints, it should be something like (0,0,deviceWith, 180).
What did I do wrong?
EDIT: That's my Storyboard setup:

Short and simple answer:
You're doing it too early.
Detailed answer:
When a view is initialized from an Interface Builder file (a xib or a storyboard) its frame is initially set to the frame it has in Interface Builder. You can look at it as a temporary placeholder.
When using Auto Layout the constraints are resolved (= the view's actual frame is computed) inside the view's layoutSubviews() method.
Thus, there are two possible solutions for your problem:
(preferrable) If you use Auto Layout, use it throughout your view.
Either add your testView in Interface Builder as well and create an outlet for it
or create your testView in code as you do, then set its translatesAutoResizingMaskIntoConstraints property to false (to sort of "activate Auto Layout") and add the required constraints for it in code.
Set your testView's frame after the MessageBox view's frame itself has been set by the layout engine. The only place where you can be sure that the system has resolved the view's frame from the constraints is when layoutSubviews() is called.
override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
(You need to declare your testView as a property / global variable, of course.)

Try to use the anchors for your view:
MessageBox.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor).active
= true
MessageBox.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor).active
= true
MessageBox.widthAnchor.constraintEqualToConstant(150).active = true
MessageBox.heightAnchor.constraintEqualToConstant(100).active = true
This method have to be used inside your class

override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
this also works when you add a custom class to a UIView in the storyboard and that uses autolayout.
thanks Mischa !

try to add a height and width constraint relative to the superview height, with some multiplier.

Related

Confused about how/where to set a Xib's height

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).

How to make autolayout work with custom UIView on XIB?

I have a UIView on a XIB, containing an UIImageView and a UILabel with a small space in between. Horizontal layout is a simple chained |--image--label--| in which -- is some fixed space. The height is fixed to 40, this view is horizontally centred in its view controller, and it has an >= 100 width constraint.
If I change the label text, the width of my composed view updates as expected width the changed width of its label, and it stays nicely centred on the view controller.
Because I need this UIView, containing an image and label, in other places, I've created a custom class consisting of a XIB and Swift file. Let's call it ItemView.
Issue I have is that the empty UIView on my view controller XIB, which I've changed class to ItemView, no longer accepts the >= 40 width constraint. This is of course because the view controller XIB no longer sees the variable width UILabel, but instead just a plain UIView of class ItemView. I get an 'Inequality Constraint Ambiguity' IB error.
The result is that the width of my custom view remains 40. It works a little bit if I specify a larger >= label width; the text is then only cut off when this width is reached. But in that second case my custom view is no longer horizontally centred, but shifted a bit to the left instead.
How do I resolve this? Or, how can I tell IB to treat my custom ItemView in a similar way as a UILabel?
In my ItemView I've done all I could find:
override class var requiresConstraintBasedLayout: Bool
{
return true
}
Call setNeedsLayout() after setting the label text.
Call myLabel.translatesAutoresizingMaskIntoConstraints = false.
Call self.translatesAutoresizingMaskIntoConstraints = false.
And call self.setNeedsUpdateConstraints() in both init()s.
Configure this pop-up in the view's Size inspector:
Now IB won't worry about your size specifications. You know better than IB does, and this is how to tell IB that fact.
Another way: configure this pop-up in the view's Size inspector:
This tells IB that the view will have an intrinsic content size that it doesn't know about.
Either of those will work. As you can see in this screenshot, I've given my custom view a width constraint of greater-than-or-equal-to-40, but IB is not complaining of any error:
How I structured my custom UIView: My XIB's Files Owner is ItemView. ItemView has an #IBOutlet var: UIView! that's connected to the view in the XIB. Then I have:
init()
{
super.init(frame: .zero)
self.initView()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initView()
}
private func initView()
{
Bundle.main.loadNibNamed("ItemView", owner: self, options: nil)
self.view.frame = CGRect(x: 0.0, y: 0.0, width: self.width, height: self.height)
self.addSubview(self.view!)
}
(I seems this extra UIView is needed when creating a custom UIView with a XIB. Would love to hear if it's not needed after all.)
To make it work I needed to call self.invalidateIntrinsicContentSize() after updating the label. And override intrinsicContentSize:
override open var intrinsicContentSize: CGSize
{
return self.view.systemLayoutSizeFitting(self.bounds.size)
}
Note that I'm calling this on self.view, not on self.
Thanks #matt for pointing me in the right direction. Was the first time I've encountered something like this.
The following things I tried we all not necessary:
override class var requiresConstraintBasedLayout: Bool
{
return true
}
Call setNeedsLayout() after setting the label text.
Call myLabel.translatesAutoresizingMaskIntoConstraints = false.
Call self.translatesAutoresizingMaskIntoConstraints = false.
And call self.setNeedsUpdateConstraints() in both init()s.

Adding a UIView to the bottom of a UICollectionViewController

I have a UICollectionViewController embedded inside a UINavigationController which in turn embedded inside a UITabBarController.
I want to add a UIView to the UICollectionViewController just above the tab bar (shown by red rectangle).
I have the UIView created separately as a nib file.
import UIKit
class BottomView: UIView {
#IBOutlet var view: UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
fileprivate func commonInit() {
Bundle.main.loadNibNamed("BottomView", owner: self, options: nil)
view.frame = self.frame
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(view)
}
}
And I initialize and add it in the UICollectionViewController like so.
class CollectionViewController: UICollectionViewController {
fileprivate var bottomView: BottomView!
override func viewDidLoad() {
super.viewDidLoad()
let yPos = view.bounds.height - (tabBarController!.tabBar.frame.size.height + 44)
bottomView = BottomView(frame: CGRect(x: 0, y: yPos, width: view.bounds.width, height: 44))
collectionView?.addSubview(bottomView)
}
// ...
}
I figured if I substract the combined height of the bottom view plus the tab bar from the entire view's height, I should be able to get the correct y position value. But it's not happening. The view is getting added but way off screen.
How do I calculate the correct y position without hardcoding it?
Example demo project
I would suggest adding the BottomView to the UICollectionViewController's view rather than to the collection view itself. This is part of the problem you're having.
You're also trying to set the frame of the BottomView in the viewDidLoad() method using values from view.bounds. The CGRect will return (0.0, 0.0, 0.0, 0.0) at this point because the layout has yet to take place, which is most likely why your positioning is off. Try moving your layout logic to the viewWillLayoutSubviews() method and see if that helps.
A better approach would be by setting auto layout constrains rather than defining a frame manually, this will take a lot of the leg work out for you.
Here's a quick example:
self.bottomView.translatesAutoresizingMaskIntoConstraints = false
self.view.insertSubview(self.bottomView, at: 0)
self.bottomView.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor).isActive = true
self.bottomView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.bottomView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.bottomView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
You can apply autolayout logic in your viewDidLoad() and it should work correctly.
You can find some more information on setting autolayout constraints programatically here:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html
Sounds what you want to achieve is exactly the footer view for the UICollectionView.
A footerView is like a view that will stick to the bottom of the collectionView and wont move with the cells.
This will help you add a footer View: https://stackoverflow.com/a/26893334/3165112
Hope that helps!

iOS get wrong frame after set autolayout use snapkit

I add a UIView in UIStoryboard and bind it to a custom UIView class called testView,next, I create a UIView called subView in textView in require init function ,
this is my step
1 initialization the subView
2 add new subView to textView
3 set autolayout
4 set cornerRadius (view.frame.height / 2)
After run the app the cornerRadius does not change
then I try to print the frame of subView , it get (0,0,0,0)
this is my code
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
circelView = UIView()
self.addSubview(circelView)
circelView.snp_makeConstraints(closure: { (make) -> Void in
make.size.equalTo(80)
make.top.equalTo(self.snp_top)
make.right.equalTo(self.snp_right)
})
print(circelView.frame) //get wrong frame
circelView.layer.cornerRadius = circelView.frame.size.height / 2
circelView.layer.masksToBounds = true
}
Your view hasn't had time to be laid out by the time you print the frame and set the corner radius. Adding an AutoLayout constraint doesn't automatically layout the view.
To get the correct result, you need to set your corner radius after the view has been laid out. This will guarantee that you have a frame that is constrained by your AutoLayout constraints.
To do so, place any code that requires the correct frame in 'viewDidLayoutSubviews':
override func viewDidLayoutSubviews() {
print(circelView.frame) // The frame will have been set
circelView.layer.cornerRadius = circelView.frame.size.height / 2
}
viewDidLayoutSubviews() is a method on UIViewController that you can override, check out the documentation here.

Making a custom UIView subview that fills its superview

So I wrote my own custom view with its own initializer. However, when my main view loads my custom view gets depicted in a wrong way. It takes bounds as 600x600 rectangle, while the superview is 375x607. I did try to put auto constraint, seems not to work. I tried to do it programmatically in the subview initialization, but whenever I try to initialize it's bounds property to its superview bounds I get nil in superview.
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
func setup() {
initPathLayer()
initHandleView()
initHandlePanGestureRecognizer()
layoutPathLayer()
layoutHandleViews()
}
I tried everything there is on the internet to make subview fill its superview, but I think that subview gets initialized before superview? Is that possible ? In the ViewController a have my custom view declared as an Outlet connection. I'm sure that the problem should be super easy and it's me who doesn't know the way Swift initializes the view.
Any ideas ?
Thank You.
Firstly, a Views init method is not the best place to perform a layout. The view could be resized at a later point which is typically always the case if the view is loaded from a Xib or you are creating it in a View Controllers viewDidLoad function.
That being said you have several approaches:
1. Use Auto Layout
Interface Builder
This can be done either in Interface Builder or programmatically. In Interface Builder you simply use the 'Pin' option and select all the sides of the view
When this is done you should be able to see your constraints in the Size inspector looking as follows:
Programmatically
Alternatively you can always add your constraints programmatically in your initializer:
override init(frame: CGRect) {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
super.init(frame: frame)
let viewsDict = ["view": view]
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[view]-0-|", options: .allZeros, metrics: nil, views: viewsDict))
addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[view]-0-|", options: .allZeros, metrics: nil, views: viewsDict))
addSubview(view)
}
convenience required init(coder aDecoder: NSCoder) {
self.init(frame: .zero)
}
2. Manual Layout
Resizing Masks
For this you can either use resizing masks or you can control the frame in layoutSubviews. Resizing masks tell the system how the view should be sized relative to the superview:
override init(frame: CGRect) {
let view = UIView(frame: CGRectZero)
view.translatesAutoresizingMaskIntoConstraints = false
super.init(frame: frame)
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
}
Layout Subviews
Lastly, you can override layoutSubviews and go from there:
override func layoutSubviews() {
super.layoutSubviews()
view.frame = bounds
}

Resources