Subview's bounds are zero in layoutSubviews() - ios

The bounds of a subview of a subview of a custom UIView seem to be 0 in layoutSubviews(), hence using the bounds in layoutSubviews() is a problem.
To show the problem I put a demo on GitHub: SOBoundsAreZero
Here's a direct link to the custom view's implementation: DemoView.swift
The structure of the custom view "DemoView" is like this:
DemoView
firstLevelSubview
secondLevelSubview
This structure is being created programmatically using Auto Layout.
When layoutSubviews() is called, the view and the firstLevelSubview have the expected bounds, but the secondLevelSubview's bounds are 0.
I expected all subviews using Auto Layout to have the correct bounds, at least in the last call to layoutSubviews.
The structure is an abstraction of a real case. To avoid the problem, secondLevelSubview could be added as a first level subview to DemoView. Though, this is something, that is not feasible in the real case.
I feel like I'm missing something simple here, even if it's expected behaviour.

I was able to fix this with a call to secondLevelSubview.layoutIfNeeded() in layoutSubviews().
override func layoutSubviews() {
super.layoutSubviews()
secondLevelSubview.layoutIfNeeded()
firstLevelSubview.layer.cornerRadius = bounds.width / 2
secondLevelSubview.layer.cornerRadius = secondLevelSubview.bounds.width / 2
print("bounds.width: \(bounds.width), contentView.bounds.width: \(firstLevelSubview.bounds.width), backgroundView.bounds.width: \(secondLevelSubview.bounds.width)")
}
The description of layoutIfNeeded() is:
Use this method to force the view to update its layout immediately.
When using Auto Layout, the layout engine updates the position of
views as needed to satisfy changes in constraints. Using the view that
receives the message as the root view, this method lays out the view
subtree starting at the root. If no layout updates are pending, this
method exits without modifying the layout or calling any
layout-related callbacks.
So, essentially you have an ordering issue here. The subview is scheduled for layout, and Auto Layout will get to it, but it hasn't done it yet. By calling layoutIfNeeded(), you tell Auto Layout to perform the pending layout immediately so that you can get the updated frame information.
Note: You can also just call self.layoutIfNeeded() and that will layout DemoView and all of its subviews. This would be useful if you had many such subviews and didn't want to have to call layoutIfNeeded() on each of them.

First, Your view is being construed without problem cause you have an initializer based on ‘CGRect’ , so it gets its dimension!
Second,
Your first subview get initialized after its parents view has got its dimensions. First subview dimensions depends on the parent view dimensions which are ready. So no problem here as you mentioned.
But meanwhile your first subview is trying to get its dimensions, your second subview also starts to get its dimensions based on first subview which is not ready yet , so it got nothing.
I suggest make different functions for each subview.
Make your object from the view class with its frame initializer in Your view controller. In ‘viewDidLoad’ call first subview function and in ’viewDidLoadSubView’ call second subview function.
Hope it helps.

You can get the desired result, if you try to access UIView.bounds under main queue.
override func layoutSubviews() {
super.layoutSubviews()
DispatchQueue.main.async {
self.firstLevelSubview.layer.cornerRadius = self.bounds.width / 2
self.secondLevelSubview.layer.cornerRadius = self.secondLevelSubview.bounds.width / 2
print("bounds.width: \(self.bounds.width), contentView.bounds.width: \(self.firstLevelSubview.bounds.width), backgroundView.bounds.width: \(self.secondLevelSubview.bounds.width)")
}
}
//print:
//bounds.width: 64.0, contentView.bounds.width: 64.0, backgroundView.bounds.width: 54.0
But why? So here I dig down into your problem. Here is the calling view hierarchy of your DemoView layoutSubviews method
DemoView
|
|___layoutSubviews
|
|___firstLevelSubview
|
|___layoutSubviews //Here you are trying to access bounds of second view which is still need to be resize or achieve it's bound
|
|___ secondLevelSubview
|
|___layoutSubviews
And so in this case, main queue will help you to get the actual bounds of any UIView.subView.
Another case I did it, I try to add secondLevelSubview to the DemoView itself with the firstLevelSubview constraints like below:
fileprivate func setupViews() {
backgroundColor = UIColor.clear
firstLevelSubview.translatesAutoresizingMaskIntoConstraints = false
firstLevelSubview.layer.masksToBounds = true
firstLevelSubview.backgroundColor = UIColor.green
addSubview(firstLevelSubview)
firstLevelSubview.topAnchor.constraint(equalTo: topAnchor).isActive = true
firstLevelSubview.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
firstLevelSubview.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
firstLevelSubview.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
secondLevelSubview.translatesAutoresizingMaskIntoConstraints = false
secondLevelSubview.layer.masksToBounds = true
secondLevelSubview.backgroundColor = UIColor.magenta
// firstLevelSubview.addSubview(secondLevelSubview)
addSubview(secondLevelSubview) //Here
secondLevelSubview.widthAnchor.constraint(equalTo: firstLevelSubview.widthAnchor, multiplier: 0.84).isActive = true
secondLevelSubview.heightAnchor.constraint(equalTo: firstLevelSubview.heightAnchor, multiplier: 0.84).isActive = true
secondLevelSubview.centerXAnchor.constraint(equalTo: firstLevelSubview.centerXAnchor).isActive = true
secondLevelSubview.centerYAnchor.constraint(equalTo: firstLevelSubview.centerYAnchor).isActive = true
}
And the hierarchy it follows like below:
DemoView
|
|___layoutSubviews
|
|___firstLevelSubview
|
|___layoutSubviews
|
|___secondLevelSubview
|
|___layoutSubviews
So, in the above case if you try to print the UIView.bounds without any main queue you will get the desired result.
Let me know, this helps you to understand the hierarchy.

