iOS get wrong frame after set autolayout use snapkit - ios

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.

Related

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.

Resize UICollectionView to content size

Summary: I have a child view within a stackview and that child view that is programatically sized. It has a couple of empty views to fill up the empty space. At runtime, however, attempts to resize the child don't work and it remains the same size as in the storyboard. How can I resize the child view so that the stack view honours its new size and positions it accordingly.
Details:
I have a UICollectionView with a custom layout. The layout calculates the positions of subviews correctly (they display where I want) and return the correct content size. The content size is narrower than the screen but possibly longer depending upon orientation.
The custom layout returns the correct, calculated size but the collection view does not resize.
I've tried programatically changing the collection view's size on the parent's viewDidLoad. Didn't work.
I've tried programatically changing the collection view's layoutMargins on the parent's viewDidLoad. Didn't work.
I am using Swift 3, XCode 8.
If I understand your question, you need your UICollectionView to have its size equals to its content, in other words, you want your UICollectionView to have an intrinsic size (just like a label which resizes automatically based on its text).
In that case, you can subclass UICollectionView to add this behaviour, then you don't need to set its size.
class IntrinsicSizeCollectionView: UICollectionView {
// MARK: - lifecycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
if !self.bounds.size.equalTo(self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
get {
let intrinsicContentSize = self.contentSize
return intrinsicContentSize
}
}
// MARK: - setup
func setup() {
self.isScrollEnabled = false
self.bounces = false
}
}
ps: In your .xib, don't forget to set your IntrinsicSizeCollectionView height constraint as a placeholder constraint (check "Remove at build time")

Custom View - self.frame is not correct?

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.

Frame doesn't change properly unless is equal to UIView frame

I have some weird behavior related to frame sizes that I can't fix after hours of trying different things. This just doesn't make any sense.
I have a custom UIView and a related .xib file:
ErrorView.swift
import UIKit
class ErrorView: UIView {
#IBOutlet weak var labelErrorTitle: UILabel!
#IBOutlet weak var view: UIView!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setupView()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
func setupView() {
NSBundle.mainBundle().loadNibNamed("ErrorView", owner: self, options: nil)
self.addSubview(view)
}
ErrorView.xib (using inferred size, the label is centered using constraints)
I want to add this view to a custom UITableView, at the bottom. I want to make it slim, with a height of 45 and a width of the view screen width. I want to add it to the bottom.
Very easy!! I just set the size with a frame like this:
class LoadingTableView: UITableView {
var errorView: ErrorView = ErrorView () // Here is the Error View
var errorFrame: CGRect! // The frame (size) I will use
required init(coder aDecoder: NSCoder) {
}
override func awakeFromNib() {
super.awakeFromNib()
// I create the frame here to put the error at the top
errorFrame = CGRectMake(0,0,self.frame.width,45)
// Init the ErrorView
errorView = ErrorView(frame: errorFrame)
// I add the subview to root (I have this rootView created)
rootView?.addSubview(errorView)
}
// This is needed, it updates the size when layout changes
override func layoutSubviews() {
super.layoutSubviews()
// Create the size again
errorFrame = CGRectMake(0,0,self.frame.width,45)
// I update the frame
errorView.frame = frame
}
This should work but it doesn't. The size is just weird. It takes the size from the nib which is 320x568. Then it just moves the frame, but the size doesn't change.
And here comes the great part. If I set the errorView.frame size to .frame, which is the frame of the tableView then it works! With orientation changes and all!
But as long as I change the frame to a custom size, whatever is in awakeFromNib or layoutSubviews it doesn't and starts to act weird.
Why does it keeps the size of the nib? And why if I put .frame it works at it should but a custom size doesn't? It looks like I'm super close, it's frustrating as hell.
The objective is to say tableView.error("errorCode") and then that errorView appears. And it works on all devices and orientations.
Instead of adding a subview to UITableView, use it's tableFooterView and tableHeaderView properties:
Replace
rootView?.addSubview(errorView)
With:
tableFooterView = errorView

Best place to set cornerRadius based on UIButton size in subclass?

So I will be having a few different rounded buttons within an app (all the way to circular buttons), and from what I can tell, the easiest way to achieve this is to set the cornerRadius property of the buttons CALayer.
However, I don't want to be doing this manually for every button that requires it in every controller, so I thought a simple subclass that sets this on init would be the way.
I am using Storyboard and Autolayout to position and size the buttons, and assigning them to this subclass.
class RoundedButton: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = self.bounds.size.height / 2.0
self.clipsToBounds = true
NSLog("BUTTON BOUNDS: H-%f W-%f", self.bounds.size.height, self.bounds.size.width)
NSLog("BUTTON FRAME: H-%f W-%f", self.frame.height, self.frame.width)
}
}
But I have come to find out that at this point (i.e. init), the size of neither the frame nor bounds are final. For a button in Storyboard sized (and constrained) to H40 x W40, the bounds/frame sizes are showing H30 x W38.
This means that cornerRadius doesn't get the value I expect.
I have confirmed that at some later point (e.g. when the button can already respond to a tap) that its frame/bounds are indeed H40 x W40.
So after all that, my question is, within the UIButton subclass, when/where can I safely set cornerRadius using the final frame/bounds values of the instance?
If you want your code to be executed after the view has gone through AutoLayout you must do it in layoutSubviews after calling super.layoutSubviews().
Like this :
class RoundedButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height / 2.0
clipsToBounds = true
}
}
This code is not perfect though because it doesn't support when height is bigger than width (easy to fix though…).
Try this in Button class.
override func layoutSubviews() {
super.layoutSubviews()
// here...
}
Since you wish to use initWithCoder rather than initWithFrame (where you do have the frame struct), you can override the layoutSubviews method.
When layoutSubviews is called the frame is correct.
In objective-c you can write
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.bounds.size.height/2;
}

Resources