Subview autolayout constraints in init - ios

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

Related

Dynamically Sized View based on Child View Controller

I have a custom view controller used as a child view controller:
class ChildViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
calculatePreferredSize()
}
func calculatePreferredSize() {
let targetSize = CGSize(width: view.bounds.width,
height: UIView.layoutFittingCompressedSize.height)
preferredContentSize = view.systemLayoutSizeFitting(targetSize)
}
}
then in the main view controller, I have this code:
class ViewController: UIViewController {
var container : UIView!
var childVC : ChildViewController!
var containerHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .purple
// setup container to hold child vc
container = UIView()
container.backgroundColor = .systemPink
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)
container.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
container.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
container.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
containerHeightConstraint = NSLayoutConstraint()
containerHeightConstraint = container.heightAnchor.constraint(equalToConstant: 0)
containerHeightConstraint.isActive = true
// setup child vc
childVC = ChildViewController()
addChild(childVC)
container.addSubview(childVC.view)
childVC.view.frame = container.bounds
childVC.didMove(toParent: self)
// add contents into the child vc
let newView = UIView()
childVC.view.addSubview(newView)
newView.backgroundColor = .systemBlue
newView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
newView.topAnchor.constraint(equalTo: newView.superview!.topAnchor),
newView.leadingAnchor.constraint(equalTo: newView.superview!.leadingAnchor),
newView.trailingAnchor.constraint(equalTo: newView.superview!.trailingAnchor),
newView.heightAnchor.constraint(equalToConstant: 123),
])
}
override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
super.preferredContentSizeDidChange(forChildContentContainer: container)
if (container as? ChildViewController) != nil {
containerHeightConstraint.constant = container.preferredContentSize.height
}
}
}
I am trying to dynamically size the container view in the main VC based on the child's calculated height. The preferredContentSizeDidChange method is being called but in my calculation of the child VC's height (using UIView.layoutFittingCompressedSize), I'm always getting back 0. Even though I've checked the frame of the view added onto that view and it has the correct frame height (in this example, 123). As shown in the output logging below:
(lldb) po view.subviews
▿ 1 element
- 0 : <UIView: 0x12251cd40; frame = (0 0; 350 123); layer = <CALayer: 0x6000007a0e60>>
(lldb) po UIView.layoutFittingCompressedSize
▿ (0.0, 0.0)
- width : 0.0
- height : 0.0
Below is a screenshot from the simulator.
Am I using the UIView.layoutFittingCompressedSize incorrectly? How do I calculate the height of the child view based on its contents?
Autolayout can't calculate the newView content height, because it is missing constraints in the Y axis to solve the equation.
newView has only these constraints defined: top, leading, trailing and height.
It is missing the bottom constraint:
newView.bottomAnchor.constraint(equalTo: newView.superview!.bottomAnchor).isActive = true
The full set of constraints would look like the following:
NSLayoutConstraint.activate([
newView.topAnchor.constraint(equalTo: newView.superview!.topAnchor),
newView.leadingAnchor.constraint(equalTo: newView.superview!.leadingAnchor),
newView.trailingAnchor.constraint(equalTo: newView.superview!.trailingAnchor),
newView.heightAnchor.constraint(equalToConstant: 123),
newView.bottomAnchor.constraint(equalTo: newView.superview!.bottomAnchor)
])
Afterwards when I place a breakpoint into preferredContentSizeDidChange, I can print the container.preferredContentSize.height, which is 123.0.
EDIT
To avoid constraint breakage, we also need to use autolayout for childVC.view. Right now it is using autosizing mask, which only flows from top-down and creates constraints with 1000 priority.
childVC.view.frame = container.bounds
needs to be replaced with
childVC.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childVC.view.topAnchor.constraint(equalTo: container.topAnchor),
childVC.view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
childVC.view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
childVC.view.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
and the containerHeightConstraint needs to have a reduced priority for the 0 height constraint, otherwise the system will always find the constraints ambiguous - the child controller wants to be 123 points tall, but the container height constraint is still at 0 before we call the preferredContentSizeDidChange method.
containerHeightConstraint.priority = .defaultLow

Can't get StackView to view items properly

