Swift make percentage of width programmatically - ios

I am programmatically creating multiple buttons in Swift for a UITableViewCell, and would like to set their widths to be a certain percentage width of the UITableViewCell so that they use up all of the space. Right now I am creating the buttons in a for loop (I want to be able to create a variable amount of buttons) and am using
button.buttonWidth = self.contentView.frame.width / numberOfButtons
where button is a UIButton and self is a UITableViewCell and numberOfButtons is obviously the number of buttons. I have also tried:
button.buttonWidth = self.frame.size.width / numberOfButtons
and every combination of using/not using .size and using/not using contentView. These don't seem to work, however, as the buttons are too big. Anyone know how to accomplish this?
NOTE: I CAN'T use Autolayout in the storyboard, as I'm creating a variable number of buttons.

You say:
NOTE: I CAN'T use Autolayout in the storyboard, as I'm creating a variable number of buttons.
You can't add the variable number of buttons right in Interface Builder, but there's nothing to stop you from using autolayout. The basic autolayout constraints you need to set up are:
Set buttons to have same width;
Set buttons to have their leading constraint connected to the previous button's trailing constraint;
The first button should have its leading constraint connected to the container view; and
The last button should have its trailing constraint connected to the container, too.
So, for example, in Swift 3, you could do something like:
var previousButton: UIButton?
for _ in 0 ..< count {
let button = ...
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
// if no "previous" button, hook leading constraint to its superview;
// otherwise hook leading constraint and width to previous button
if previousButton == nil {
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 5).isActive = true
} else {
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: previousButton!.trailingAnchor, constant: 5),
button.widthAnchor.constraint(equalTo: previousButton!.widthAnchor, constant: 5)
])
}
// add vertical constraints
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: view.topAnchor, constant: 50),
view.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: 5)
])
previousButton = button
}
// make sure to hook last button's trailing anchor to superview, too
view.trailingAnchor.constraint(equalTo: previousButton!.trailingAnchor, constant: 5).isActive = true
Or, in Swift 2:
var previousButton: UIButton?
for _ in 0 ..< buttonCount {
let button = ...
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
// if no "previous" button, hook leading constraint to its superview;
// otherwise hook leading constraint and width to previous button
if previousButton == nil {
button.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: 5).active = true
} else {
NSLayoutConstraint.activateConstraints([
button.leadingAnchor.constraintEqualToAnchor(previousButton!.trailingAnchor, constant: 5),
button.widthAnchor.constraintEqualToAnchor(previousButton!.widthAnchor, constant: 5)
])
}
// add vertical constraints
NSLayoutConstraint.activateConstraints([
button.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 5),
view.bottomAnchor.constraintEqualToAnchor(button.bottomAnchor, constant: 5)
])
previousButton = button
}
// make sure to hook last button's trailing anchor to superview, too
view.trailingAnchor.constraintEqualToAnchor(previousButton!.trailingAnchor, constant: 5).active = true
And, instead of fixed spacing between the buttons, you also could use UILayoutGuide, too (making the guides the same size as each other but a percent of the width of the buttons, achieving a more natural spacing regardless of the width of the superview).
You also can use UIStackView, which is another great way to get controls evenly spaced in a view.
Bottom line, there are many ways to achieve this using autolayout.

Maybe because you're doing this before the cell layouts its subviews and it's size is the same as size set in Any-Any size class 600 * 600.
Try doing adding your buttons in layoutSubviews method, like this:
override func layoutSubviews() {
super.layoutSubviews()
self.layoutIfNeeded()
// add your buttons here
}