Related

iOS Swift round views inside cell show incorrectly

Evening, I have a calendar collection.
The cells have some rounded views shown incorrectly at the first time, but when they are reloaded they are shown correctly.
I know that the issue is that at the first time the cell doesn't know the right size of the frame.
What I've tried:
1- call the round function inside layoutSubviews(): only the right side is rounded correctly
2 - call the round function inside the cellWillLayout: nothing changes
This is the rounding function:
func makeRound() {
print("rounding")
currentDayView.layer.cornerRadius = currentDayView.frame.height/2
currentDayView.layer.masksToBounds = true
currentDayView.clipsToBounds = true
selectedDayView.layer.cornerRadius = selectedDayView.frame.height/2
selectedDayView.layer.masksToBounds = true
selectedDayView.clipsToBounds = true
}
Any suggestion?
The best place to do corner rounding is either in each view's layoutSubviews or (for example, if you haven't sub-classed them) put it in your view controllers viewDidLayoutSubviews.
Each view in your case is the layoutSubviews of currentDayView and selectedDayView.
You need to override layoutSubviews method, then call your method inside it:
override func layoutSubviews() {
super.layoutSubviews()
self.makeRound()
}

Swift: Rounded corners appear different upon first appearance and reappearing

class ApplyCorners: UIButton {
override func didMoveToWindow() {
self.layer.cornerRadius = self.frame.height / 2
}
}
I apply this class to the buttons in my application and it is working great, but when I apply it to a button used in every cell in a table view the button corners are not round upon entering the view, but if I click one of the buttons I get segued to another view. If I then segue back the corners are "fixed" / round.
The green is the button when returning and the red is upon first entering the view.
Anyone know how to fix this?
I'd suggest layoutSubviews, which captures whenever the frame of the button changes:
class ApplyCorners: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.height / 2
}
}
This takes care of both the original appearance and any subsequent appearance. It also avoids all sorts of problems related to not only whether the frame was known when the view appeared, but also if you do anything that might change the size of the button (e.g. anything related to constraints, rotation events, etc.).
This sort of thing is likely to be a timing problem. Consider the phrase self.frame.height. At the time didMoveToWindow is called, we may not yet know our frame. If you are going to call a method that depends upon layout, do so when layout has actually occurred.
Gonna propose another alternative: listen to any bounds changes. This avoids the problem of wondering "is my frame set yet when this is called?"
class ApplyCorners: UIButton {
override var bounds: CGRect {
didSet {
layer.cornerRadius = bounds.height / 2
}
}
}
Edited frame to bounds because as #Rob points out, listening for frame changes will cause you to miss the initial load sometimes.
Putting your code in didMoveToWindow() does not make sense to me. I'd suggest implementing layoutSubviews() instead. That method gets called any time a view object's layout changes, so it should update if you resize your view.
(Changed my suggestion based on comments from TNguyen and and Rob.)

How to get a correct automatic row height calculation when modifying the layout inside a UITableViewCell's layoutSubviews() method?

I have a UITableView with cells that are variable in height, laid out with Auto Layout in Interface Builder (rowHeight = UITableViewAutomaticDimension). When the user rotates the device I perform some manual layout changes inside the cell that affect the intrinsicContentSize one of the cell's subviews. As these changes are dependent on the initial frame of that particular subview I can only apply these changes once the layout engine has resolved the constraints and applied the correct frames to all subviews for the new (rotated) layout. This happens inside the layoutSubviews() method. So to apply my manual changes I override that method and first call the super implementation.
override func layoutSubviews() {
// Call `super` implementation to let the layout engine do its job
// and apply the new frames to subviews after resolving constraints.
super.layoutSubviews()
// 🔄 Apply manual layout changes here
}
Now the problem is that my manual layout changes possibly also change the cell's height. Hence I need a way to inform the table view that a second layout pass is required for this cell before the (automatic) height calculation returns. (Otherwise the table view will just use the row height it computed before the manual layout changes.)
Usually you do this by invalidating the layout with a setNeedsLayout() call on the view that needs another layout pass. However, I cannot do this from inside the layoutSubviews() method because it would result in an infinite loop of layout passes and crash the app.
So I tried to figure out a way if the layout had changed in this particular call of layoutSubviews() and came up with this approach:
override func layoutSubviews() {
// Get old image view size before layout pass
let previousImageSize = imageView.frame.size
// Run a layout pass
super.layoutSubviews()
// Get the new image view size
let newImageSize = imageView.frame.size
// Only trigger a new layout pass if the size changed
if newImageSize != previousImageSize {
// 🔄 Apply manual layout changes here
// Trigger another layout pass
setNeedsLayout()
}
}
(In this example, the imageView is a subview of the cell on which my layout depends.)
When running this code I realized that the code inside the if branch is never executed, i.e. the imageView size never changes inside the layoutSubviews() method. But it does change - only at a different time.
That's inconsistent to my understanding Auto Layout in general:
A view is always responsible for laying out its subviews, i.e. for setting their frames appropriately and this is done in layoutSubviews().
So here are my questions:
Why does the system change the image view's size from outside the layoutSubviews() method?
and more importantly:
How can I get the table view to run a second layout pass when computing the row height for a cell?

