UIKit: Relative autolayout constraints programmatically - swift

I'm using swift + UIKit to autolayout a view programatically...
private func autoLayout(for child: UIView, in parent: UIView, width inset: CGFloat = 0) {
parent.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
child.topAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.topAnchor, constant: inset) .isActive = true
child.bottomAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.bottomAnchor, constant: -inset).isActive = true
child.leadingAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.leadingAnchor, constant: inset) .isActive = true
child.trailingAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.trailingAnchor, constant: -inset).isActive = true
}
In one instance I'm calling the method on a UILabel (with a set font size: 24), but I'd like to add a height constraint that adjusts the parent view to be 2.4x the child's height. Something like this inside the method...
if let label = child as? UILabel {
let value = child.frame.height * 2.4
parent.heightAnchor.constraint(equalToConstant: value).isActive = true
}
If I use the equalToConstant: parameter, then the size will be calculated when the method is called, which happens before the views are added to a UIStackView and their sizes are adjusted (essentially if I set the parent's constant equal to say child.frame.height * 2.4 it will calculate when the height is still zero).
How can I achieve this dynamically?

All you need to do is create a height constraint on the parent that is 2.4 times the height constraint of the child (label).
Here's an updated version of your code:
private func autoLayout(for child: UIView, in parent: UIView, width inset: CGFloat = 0) {
parent.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
child.leadingAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.leadingAnchor, constant: inset) .isActive = true
child.trailingAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.trailingAnchor, constant: -inset).isActive = true
if child is UILabel {
// This alternate syntax for creating a constraint allows you to set a multiplier
NSLayoutConstraint(item: parent, attribute: .height, relatedBy: .equal, toItem: child, attribute: .height, multiplier: 2.4, constant: 0).isActive = true
child.centerYAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.centerYAnchor).isActive = true
} else {
child.topAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.topAnchor, constant: inset) .isActive = true
child.bottomAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.bottomAnchor, constant: -inset).isActive = true
}
}
I made an assumption that for the label you just want the label to be centered in the parent in addition to the parent's height being 2.4 times the child height. Adjust as needed.

Related

Unexpected behaviour of autolayout when isScrollEnabled of TextView is set to false

