Autolayout frame update in viewDidLoad - ios

I have a custom subview defined this way:
class CustomSubview:UIView {
let contentView = ContentView(frame: .zero) //ContentView is another custom subview
override init(frame: CGRect) {
super.init(frame: frame)
setupContentView()
}
private func setupContentView() {
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.backgroundColor = UIColor.clear
addSubview(contentView)
contentView.leftAnchor.constraint(equalTo: leftAnchor, constant:10).isActive = true
contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
contentView.clipsToBounds = true
contentView.isUserInteractionEnabled = true
contentView.layoutIfNeeded()
NSLog("Contentview frame \(contentView.frame)")
}
}
The problem is even after calling layoutIfNeeded, the frame is still CGRect.zero. Why is that? How do I ensure bounds to be updated immediately?

In CustomSubview class, implement:
override func layoutSubviews() {
super.layoutSubviews()
NSLog("In layoutSubviews() - Contentview frame \(contentView.frame)")
}
That's where you have the actual frame. Note that it will be called anytime your instance of CustomSubview changes, such as on device rotation (assuming you have it set up to change size when its superview changes size).
So, you would want to calculate how many "thumbnails" you want to display, and either add or remove them as needed.

Change your
contentView.layoutIfNeeded()
to
contentView.setNeedsLayout()
layoutIfNeeded()
You need to tell the CustomSubview object to lay itself out again, not just the contentView.

Related

Subview autolayout constraints in init

I have a custom UIView which is a subclass of UIScrollView. In the init(_ frame) method I add a subview as follows:
contentView = ContentView()
contentView.backgroundColor = UIColor.blue
contentView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(contentView)
contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
// create contentView's Width and Height constraints
cvWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0.0)
cvHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 0.0)
// activate them
cvWidthConstraint.isActive = true
cvHeightConstraint.isActive = true
cvWidthConstraint.constant = timelineWidth //It's non-zero
cvHeightConstraint.constant = 2.0*frame.height
The problem is it takes time for the contentView frame to be updated. It certainly doesn't get updated in init(_ frame) call. When exactly does contentView frame gets set and how do I catch the event of frame updation?
Inside
override func layoutSubviews() {
super.layoutSubviews()
// to do
}
You get parent view actual frame , but keep in mind as it's called multiple times
before setting the constraints, I would make sure the view is added to the parent view, like “self.addSubview(contentView)”. If “self” view is already added to the view hierachy, then the contentView layout will be effective right away. Alternatively, you can override the method viewDidMoveToSuperview, and there you check if the superview is not nil and, if so, you initialize and plug the subview contentView to it

Swift UIView Subviews not rounding corners in Custom UIView Subclass

Greetings stack overflow.
I am trying to build a "bullseye" type view, using coloured subviews and the corner radius. The problem I have is, only my first subview's corners are getting rounded and the inner views are still squares. The black view is a subview of my custom view. The red view is it's subview, and they yellow view the subview of that. Pretty simple hierarchy.
The result looks like this:
I add the views and set their constraints manually. My test app just has the ThreeCircleView dead center of a view controller with the X,Y centered and the width, height constant. I do the actual rounding of the corners in didLayoutSubViews because the size of the view might change, so the corners would have to be resized.
I wrote a test view to isolate this, here it is
class ThreeCircleView: UIView {
var outerCircle: UIView = UIView()
var middleCircle: UIView = UIView()
var innerCircle: UIView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
translatesAutoresizingMaskIntoConstraints = false
addSubViews()
}
func addSubViews() {
outerCircle.backgroundColor = .black
middleCircle.backgroundColor = .red
innerCircle.backgroundColor = .yellow
self.addSubview(outerCircle)
outerCircle.addSubview(middleCircle)
middleCircle.addSubview(innerCircle)
let outerCenterY = outerCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let outerCenterX = outerCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let outerCenterWidth = outerCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -50.0 )
let outerCenterHeight = outerCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -50.0 )
outerCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([outerCenterY,outerCenterX,outerCenterWidth,outerCenterHeight])
self.setNeedsLayout()
let middleCenterY = middleCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let middleCenterX = middleCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let middleCenterWidth = middleCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -100.0 )
let middleCenterHeight = middleCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -100.0 )
middleCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([middleCenterY,middleCenterX,middleCenterWidth,middleCenterHeight])
let innerCenterY = innerCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let innerCenterX = innerCircle.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let innerCenterWidth = innerCircle.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -150.0 )
let innerCenterHeight = innerCircle.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -150.0 )
innerCircle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([innerCenterY,innerCenterX,innerCenterWidth,innerCenterHeight])
}
func makeCircle(v:UIView) {
v.layer.cornerRadius = v.frame.size.width * 0.50
v.clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
makeCircle(v: outerCircle)
makeCircle(v: middleCircle)
makeCircle(v: innerCircle)
}
}
An easy way to make it look as expected is to add layoutIfNeeded() call inside your makeCircle(v:UIView) method. This will make you sure that all views' frames are updated correctly before applying visual changes:
func makeCircle(v:UIView) {
v.layoutIfNeeded()
v.layer.cornerRadius = v.frame.size.width * 0.50
v.clipsToBounds = true
}