Hi there is multiple ways of solving your problem:
Using Autolayout, you can using Autolayout in code and in StoryBoard ;-). Bear with me, I didn't check the code of Autolayout but the logic is hereā€¦
let button1: UIButton!, button2: UIButton!, button3: UIButton!
convenience init() {
self.init(frame:CGRectZero)
contentView.addSubview(button1)
contentView.addSubview(button2)
contentView.addSubview(button3)
button1.translatesAutoresizingMaskIntoConstraints = false
button2.translatesAutoresizingMaskIntoConstraints = false
button3.translatesAutoresizingMaskIntoConstraints = false
addConstraint(
item: button1,
attribute: .Left,
relatedBy: .Equal,
toItem: self,
attribute: .Left,
multiplier: 1,
constant: 8)
)
addConstraint(
item: button2,
attribute: .Left,
relatedBy: .Equal,
toItem: button1,
attribute: .Right,
multiplier: 1,
constant: 8)
)
addConstraint(
item: button1,
attribute: .Left,
relatedBy: .Equal,
toItem: button2,
attribute: .Right,
multiplier: 1,
constant: 8)
}
Source: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/
Using Snapkit
let button1: UIButton!, button2: UIButton!, button3: UIButton!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
button1 = UIButton()
button1.setTitle("Button 1", forState: .Normal)
contentView.addSubview(button1)
button1.snp_makeConstraints { (make) in
make.bottom.equalTo(contentView.snp_botton)
make.left.equalTo(contentView.snp_left)
make.height.equalTo(20)
}
button2 = UIButton()
button2.setTitle("Button 2", forState: .Normal)
contentView.addSubview(button2)
button2.snp_makeConstraints { (make) in
make.bottom.equalTo(contentView.snp_botton)
make.left.equalTo(button2.snp_right)
make.height.equalTo(20)
}
button1 = UIButton()
button1.setTitle("Button 3", forState: .Normal)
contentView.addSubview(button2)
button1.snp_makeConstraints { (make) in
make.bottom.equalTo(contentView.snp_botton)
make.left.equalTo(button2.snp_right)
make.height.equalTo(20)
}
}
Source: http://snapkit.io/docs/
My preferred version is Snapkit because it is less verbose than Autolayout.

UIStackView is exactly what you need. Check out my tutorial below:
http://www.raizlabs.com/dev/2016/04/uistackview/

Related

Which lifecycle event to use for declaring layout constraints in a child view controller?