I'm trying to make growing textview and make it scrollable at some content size. What i'm doing is:
func textViewDidChange(_ textView: UITextView) {
if textView.contentSize.height > 100 && !textView.isScrollEnabled {
let frame = textView.frame
textView.isScrollEnabled = true
let heightConstraint: NSLayoutConstraint = .init(item: textView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: frame.height)
heightConstraint.identifier = "height"
textView.addConstraint(heightConstraint)
}
if textView.contentSize.height < 100 && textView.isScrollEnabled {
textView.isScrollEnabled = false
let heightConstraint = textView.constraints.first { constraint in
constraint.identifier == "height"
}
textView.removeConstraint(heightConstraint!)
}
print(textView.contentSize.height, textView.frame.height)
}
if content size is greater than 100 i enable the scroll and add constraint for height
if content size is less than 100 i disable the scroll and remove the constraint (i expect that textview to fit to its content, as it usually does when scroll is disabled)
i have no problems with enabling the scroll, but when contentsize becomes less than 100, the second if statement fires and the textview for some reason takes the whole screen space. when i call textViewDidChange again (by deleting or adding smth to textview) both ifs fire and everything works as it should be.
what i tried to do is to call textview.sizeToFit() and view.layoutIfNeeded() but had no success. what am i doing wrong here?
I think you're doing a lot more than you need to do.
A better approach would be to create the Height constraint, equal to a constant value of 100, and then activate/de-activate it as needed.
Try it like this:
class ViewController: UIViewController, UITextViewDelegate {
let myTextView = UITextView()
var tvHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
myTextView.translatesAutoresizingMaskIntoConstraints = false
myTextView.font = .systemFont(ofSize: 18.0)
myTextView.backgroundColor = .yellow
view.addSubview(myTextView)
let g = view.safeAreaLayoutGuide
tvHeightConstraint = myTextView.heightAnchor.constraint(equalToConstant: 100.0)
NSLayoutConstraint.activate([
myTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
myTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
myTextView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
myTextView.isScrollEnabled = false
myTextView.delegate = self
}
func textViewDidChange(_ textView: UITextView) {
textView.isScrollEnabled = (textView.contentSize.height > 100)
tvHeightConstraint.isActive = textView.isScrollEnabled
}
}

Why is my second button behaving different when using auto layout constraints?

I can not figure out why my button is behaving different when using auto layout constraints programmatically.
When setting the view that holds my cancelBtn every thing is working fine:
let cancelBtnView = TriangularView(frame: CGRect(x: 0, y: 0, width: 300, height: 150))
let cancelBtn = UIButton()
cancelBtnView.addSubview(cancelBtn)
cancelBtn.titleLabel!.font = UIFont.fontAwesome(ofSize: 35, style: .regular)
cancelBtn.setTitle(String.fontAwesomeIcon(name: .times), for: .normal)
cancelBtn.frame = CGRect(x: cancelBtnView.bounds.width / 16, y: cancelBtnView.bounds.height / 2 ,width: 55, height: 55)
I get the following layout:
When setting up the view for my doneBtn I get a different output:
let doneBtnView = TriangularView()
doneBtnView.translatesAutoresizingMaskIntoConstraints = false
doneBtnView.widthAnchor.constraint(equalToConstant: 300).isActive = true
doneBtnView.heightAnchor.constraint(equalToConstant: 150).isActive = true
doneBtnView.trailingAnchor.constraint(equalTo: actionButtonView.trailingAnchor).isActive = true
doneBtnView.bottomAnchor.constraint(equalTo: actionButtonView.bottomAnchor).isActive = true
let doneBtn = UIButton()
doneBtnView.addSubview(doneBtn)
doneBtn.titleLabel!.font = UIFont.fontAwesome(ofSize: 35, style: .regular)
doneBtn.setTitle(String.fontAwesomeIcon(name: .check), for: .normal)
doneBtn.translatesAutoresizingMaskIntoConstraints = false
doneBtn.widthAnchor.constraint(equalToConstant: 55).isActive = true
doneBtn.heightAnchor.constraint(equalToConstant: 55).isActive = true
doneBtn.leadingAnchor.constraint(equalTo: doneBtnView.leadingAnchor, constant: doneBtnView.bounds.width / 16).isActive = true
doneBtn.bottomAnchor.constraint(equalTo: doneBtnView.bottomAnchor, constant: doneBtnView.bounds.height / 2).isActive = true
Setting the constraints for my doneBtn programmatically I get the following:
The constraints for the doneBtnView are set programmatically because I want to pin it to the bottom right of its superview.
Your problem is that you are looking at the bounds of the other view when setting your constraint for your doneBtn, but those bounds will not have been set yet because layout has not happened. In general, it is a bad idea to combine frame/bounds and constraints.
This can be done, but not with anchors. Try the following NSLayoutConstraints:
NSLayoutConstraint(item: doneBtn, attribute: .leading, relatedBy: .equal,
toItem: doneBtnView, attribute: .trailing, multiplier: 1/16, constant: 0).isActive = true
NSLayoutConstraint(item: doneBtn, attribute: .top, relatedBy: .equal,
toItem: doneBtnView, attribute: .bottom, multiplier: 1/2, constant: 0).isActive = true
and then set the width and height using anchors:
doneBtn.widthAnchor.constraint(equalToConstant: 55).isActive = true
doneBtn.heightAnchor.constraint(equalToConstant: 55).isActive = true
Note: Because we're using the .trailing and .bottom constraints to represent width and height, it might be necessary to put doneBtnView into a container view of the same size because the values will be in the coordinate system of the parent view. By making the parent view the exact same size, width will be equal to the trailing constraint, and height will be equal to the bottom constraint.

Conforming cornerRadius of a subviews inside a UIView

I have a table view cell which consists of an imageView and a couple of labels. The image is confined to the top of the cell. In order to create the shadow effect, I have a background UIView which acts as the container for the image and labels. I have applied a cornerRadius of 6 to the background view.
You will notice that the bottom of the card has a corner radius applied but the image does not allow this for the top corners.
Here is my background UIView:
lazy var restaurantBackground : UIView = {
let view = UIView()
view.layer.cornerRadius = 6
view.backgroundColor = .white
return view
}()
How can I solve this? maskToBounds or clipToBounds for the background view does not work. It removes the shadow effect.
Addendum
// Adding the background view
contentView.addSubview(restaurantBackground)
restaurantBackground.translatesAutoresizingMaskIntoConstraints = false
restaurantBackground.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 4).isActive = true
restaurantBackground.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 4).isActive = true
restaurantBackground.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -4).isActive = true
restaurantBackground.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant: -4).isActive = true
// Adding shadow
restaurantBackground.layer.shadowColor = UIColor.black.cgColor
restaurantBackground.layer.shadowOpacity = 0.6
restaurantBackground.layer.shadowRadius = 4
restaurantBackground.layer.shadowOffset = CGSize(width: 0, height: 2)
restaurantBackground.layer.shouldRasterize = true
restaurantBackground.layer.rasterizationScale = UIScreen.main.scale
// Adding restaurant image to background
restaurantBackground.addSubview(restaurantImage)
restaurantImage.translatesAutoresizingMaskIntoConstraints = false
restaurantImage.topAnchor.constraint(equalTo: restaurantBackground.topAnchor, constant: 0).isActive = true
restaurantImage.leadingAnchor.constraint(equalTo: restaurantBackground.leadingAnchor, constant: 0).isActive = true
restaurantImage.trailingAnchor.constraint(equalTo: restaurantBackground.trailingAnchor, constant: 0).isActive = true
restaurantImage.heightAnchor.constraint(equalTo: restaurantImage.widthAnchor, multiplier: 1.0/2.0).isActive = true
Your food image view needs to be a subview of the resturantBackground view with a border radius, otherwise its normal borders will be placed above the rounded background. Right now, I assume you're adding both views to the same superview.