Insert sublayer behind UIImageView

i'm trying to add a sublayer behind the imageView however the issue is that since it is using constraints it can't seem to figure out the position and just places sublayer in left corner? i've tried to add the LFTPulseAnimation to viewDidLayoutSubViews but then everytime i reopen the app it will add one on top.
viewDidLoad
//GroupProfile ImageView
imageGroupProfile = UIImageView(frame: CGRect.zero)
imageGroupProfile.backgroundColor = UIColor.white
imageGroupProfile.clipsToBounds = true
imageGroupProfile.layer.cornerRadius = 50
self.view.addSubview(imageGroupProfile)
imageGroupProfile.snp.makeConstraints { (make) -> Void in
make.height.equalTo(100)
make.width.equalTo(100)
make.centerX.equalTo(self.view.snp.centerX)
make.centerY.equalTo(self.view.snp.centerY).offset(-40)
}
let pulseEffect = LFTPulseAnimation(repeatCount: Float.infinity, radius:160, position:imageGroupProfile.center)
self.view.layer.insertSublayer(pulseEffect, below: imageGroupProfile.layer)
i've tried to add the LFTPulseAnimation to viewDidLayoutSubViews but then everytime i reopen the app it will add one on top.
Nevertheless that is the way to do it. Just add a Bool property so that your implementation of viewDidLayoutSubViews inserts the layer only once:
var didLayout = false
override func viewDidLayoutSubviews() {
if !didLayout {
didLayout = true
// lay out that layer here
}
}
The reason is that you don't have the needed dimensions until after viewDidLayoutSubviews tells you that (wait for it) your view has been laid out! But, as you rightly say, it can be called many times subsequently, so you also add the condition so that your code runs just once, namely the first time viewDidLayoutSubviews is called.

Loading view on UITableViewController

I am attempting to show a loading view on top of a UITableViewController when a user taps on a cell or a button in the cell. For some reason, the view does not show up, nor does it show any constraint failures. Can someone spot error in my code. I need this view to show up covering the tableview on both orientations, when shown. I thought this to be a view render issue, tried the same code in viewWillAppear. Still does not work. Hence I eliminated layout rendering issues. Same code works perfectly fine on UIViewController derived classes. Seems to have issues on UITableViewController classes alone!!!
private func initActivityView() {
let overlayView = UIView(frame: CGRectZero)
overlayView.backgroundColor = UIColor.lightGrayColor()
overlayView.translatesAutoresizingMaskIntoConstraints = false
overlayView.alpha = 0.5
self.view.addSubview(overlayView)
// add constraints
let viewDictionary = ["overlayView":overlayView]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
}
You should let the constraints engine know that it needs update. See the triggering Auto layout section of the UIView Class Reference. Also I would consider making your overlay view an instance variable or find some other way to keep a reference of it so that when the time comes you can remove it.
Do not call updateConstraints() but rather setNeedsUpdateContraints()
From the UIView Class Reference.
Discussion
When a property of your custom view changes in a way that
would impact constraints, you can call this method to indicate that
the constraints need to be updated at some point in the future. The
system will then call updateConstraints as part of its normal layout
pass. Updating constraints all at once just before they are needed
ensures that you don’t needlessly recalculate constraints when
multiple changes are made to your view in between layout passes.
If your having trouble yo can always set a breakpoint on the UIViewController's
viewWillLayoutSubViews() and viewDidLayoutSubViews() and inspect your subview frames.
It is also pretty helpful to keep update with Apple's documentation for Auto Layout as Auto Layout is constantly changing and improving.
Apparently doing it the autolayout way will not work, due to the inherent nature of UITableViewController. However a better solution for this problem will be something like this
let overlayView1 = UIView(frame: self.tableView.frame)
overlayView1.autoresizingMask = self.tableView.autoresizingMask
overlayView1.backgroundColor = UIColor.lightGrayColor()
overlayView1.alpha = 0.5
self.tableView.addSubview(overlayView1)

Resources