I have a simple parent view controller and I'm adding a child view controller to it. Here's the parent:
class ParentViewController: UIViewController {
private var childViewController: ChildViewController!
override func viewDidLoad() {
super.viewDidLoad()
childViewController = ChildViewController()
addChild(childViewController)
view.addSubview(childViewController.view)
childViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
childViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
childViewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50),
childViewController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -50),
childViewController.view.heightAnchor.constraint(equalToConstant: 100)
])
}
}
The child view controller is declared like this:
class ChildViewController: UIViewController {
private let label1: UILabel = {
let label = UILabel()
label.text = "First label"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let label2: UILabel = {
let label = UILabel()
label.text = "Second label"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
view.translatesAutoresizingMaskIntoConstraints = false
setupSubViews()
}
private func setupSubViews() {
view.addSubview(label1)
view.addSubview(label2)
print("view.frame.size: \(view.frame.size)")
NSLayoutConstraint.activate([
label1.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
label1.centerYAnchor.constraint(equalTo: view.topAnchor, constant: self.view.frame.size.height * (1/3)),
label2.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
label2.centerYAnchor.constraint(equalTo: view.topAnchor, constant: self.view.frame.size.height * (2/3)),
])
}
}
Running the code produces the following:
The position of the two labels are obviously not what I intended. What I'm trying to do in the child view controller is define centerYAnchor constraints for the two labels using the height of the child view controller's view after it has been positioned in the parent view controller. If I print out the value of view.frame.size inside of setupSubViews() in my child view controller, the size of the view is the entire screen (428.0 x 926.0, in my case). Presumably, this is because the child view controller's view hasn't been fully loaded/positioned in the parent view controllers view yet?
So I moved the call to setupSubViews() into viewDidLayoutSubviews() of the child view controller and then the value of view.frame.size is correct (328.0 x 100.0) and the labels are positioned correctly within the child view controller's view. But I do see viewDidLayoutSubviews() being called multiple times, though, so I'm wondering if that's really the "correct" lifecycle method for declaring constraints like this? I've seen some people suggest using a boolean to ensure that the constraint code only runs once but I'm not sure that's the right way to handle this situation either.
The constraint you create this way must be updated whenever the height of the view changes, which can happen multiple times. When this happens, viewDidLayoutSubviews gets called. So it is not inappropriate to put the code to update the constraints' constants in viewDidLayoutSubviews, just because it is called multiple times. You don't want to set the constraints' constants only on the first time the height of the view changes, right?
Though, note that you should just update the constraint's constants in viewDidLayoutSubviews, and not call setupSubview there. You should call setupSubview in loadView, not viewDidLoad, since you are building the view programmatically. See What is the difference between loadView and viewDidLoad?
In fact, there is a much better of doing this. Rather than making the constraints constant, you can constraint the label's center Y to the view's center Y with a multiplier. Label 1 will have a multiplier of 2/3, and label 2 will have 4/3. This will make them one third, and two thirds of the way from the top of the view respectively,
NSLayoutConstraint(
item: label1,
attribute: .centerY,
relatedBy: .equal,
toItem: view,
attribute: .centerY,
multiplier: 2/3,
constant: 0),
NSLayoutConstraint(
item: label2,
attribute: .centerY,
relatedBy: .equal,
toItem: view,
attribute: .centerY,
multiplier: 4/3,
constant: 0),

NSLayoutConstraints programmatically

I want to add constraints to a view programmatically.
This is what I did:
extension UIView {
func bottomToTop(other: UIView) {
self.translatesAutoresizingMaskIntoConstraints = false
other.translatesAutoresizingMaskIntoConstraints = false
let constraint = NSLayoutConstraint(
item: self,
attribute: .bottom,
relatedBy: .equal
toItem: other,
attribute: .top,
multiplier: 1.0,
constant: 0.0
)
superview?.addConstraint(constraint)
constraint.isActive = true
}
}
let label = UILabel()
label.text = "Lenaaaaa"
label.sizeToFit()
label.backgroundColor = .green
let label1 = UILabel()
label1.text = "Lena 2"
label1.sizeToFit()
label1.backgroundColor = .green
let uiView = UIView(frame: frame) (not zero)
uiView.addSubview(label)
uiView.addSubview(label2)
label.bottomToTop(label2)
Why do I end up with this?
Why do I end up with this?
Because your constraints are ambiguous. Once you add even one constraint that affects a view, you must describe that view's position and size in terms of autolayout completely. (And you must stop talking about .frame, as it is now effectively meaningless.)
Thus, you have said only
label.bottomToTop(label2)
But you have not said where the top of label is, where the left of label is, where the left of label2 is, and so on. Thus the autolayout engine throws up its hands in despair.
You could easily have discovered this just by running your app and using the view debugger. It puts up great big exclamation marks telling you what your autolayout issues are.

Remove trailing constraint with ib action- Swift

I have created a View Controller with a segmented controller at the top. When you tap the segmented controller it just acts as a button and changes whether the imageView inside of the controller is portrait mode or in landscape mode just by calling a function that changes it to the according dimensions.
My problem is that the way I made it change is I just added contraints to the imageView, but when changing to landscape mode the trailing constraint doesn't get removed.
And, this is the code to change the imageView to portrait (The imageView already has the top and bottom contraints on it) :
func portraitContraints() {
NSLayoutConstraint.activate ([
// the trailing contraint
imageView.trailingAnchor.constraint(equalTo: viewContainer.safeAreaLayoutGuide.trailingAnchor, constant: -75), // this is the contraints that doesn't get removed
// the leading contraints
imageView.leadingAnchor.constraint(equalTo: viewContainer.safeAreaLayoutGuide.leadingAnchor, constant: 75
])
// the aspect ratio contraints
imageView.addConstraint(NSLayoutConstraint(item: self.imageView as Any,attribute: .height,relatedBy: .equal,toItem: self.imageView,attribute: .width,multiplier: (4.0 / 3.0),constant: 0))
}
This is the code to change the imageView to landscape:
func landscapeContraints() {
NSLayoutConstraint.activate([
// the trailing contraints
imageView.trailingAnchor.constraint(equalTo: viewContainer.safeAreaLayoutGuide.trailingAnchor, constant: 0),
// the leading contraint
imageView.leadingAnchor.constraint(equalTo: viewContainer.safeAreaLayoutGuide.leadingAnchor, constant: 0)
])
// the aspect ratio contraint
imageView.addConstraint(NSLayoutConstraint(item: self.imageView as Any,attribute: .height,relatedBy: .equal,toItem: self.imageView,attribute: .width,multiplier: (9.0 / 16.0),constant: 0))
}
This code works like a charm, except the only problem is that the trailing constraint from the portrait mode will stay on the view (the -75 constant constraint).
Landscape looks like this (notice the right side constant is -75):
Portrait looks like this:
You can do this by adding both ratio constraints, with different priorities. Change the priorities to make the desired ratio "active".
And, create leading and trailing constraint vars, so you can change their constants.
Here's a quick example:
class ToggleConstraintsViewController: UIViewController {
let imageView = UIImageView()
var isPortrait: Bool = true
var portraitConstraint: NSLayoutConstraint!
var landscapeConstraint: NSLayoutConstraint!
var leadingConstraint: NSLayoutConstraint!
var trailingConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
imageView.backgroundColor = .blue
imageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
let g = view.safeAreaLayoutGuide
portraitConstraint = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 4.0/3.0)
portraitConstraint.priority = .defaultHigh
landscapeConstraint = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 9.0/16.0)
landscapeConstraint.priority = .defaultLow
leadingConstraint = imageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 75.0)
trailingConstraint = imageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -75.0)
NSLayoutConstraint.activate([
// y-position constraint does not change
imageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// these will have their priorities changed when desired
portraitConstraint,
landscapeConstraint,
// these will have their constants changed when desired
leadingConstraint,
trailingConstraint,
])
let t = UITapGestureRecognizer(target: self, action: #selector(self.toggleOrientation(_:)))
view.addGestureRecognizer(t)
}
#objc func toggleOrientation(_ g: UITapGestureRecognizer) -> Void {
isPortrait.toggle()
portraitConstraint.priority = isPortrait ? .defaultHigh : .defaultLow
landscapeConstraint.priority = isPortrait ? .defaultLow : .defaultHigh
leadingConstraint.constant = isPortrait ? 75.0 : 0.0
trailingConstraint.constant = isPortrait ? -75.0 : 0.0
}
}
Each time you tap the view, the imageView's width:height ratio will change, and the leading/trailing constraint constants will change.
Try removing the old constraints before adding in the new ones. Here's Apples documentation on how to do that: https://developer.apple.com/documentation/uikit/uiview/1622593-removeconstraints