Dynamic content to uiscrollview with autolayout not working as expected

I am trying to add dynamic content to the scrollview as below
for i in 0 ..< 4 {
let gameView = Card.instantiate()
gameView.frame.origin.y = gameView.frame.size.height * CGFloat(i)
contentView.addSubview(gameView)
gameView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
let widthConstraint = NSLayoutConstraint(item: gameView, attribute: .width, relatedBy: .equal,toItem: contentView, attribute: .width, multiplier: 0.8,constant : 0.0)
contentView.addConstraint(widthConstraint)
let heightConstraint = NSLayoutConstraint(item: gameView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: 100)
gameView.addConstraint(heightConstraint)
contentView.frame.size.height = contentView.frame.size.height + gameView.frame.size.height
scrollView.contentSize.height = scrollView.contentSize.height + gameView.frame.size.height
}
Cardview is defined xib with autolayout
And view structure in IB as below SuperView > ScrollView > ContentView > DynamicViews
ContentView Also contain some static contents like buttons and labels.Dynamic View is below that static contents
And the output screen is look like below but which is not proper-aligned
Is this the correct approach to add dynamic views to scrollview?
This is an ideal case for UIStackView
The stack view will handle vertical positioning and width of the instantiated gameViews (assuming you have your xib loading set up correctly).
By constraining the top, leading, trailing, bottom and width of the stack view to the scrollView, it will also define the "scrollable area" (the .contentSize).
All done with auto-layout:
let sv = UIStackView()
sv.axis = .vertical
sv.alignment = .fill
sv.distribution = .fill
sv.spacing = 0 // change to add vertical spacing if desired
sv.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: scrollView.topAnchor),
sv.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
sv.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
sv.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
sv.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1.0),
])
for _ in 0 ..< 4 {
let gameView = Card.instantiate()
gameView.translatesAutoresizingMaskIntoConstraints = false
gameView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
sv.addArrangedSubview(gameView)
}

Label Not Resizing Within Container Despite Several Methods