Creating a reusable UIView subclass - running into sizing issues

I am trying to create a small reusable view for the "tags" at the bottom ("PC-12 Restricted" and "PC-24 Restricted").
This is the code I have, and mostly it's working.
class TagView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private let label = UILabel()
func setup() {
translatesAutoresizingMaskIntoConstraints = false
label.text = "Placeholder"
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .caption2)
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
])
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
func configure(tag: Tag) {
label.text = tag.label
backgroundColor = tag.backgroundColor
}
}
However, when I place it inside a UIStackView inside of a UITableViewCell, it moves outside of the stackview? Also, I added two of these TagViews, but they are showing on top of each other!
When I use the view debugger in Xcode, I get some exclamation marks with the following info:
Why is the position ambiguous? What am I missing? When I print the TagViews that I am adding to my view hierarchy, I do see that they have a frame with everything set to 0, but when I look in the view debugger, they do have a frame.
let tagViews = airport.tags.map { tag -> TagView in
let tagView = TagView()
tagView.configure(tag: tag)
return tagView
}
print(tagViews)
for view in tagViews {
tagsStackView.addSubview(view)
}
[<MyApp.TagView: 0x7f81ac449570; frame = (0 0; 0 0); layer = <CALayer: 0x600001afaba0>>, <MyApp.TagView: 0x7f81ac449c70; frame = (0 0; 0 0); layer = <CALayer: 0x600001afae60>>]
Edit:
If I add two TagViews to my the UIStackView inside of my UITableViewCell .xib file, it works fine:
But when I instantiate and add them programmatically, I have the layout problems as described.
Edit 2:
I have a very small test project so you can reproduce the problem: https://github.com/kevinrenskers/TagViewTest
Finally found the problem. And I feel stupid 🤦‍♂️
tagsStackView.addSubview(tagView)
should of course be
tagsStackView.addArrangedSubview(tagView)

UICollectionViewCell layout with SnapKit