I'm trying to add 4 items to a UIStackView, this 4 Items are all a simple square UIView, I added them all to a UIStackView but they won't stay square, it's like the UIStackView squeezes them or something. I tried setting the UIStackView to be the same height of the items, and set it's width to be the height of the items * 4 so I can try and get 1:1 ratio, but nothing worked for me.
The UIView is a simple UIView with background color. I tried to set it's widthAnchor and heightAnchor to 50, but I know the UIStackView has it's own way to size the items in it.
I don't really know what to do about this.
This is my UIStackView setup and constraints:
Setup:
private lazy var optionButtonStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [self.optionButton1, self.optionButton2, self.optionButton3, self.optionButton4])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fillEqually
stack.axis = .horizontal
stack.spacing = 2.5
return stack
}()
Constraints:
private func setupOptionButtonStack() {
addSubview(optionButtonStack)
NSLayoutConstraint.activate([
optionButtonStack.heightAnchor.constraint(equalToConstant: 50),
optionButtonStack.widthAnchor.constraint(equalToConstant: 200),
optionButtonStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
optionButtonStack.bottomAnchor.constraint(equalTo: buyNowButton.topAnchor, constant: -8),
])
}
This is the UIView in case this is needed:
private let optionButton1: UIView = {
let view = UIView()
view.backgroundColor = .appBlue
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: 50).isActive = true
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
view.tag = 1
return view
}()
Give the button view a single constraint setting its width equal to its height:
view.widthAnchor.constraint(equalTo: view.heightAnchor).isActive = true
and set the stack view alignment at center.

Autolayout frame update in viewDidLoad

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.

UILayoutPriority in an UIScrollView doesn't affect anything

I have a contentview (Defined in an xib file) which has some labels and two buttons in it (white view on screenshot 1.a). If we came how It is shown on the screen, let me draw the scheme;
ViewController View -> View (Custom View Class) -> UIScrollView -> ContentView (Xib file)
Buttons are placed related to bottom margin. They both placed to bottom. Top button has a constraint which has 10 constant related to bottom of last label (This is greater than equal constraint and has UILayoutPriority of 5).
Problem: When I give constraint from button to label even It is greater than equal, button stacks just 10 point below to label. Even there is so much gap in bottom. (Screenshot 2.a)
What I want to achieve: I want that If all contents are fit on the screen with minimum 10 constraint from last label to first button, don't scroll. If not, scroll.
Custom View Class:
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
func initialize() {
self.translatesAutoresizingMaskIntoConstraints = false
let name = String(describing: type(of: self))
let nib = UINib(nibName: name, bundle: .main)
nib.instantiate(withOwner: self, options: nil)
cancelButton.layer.borderColor = UIColor.darkGray.cgColor
cancelButton.layer.borderWidth = 1.0
scrollView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.scrollView)
scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
scrollView.addSubview(contentView)
self.contentView.translatesAutoresizingMaskIntoConstraints = false
//self.contentView.clipsToBounds = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
}
Example Screenshots:
If content fit into view, I want like below image. If not button can go up till It fits, If It doesn't fit even though, View should be scrollable.
Screenshot 2.a
The following line should be enough to calculate the height of the contents inside the scroll view and give you the proper behavior you expect.
contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.heightAnchor).isActive = true

How to Make a UIButton fix at UIScrollView?

I am having an UIButton at my UIScrollView and when i zoom my scroll view the button position is not fix...is there any way to make it fix at one location?
Place/set your button over scroll view (not inside scroll view) as shown here in this snapshot. And also set button constraints (position) with respect to super view of your scrollview.
Here is ref. snapshot of hierarchy of position of each view over each-other.
Since iOS 11, UIScrollView has an instance property called frameLayoutGuide. frameLayoutGuide has the following declaration:
var frameLayoutGuide: UILayoutGuide { get }
The layout guide based on the untransformed frame rectangle of the scroll view.
Use this layout guide when you want to create Auto Layout constraints that explicitly involve the frame rectangle of the scroll view itself, as opposed to its content rectangle.
The following Swift 5.1 / iOS 13 UIViewController implementation shows how to use frameLayoutGuide in order to center a UIButton inside a UIScrollView.
import UIKit
class ViewController: UIViewController {
let scrollView = UIScrollView()
let button = UIButton(type: .system)
let imageView = UIImageView(image: UIImage(named: "LargeImage"))
override func viewDidLoad() {
super.viewDidLoad()
// Set scrollView constraints
scrollView.contentInsetAdjustmentBehavior = .never
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// Set imageView constraints
scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
])
// Set button constraints (centered)
button.setTitle("Button", for: .normal)
scrollView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: scrollView.frameLayoutGuide.centerXAnchor),
button.centerYAnchor.constraint(equalTo: scrollView.frameLayoutGuide.centerYAnchor)
])
}
}
If you want your button to stay at a fixed size and position when you modify your scrollview, the best way would be to not have it be in the scrollview at all. Add the button to the parent view of the scrollview, then it won't be affected by any changes to the scrollview and can be overlaid on top of it.
You should position your button outside the scroll view and anchor it to the bottom of the page. This way the content in your scroll view will change but your button will remain statically fixed on the view.

Resources