Swift PureLayout: Add x points of padding above and below tallest vertically centered subview

I am using the PureLayout framework and writing my UIView in Swift.
I have a view, which contains four buttons that are not all the same height.
I have added constraints to equally space these buttons, and center them vertically within the view, and added the padding on either side of the buttons on the end. However, I'm not sure how to set the height / top constraints of the view so that it has an 8.0 point gap between it and the top of the tallest button. I can add the constraints to the top and bottom of each button so that they are all >= 8.0 pixels from the edge of the view, but that would mean that the view could stretch vertically while still maintaining those constraints. I won't know which of these buttons is the tallest until runtime.
The only option I can think of is iterating through my buttons before hand and finding the tallest to store in a variable, and using that while making my constraints, but I was wondering if there's a way to do this with PureLayout a bit more simply, without having to run through all of those objects. Is there a method I'm missing that says something like "Make this value as small as possible while satisfying the rest of the constraints" that I could apply to the height of the view, instead?
let topOffset = CGFloat(8.0)
let bottomOffset = CGFloat(8.0)
button1.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button1, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
button2.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button2, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
button3.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button3, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
button4.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button4, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
//MISSING: Somehow make self's height as small as possible
I apologize for the sloppiness, but here is in image of what I'm aiming for sort of. Ignore the horizontal spacing. The rectangle is the view, and the circles are the buttons, with varying sizes. The view needs to have 8.0 points of padding above/below the tallest of these buttons, i.e. the blue one in this case. They are all centered vertically within the view, and I don't know their sizes until I dynamically set the images while instantiating the view.
For what I can understand, this is what you are trying to achieve, right?
To achieve this, you could use an UIStackView with equal spacing as a container, and add all your buttons as children to it. The stack view will automatically assume the height of the tallest button, and that you can space out 8px from the top.
You could use a mixture of a container view and a stackview like this:
override func viewDidLoad() {
super.viewDidLoad()
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.backgroundColor = .lightGray
view.addSubview(container)
container.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
container.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fillProportionally
stackView.spacing = 8
container.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: container.topAnchor, constant: 8).isActive = true
stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8).isActive = true
container.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 8).isActive = true
container.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 8).isActive = true
let buttonHeights: [CGFloat] = [30, 50, 40, 60]
for (i, buttonHeight) in buttonHeights.enumerated() {
let button = RoundButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .darkGray
button.setTitle("\(i)", for: .normal)
button.setTitleColor(.white, for: .normal)
stackView.addArrangedSubview(button)
button.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true
button.widthAnchor.constraint(equalTo: button.heightAnchor).isActive = true
}
}
Note: My custom RoundButton class simply sets the corner radius to make it a round button.
Result:

Adding constraints to UITableViewCell contentView

I am trying to add constraints to tableViewCellSubViews, like so -
import UIKit
class SnakeTableViewCell: UITableViewCell {
var lessonViews = Array<UIView>()
override func awakeFromNib() {
super.awakeFromNib()
for var i = 0; i < 3; ++i
{
var view = UIView(frame: CGRectMake(CGFloat(i) * 110.0, 0.0, 100.0, 100.0))
view.backgroundColor = UIColor.redColor()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
self.contentView.addSubview(view)
lessonViews.append(view)
}
self.addConstraints(NSLayoutConstraint.constraintsForEvenDistributionOfViews(lessonViews, relativeToCenterOfView: self, vertically: false))
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
And the constraints code -
extension NSLayoutConstraint {
class func constraintsForEvenDistributionOfViews(views:Array<UIView>,relativeToCenterOfView toView:UIView, vertically:Bool ) -> Array<NSLayoutConstraint> {
var constraints = Array<NSLayoutConstraint>()
let attribute = vertically ? NSLayoutAttribute.CenterY : NSLayoutAttribute.CenterX
for (index, view) in enumerate(views) {
let multiplier = CGFloat(2*index + 2) / CGFloat(views.count + 1)
let constraint = NSLayoutConstraint(item: view, attribute: attribute, relatedBy: NSLayoutRelation.Equal, toItem:toView, attribute: attribute, multiplier: multiplier, constant: 0)
constraints.append(constraint)
}
return constraints
}
}
The issue is that when I add the constraints, all the subviews disappears.
Any idea what am I doing wrong ?
Thanks
The multiplier generally is 1. The constant is the variable (horizontal or vertical amount.
You can setup your custom cell, its evenly distributed subViews, and its constraints, all within Storyboard. It's much simpler to do it in Interface Builder, than creating and constraining views in code.
You simply dequeue a reusable cell, and it's already got its properly spaced views, because the cell instantiated and spaced them for you.
You can add constraints to the contentView of a UITableViewCell in init(style:, reuseIdentifier:) as follows (in this case adjusting with an inset of 5pt):
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 5).isActive = true
contentView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5).isActive = true
contentView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -5).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5).isActive = true
It works, but... the system will complain:
Changing the translatesAutoresizingMaskIntoConstraints property of the contentView of a UITableViewCell
is not supported and will result in undefined behavior
The warning appears only one time (even though I have multiple cells)
I am doing this to add an inner border and a shadow (without using additional views that will affect the smooth scroll). But the 'not supported' and 'undefined behavior' are things you may consider if you want to use this in a production environment.

Resources