I have a simple UICollectionViewCell which is full width and I'm using SnapKit to layout its subviews. I'm using SnapKit for my other view and it's working great but not within the collectionViewCell. This is the layout I'm trying to achieve:
collectionViewCell layout
The code for the collectionViewCell is:
lazy var imageView: UIImageView = {
let img = UIImageView(frame: .zero)
img.contentMode = UIViewContentMode.scaleAspectFit
img.backgroundColor = UIColor.lightGray
return img
}()
override init(frame: CGRect) {
super.init(frame: frame)
let imageWidth = CGFloat(frame.width * 0.80)
let imageHeight = CGFloat(imageWidth/1.77)
backgroundColor = UIColor.darkGray
imageView.frame.size.width = imageWidth
imageView.frame.size.height = imageHeight
contentView.addSubview(imageView)
imageView.snp.makeConstraints { (make) in
make.top.equalTo(20)
//make.right.equalTo(-20)
//make.right.equalTo(contentView).offset(-20)
//make.right.equalTo(contentView.snp.right).offset(-20)
//make.right.equalTo(contentView.snp.rightMargin).offset(-20)
//make.right.equalToSuperview().offset(-20)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Without applying any constraints the imageView is displayed top left in the cell but applying any constraints makes the image disappear. Inspecting the collectionViewCell in debug view shows the imageView and constraints but the 'Size and horizontal position are ambiguous for UIImageView'.
I have also tried setting contentView.translatesAutoresizingMaskIntoConstraints = false amongst other things but the same results.
I'm using all code and no storyboard UI or layouts.
Thanks for any help!
You can't use both auto layout (SnapKit) and manual layout (set frame). Usually, using auto layout will cause manual layout to fail. However, your code of the constraints in snp closure is not complete. Whether using auto layout or manual layout, finally the view should get the position and size.
So, you can try like this:
imageView.snp.makeConstraints { make in
// set size
make.size.equalTo(CGSize(width: imageWidth, height: imageHeight))
// set position
make.top.equalTo(20)
make.centerX.equalTo(contentView)
}
I don't know much about SnapKit but it looks like you're missing a constraint. You need height, width, x and y. Works nicely with IOS 9 constraints.
let imageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.backgroundColor = .lightGray
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
self.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
imageView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
imageView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 2/3, constant: -10).isActive = true
imageView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1/2, constant: -10).isActive = true
}

How to position a subView below a subView with dynamic height?

I have the following UIView:
It is a UIView which contains three subviews. A regular UILabel (Hello World) at the top, a custom UIViewController (CategoryList) which contains a CollectionView with buttons (alpha,beta, ...) and another custom UIViewController with just a label (SALUT).
I do auto-layout programmatically and position SALUT (var sCtrl) below the CategoryList (var catList) with
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
This results in the picture above, where SALUT is not positioned below the category list as I would like it to be. I sort of understand why since when I set the constraint the buttons are not yet laid out properly in the CollectionView and thus the bottom anchor of the catList is not set.
In the CategoryList:UIVIewController I have the following in order to get the collection view to get the correct height.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
}
This works but since viewDidLayoutSubviews() is called after the constraint is set on SALUT the SALUT position is wrong. How can I get it right in an easy way? (I guess I could make a controller for my main view and check there when subviews are laid out and then update subview positions but this seems overly complicated to me).
The full code for the main view is below so you can see the layout positioning code. If needed I can also provide the subviews code but I suppose it shouldn't be needed since they should be considered black boxes...
class MyView: UIView {
let label = UILabel()
var sCtrl : SubController
var catList : CategoryList
var topConstraint = NSLayoutConstraint()
override init(frame: CGRect) {
sCtrl = SubController()
catList = CategoryList()
super.init(frame: frame)
setup()
}
private func setup() {
self.backgroundColor = UIColor.green
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.yellow
label.text = "Hello world"
label.textAlignment = .center
self.addSubview(label)
catList.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(catList.view)
sCtrl.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(sCtrl.view)
let margins = self.layoutMarginsGuide
label.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
label.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 0).isActive = true
label.topAnchor.constraint(equalTo: margins.topAnchor, constant: 0).isActive = true
label.heightAnchor.constraint(equalToConstant: 100.0).isActive=true
catList.view.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
catList.view.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 1.0).isActive = true
catList.view.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 1.0).isActive = true
catList.view.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant:0).isActive = true
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let v = MyView(frame: CGRect(x: 0, y: 0, width: 400, height: 600))
Ok, I found the issue. I had missed to add the height constraint to the CategoryList's own view. Adding this it works just fine.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
//THE MISSING LINE!!
self.view.heightAnchor.constraint(equalTo: self.collectionView.heightAnchor, constant: 0).isActive = true
}

Resources