I have a reusable view class that I call when I want to add a disappearing subView to another view. I have a UILabel extension to determine when there is to much text for the label's current size(this extension works), and within this closure I'm trying to expand the: contianerView(regView)'s height, the label's height, and the label's height anchor, since the label was created programatically. As you guessed, the label isn't expandng correctly.
I've tried setting the numberOfLines to 0; changing the label's frame; using .layoutSubviews; removing when the height anchor was originally set, so now it's only called when the resize view method is used.
Label extension:
extension UILabel {
var isTruncated: Bool {
guard let labelText = text else {
return false
}
let labelTextSize = (labelText as NSString).boundingRect(
with: CGSize(width: frame.size.width, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [.font: font],
context: nil).size
return labelTextSize.height > bounds.size.height
}
}
function to add reusable view(most of it is within the while loop towards the bottom):
func addDisapearingView(toview: UIView, text: String, textColor: UIColor, colorView: UIColor, alpha: CGFloat, height: CGFloat){
regView.backgroundColor = colorView
regView.alpha = alpha
toview.addSubview(regView)
regView.translatesAutoresizingMaskIntoConstraints = false
if #available(iOS 11.0, *) {
let guide = toview.safeAreaLayoutGuide
regView.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
regView.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
regView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
UIView.animate(withDuration: 5.0) {
self.regView.heightAnchor.constraint(equalToConstant: height).isActive = true
}
} else {
NSLayoutConstraint(item: regView,
attribute: .top,
relatedBy: .equal,
toItem: toview, attribute: .top,
multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: regView,
attribute: .leading,
relatedBy: .equal, toItem: toview,
attribute: .leading,
multiplier: 1.0,
constant: 0).isActive = true
NSLayoutConstraint(item: regView, attribute: .trailing,
relatedBy: .equal,
toItem: toview,
attribute: .trailing,
multiplier: 1.0,
constant: 0).isActive = true
regView.heightAnchor.constraint(equalToConstant: height).isActive = true
}
let label = UILabel(frame: CGRect(x: regView.frame.origin.x, y: regView.frame.origin.y, width: regView.bounds.width, height: regView.bounds.height))
label.numberOfLines = 0
label.adjustsFontSizeToFitWidth = true
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.translatesAutoresizingMaskIntoConstraints = false
label.center.x = newView.center.x
label.center.y = newView.center.y
label.textAlignment = .center
label.text = text
label.textColor = textColor
regView.addSubview(label)
if label.isTruncated {
print("LABEL IS TRUNCATED")
}
//test if there is more text than the label has room for
while label.isTruncated {
print("printing while truncating in the wHiLE loop")
regView.bounds.size.height += 5
label.bounds.size.height += 5
var currentLabelHeight = label.bounds.height
let amt = currentLabelHeight + 5
label.frame = CGRect(x: regView.frame.origin.x, y: regView.frame.origin.y, width: regView.bounds.width, height: CGFloat(amt))
var heighT : CGFloat = height
heighT += 5
regView.heightAnchor.constraint(equalToConstant: heighT).isActive = true
}
regView.layoutSubviews()
label.sizeToFit()
//remove
Timer.scheduledTimer(withTimeInterval: 2.8, repeats: false) { (action) in
UIView.animate(withDuration: 2.8, animations: {
self.regView.heightAnchor.constraint(equalToConstant: 0).isActive = true
label.heightAnchor.constraint(equalToConstant: 0).isActive = true
})
}
}
I've briefly done this before in storyboard where I had to expand a label within another view when the text was too long(this time it did work!), and the important part there was editing the height constraint, so I think this might have something to do with modifying the height constraint.
Any help would be greatly appreciated!
ANSWER:
I asked another question here: Programatically Created Label Within Container View Won't Expand For Text
it has the same code here and everything in the question but the answer works.
If i understand correct, you have your view and a label, and you want your view to dynamically change height depend on label content. I suggest you to break that task to chunks and resolve it step by step.
1 - You might want to add a test UIView object instead of label with fixed size. When u do this, you will see whether you parent view expand depending of test view size.
2 - If it is, you are up to create a label with height you need. All that you need to know its font, text and width. I think this link may help you. After you sure, that your label size is correct (you may want to print it out) you may add it as any other UIView object to your parent view.