Dynamic content to uiscrollview with autolayout not working as expected - swift

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)
}

Related

UIKit: Relative autolayout constraints programmatically

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.

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.

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.

Organize and adjust the UILabel position per devices - Swift 3

I have SQLite database file and UILabel, and I set text for label from database on the number of characters after convert String to Characters, and I added extension its name (length), its job counts the number of characters.
My problem in this picture is : The UILabels are not organized in sizes and positions in per devices I want them to have their positions in the center of X-Axis
Questions:
1) How to set the label into center and set spaces right and left of screen for labels (per device) ??
2) How to set width for labels if device is iPhone 7/6S/6 plus width = 50, if device is iPhone 7/6S/6 width = 45 and if device is iPhone SE/5S/5C/5 width = 38 ??
3) Finally, how does the label become smaller by 10 if the number of characters is more than 8 ?
This my code :
func createTarget(id: Int) {
listdata = dbHelpr.getDatabase(rowId: id)
for data in listdata {
let lengthOfChar : CGFloat = data.ans.length
let yAxis : CGFloat = (self.view.frame.height) * 60%
let width: CGFloat = view.frame.size.width - 40 // frame width
var targetWidth: CGFloat = (width - (lengthOfChar - 1) * 5) / lengthOfChar
let targetHeigt : CGFloat = 5
if lengthOfChar >= 8 {
targetWidth = 40
} else {
targetWidth = 50
}
let totalWidth: CGFloat = (targetWidth * lengthOfChar) + ((lengthOfChar - 5) * 5)
for (indexTar, tar) in data.ans.characters.enumerated() {
let x : CGFloat = (width / 2) - (totalWidth / 2)
let xx : CGFloat = (CGFloat(indexTar) * targetWidth) + (CGFloat(indexTar) * 5) + 20
var xAxis : CGFloat = (x + xx)
xAxis = width - xAxis
let targetLabel = UILabel(frame: CGRect(x: xAxis, y: yAxis, width: targetWidth, height: targetHeigt))
targetLabel.backgroundColor = .white
targetLabel.layer.masksToBounds = true
targetLabel.layer.cornerRadius = 5
targetLabel.text = String(describing: tar)
targetLabel.textAlignment = .center
targetLabel.textColor = .white
self.view.addSubview(targetLabel)
}
}
}
In order to position your labels you need a few things things:
Label container to keep everything centered and with margins from the screen sides
Constraints between your labels depending on the device sizes etc..
If you need to break lines - you need to handle it manually as well
To identify device kinds and have different logics, you can check this posts:
iOS: How to determine the current iPhone/device model in Swift?
In order to fill your view with labels, here is a sample code to achieve this:
self.view.backgroundColor = UIColor.black
let labelContainerView : UIView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
labelContainerView.translatesAutoresizingMaskIntoConstraints = false
let containerLeftConstraint : NSLayoutConstraint = NSLayoutConstraint(item: self.view, attribute: .left, relatedBy: .greaterThanOrEqual, toItem: labelContainerView, attribute: .left, multiplier: 1, constant: 8)
let containerRightConstraint : NSLayoutConstraint = NSLayoutConstraint(item: self.view, attribute: .right, relatedBy: .greaterThanOrEqual, toItem: labelContainerView, attribute: .right, multiplier: 1, constant: 8)
let containerCenterX : NSLayoutConstraint = NSLayoutConstraint(item: labelContainerView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let containerCenterY : NSLayoutConstraint = NSLayoutConstraint(item: labelContainerView, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0)
self.view.addSubview(labelContainerView)
self.view.addConstraints([ containerLeftConstraint, containerRightConstraint, containerCenterX, containerCenterY ])
// Add some labels
let totalLabels : Int = 10
var lastAddedLabel : UILabel? = nil
for labelAt : Int in 1 ... totalLabels
{
var addedConstraint : [NSLayoutConstraint] = [NSLayoutConstraint]()
let label : UILabel = UILabel(frame: CGRect.zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "_"
label.textColor = UIColor.white
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
if (lastAddedLabel != nil)
{
// Add left constraint to previous label
let leftConstraint : NSLayoutConstraint = NSLayoutConstraint(item: label, attribute: .left, relatedBy: .equal, toItem: lastAddedLabel!, attribute: .right, multiplier: 1, constant: 8)
let equalWidth : NSLayoutConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: lastAddedLabel!, attribute: .width, multiplier: 1, constant: 0)
addedConstraint.append(contentsOf: [ leftConstraint, equalWidth ])
}
else
{
// Add left constraint to super view
let leftConstraint : NSLayoutConstraint = NSLayoutConstraint(item: labelContainerView, attribute: .left, relatedBy: .equal, toItem: label, attribute: .left, multiplier: 1, constant: 0)
addedConstraint.append(leftConstraint)
}
// Add top bottom constraint
let topConstraint : NSLayoutConstraint = NSLayoutConstraint(item: labelContainerView, attribute: .top, relatedBy: .equal, toItem: label, attribute: .top, multiplier: 1, constant: 0)
let bottomConstraint : NSLayoutConstraint = NSLayoutConstraint(item: labelContainerView, attribute: .bottom, relatedBy: .equal, toItem: label, attribute: .bottom, multiplier: 1, constant: 0)
addedConstraint.append(contentsOf: [ topConstraint, bottomConstraint ])
// Add right constraint if this is the last label
if (labelAt == totalLabels)
{
let rightConstraint : NSLayoutConstraint = NSLayoutConstraint(item: labelContainerView, attribute: .right, relatedBy: .equal, toItem: label, attribute: .right, multiplier: 1, constant: 0)
addedConstraint.append(rightConstraint)
}
labelContainerView.addSubview(label)
labelContainerView.addConstraints(addedConstraint)
lastAddedLabel = label
}
self.view.layoutIfNeeded()
This gives out the output:
Changes you might need to make:
Change the "totalLabels" number depending on your requirement
Maybe adding another width constraint to the first generated label in order to define a specific with for all labels - the rest of the labels keep the same width as the first
Adjust the containing view margins from both sides for different devices
Handle new lines manually by pre-calculating the width the labels might use.
Good luck
I solved my problem and this answer :
func createTarget(id: Int) {
listdata = dbHelpr.getDatabase(rowId: id)
var targetHeigt = CGFloat()
let viewWidth = self.view.frame.size.width
if viewWidth == 320 {
targetHeigt = 2.5
} else {
targetHeigt = 5
}
for data in listdata {
let yAxis : CGFloat = (self.view.frame.height) * 60%
let i = data.ans.length
// char count
let width: Int = Int(view.frame.size.width) - 40
// frame width
var targetWidth: Int = (width - (i - 1) * 5) / i
if targetWidth > 50 {
targetWidth = 50
}
let totalWidth: Int = (targetWidth * i) + ((i - 1) * 5)
for x in 0..<i {
let currentWidth: Int = (width / 2) - (totalWidth / 2) + (x * targetWidth) + (x * 5) + 20
let targetLabel = UILabel(frame: CGRect(x: CGFloat(currentWidth), y: yAxis, width: CGFloat(targetWidth), height: targetHeigt))
targetLabel.backgroundColor = .white
targetLabel.layer.masksToBounds = true
targetLabel.layer.cornerRadius = 5
targetLabel.textAlignment = .center
targetLabel.textColor = .white
for i in listdata {
let tar = data.ans.characters.map{String($0)}
targetLabel.text = String(describing: tar)
}
self.view.addSubview(targetLabel)
}
}
}

UIStackView not displaying

In my viewController viewDidLoad method I have:
let stackView = UIStackView()
stackView.axis = .Vertical
self.view.addSubview(stackView)
for _ in 1..<100{
let vw = UIButton(type: .System)
vw.setTitle("Button", forState: .Normal)
stackView.addArrangedSubview(vw)
}
but when I compile I only get a totally white screen.
What am I doing wrong?
You're adding the stack view to the parent view, but you're not telling the parent view how to lay the stack view out. If you're using constraint based layout, you need to pin the stack view to some of the edges of the parent view:
view.addConstraint(NSLayoutConstraint(item: stackView, Attribute: .Top, relatedBy: .Equal, toItem: self.view, attribute: .Top, multiplier: 1.0, constant: 0.0)
view.addConstraint(NSLayoutConstraint(item: stackView, Attribute: .Leading, relatedBy: .Equal, toItem: self.view, attribute: .Leading, multiplier: 1.0, constant: 0.0)
view.addConstraint(NSLayoutConstraint(item: stackView, Attribute: .Trailing, relatedBy: .Equal, toItem: self.view, attribute: .Trailing, multiplier: 1.0, constant: 0.0)
This will pin the stack view to the top, leading and trailing edges of the view.
The other approach would be to use Interface Builder to set up the constraints, as they are pretty verbose to manage in code.
You have add constraints or set frame for your stack view. Please check modified code below :
let stackView = UIStackView()
stackView.axis = .Vertical
stackView.distribution = .FillEqually
for i in 1..<100{
let vw = UIButton(type: .System)
vw.setTitle("Button\(i)", forState: .Normal)
stackView.addArrangedSubview(vw)
}
self.view.addSubview(stackView)
stackView.frame = self.view.bounds