Swift - Visual format language - how to make multiple rows and columns - swift

I just started swift programming and i'm facing difficulties with visual format constraints. I'm trying to make multiple tables of 3 by 6 rows and columns with and header on top of the table. I have added the names of the row and columns but it's not aligned in the expected (by me) order. The problem in below code is: the line
> addConstraintsWithFormat(format: "H:|-40-[v0][v1][v2]-[v3]-[v4]-|",
> views: cashLabel, pinLabel, idealLabel, houseLabel,
> totalPerPayMethodLabel) is placed in between the rows of
> addConstraintsWithFormat(format: "V:|-[v0(30)]-[v1]-[v2]-[v3]-|",
> views: timePeriodLabel, highBtwLabel, lowBtwLabel, totalPerBtwLabel).
Also the cashLabel has a big gap with the pinLabel. When i remove the (30) from view v0 the line with the cashLabel, pinLabel etc. is placed above the other rows (V:) as expected. Also the cashLabel does not seem to be affected by the H:-40-[v0] etc.
class AccountingCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let timePeriodLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.red
label.text = "Header"
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
return label
}()
let highBtwLabel: UILabel = {
let label = UILabel()
label.text = "high vat"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let lowBtwLabel: UILabel = {
let label = UILabel()
label.text = "low vat"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let cashLabel: UILabel = {
let label = UILabel()
label.text = "Cash"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let pinLabel: UILabel = {
let label = UILabel()
label.text = "Pin"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let idealLabel: UILabel = {
let label = UILabel()
label.text = "IDEAL"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let houseLabel: UILabel = {
let label = UILabel()
label.text = "House"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let totalPerBtwLabel: UILabel = {
let label = UILabel()
label.text = "Totaal"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let totalPerPayMethodLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.red
label.text = "Totaal"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
func setupViews() {
addSubview(timePeriodLabel)
addSubview(highBtwLabel)
addSubview(lowBtwLabel)
addSubview(cashLabel)
addSubview(pinLabel)
addSubview(idealLabel)
addSubview(houseLabel)
addSubview(totalPerBtwLabel)
addSubview(totalPerPayMethodLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: timePeriodLabel)
addConstraintsWithFormat(format: "H:|-40-[v0][v1][v2]-[v3]-[v4]-|", views: cashLabel, pinLabel, idealLabel, houseLabel, totalPerPayMethodLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: timePeriodLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: highBtwLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: lowBtwLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: totalPerBtwLabel)
addConstraintsWithFormat(format: "V:|-[v0(30)]-[v1]-[v2]-[v3]-|", views: timePeriodLabel, highBtwLabel, lowBtwLabel, totalPerBtwLabel)
addConstraintsWithFormat(format: "V:|-[v0]-|", views: cashLabel)
addConstraintsWithFormat(format: "V:|-[v0]-|", views: pinLabel)
addConstraintsWithFormat(format: "V:|-[v0]-|", views: idealLabel)
addConstraintsWithFormat(format: "V:|-[v0]-|", views: houseLabel)
addConstraintsWithFormat(format: "V:|-[v0]-|", views: totalPerPayMethodLabel)
}

Here the cell height is 300. And also you have to add more UILabels to fill those tables.
addConstraintsWithFormat(format: "H:|[v0]|", views: timePeriodLabel)
addConstraintsWithFormat(format: "H:|-60-[v0(==v1)][v1(==v2)][v2(==v3)][v3(==v4)][v4]-|", views: cashLabel, pinLabel, idealLabel, houseLabel, totalPerPayMethodLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: timePeriodLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: highBtwLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: lowBtwLabel)
addConstraintsWithFormat(format: "H:|[v0]|", views: totalPerBtwLabel)
addConstraintsWithFormat(format: "V:|[v0]-(30)-[v1(==v2)][v2(==v3)][v3]-|", views: timePeriodLabel, highBtwLabel, lowBtwLabel, totalPerBtwLabel)
addConstraintsWithFormat(format: "V:|[v0]-230-|", views: cashLabel)
addConstraintsWithFormat(format: "V:|[v0]-230-|", views: pinLabel)
addConstraintsWithFormat(format: "V:|[v0]-230-|", views: idealLabel)
addConstraintsWithFormat(format: "V:|[v0]-230-|", views: houseLabel)
addConstraintsWithFormat(format: "V:|[v0]-230-|", views: totalPerPayMethodLabel)

Auto Layout: quick prototyping and shared UI components with Visual Format Language and JSONLayout provides an extensive example of building a calculator layout with VFL.

Related

UIScrollView constraints unexpected behaviour

I have a simple log in view implemented as follows :
import UIKit
class LoginViewController: UIViewController {
private var safeArea : UILayoutGuide!
private let scrollView : UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.keyboardDismissMode = .onDrag
return view
}()
private let containerView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let logoView : UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 8
view.image = UIImage(named: "logo")!
return view
}()
private let emailOrPhoneTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Email or phone"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.backgroundColor = .systemGray
return view
}()
private let passwordTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Password"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.isSecureTextEntry = true
view.backgroundColor = .systemGray
return view
}()
private let logInButtonView : UIButton = {
let view = UIButton()
view.setTitle("Log in", for: .normal)
view.setTitleColor(.white, for : .normal)
view.setBackgroundImage( UIImage(named: "blue_pixel")!, for: .normal)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
view.addTarget(self, action: #selector(logInButtonClickedHandler), for: .touchUpInside)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
safeArea = view.layoutMarginsGuide
setupViews()
}
private func setupViews()
{
view.addSubview(scrollView)
containerView.addSubview(logoView)
containerView.addSubview(emailOrPhoneTextFieldView)
containerView.addSubview(passwordTextFieldView)
containerView.addSubview(logInButtonView)
scrollView.addSubview(containerView)
let constraints = [
scrollView.topAnchor.constraint(equalTo: safeArea.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
logoView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 120),
logoView.widthAnchor.constraint(equalToConstant: 100),
logoView.heightAnchor.constraint(equalToConstant: 100),
logoView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
emailOrPhoneTextFieldView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
emailOrPhoneTextFieldView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
emailOrPhoneTextFieldView.topAnchor.constraint(equalTo: logoView.bottomAnchor, constant: 120),
emailOrPhoneTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.topAnchor.constraint(equalTo: emailOrPhoneTextFieldView.bottomAnchor),
passwordTextFieldView.leadingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.leadingAnchor),
passwordTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.trailingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.trailingAnchor),
logInButtonView.topAnchor.constraint(equalTo: passwordTextFieldView.bottomAnchor, constant: 16),
logInButtonView.leadingAnchor.constraint(equalTo: passwordTextFieldView.leadingAnchor),
logInButtonView.trailingAnchor.constraint(equalTo: passwordTextFieldView.trailingAnchor),
logInButtonView.heightAnchor.constraint(equalToConstant: 50),
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
#objc private func logInButtonClickedHandler() {
print("button pressed")
}
}
//MARK: Keyboard Notifications
private extension LoginViewController {
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
scrollView.contentInset.bottom = keyboardSize.height
scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
}
}
#objc func keyboardWillHide(notification: NSNotification) {
scrollView.contentInset.bottom = .zero
scrollView.verticalScrollIndicatorInsets = .zero
}
}
Everything is fine with the implementation but 2 things looks very strange for me and I guess I misunderstood smth
If I comment out
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
I see that my container view does not fit the whole screen width (actually it's about 50% of it)
Why? I set trailing and leading constraints to scrollview, which is 100% of view width.
If I comment out
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
I don't get button click events and I'm not able to input anything inside textfields. What is the issue here?
From the Apple Docs:
Constraints between the edges or margins of the scroll view and its
content attach to the scroll view’s content area.
Constraints between the height, width, or centers attach to the scroll
view’s frame.
Hence you need the width constraint in order to make the contentView the full width of the ScrollView's frame.
As above, without that constraint the contentView only has constraints to the top/bottom edge of the scrollView this doesn't define its height and so you need to add full top-to-bottom constraints on the subviews of the contentView in order to define its height.
If you use the View Hierarchy Debugger you'll see the contentView has 0 height without that constraint (it just isn't clipping the content), hence why you can't tap on any controls.
It's worth giving the 'Working with Scroll Views' section of Apple Auto-Layout docs a read.

How to fix UITableView not displaying in UIStackView

I followed a tutorial to build a weather app programmatically (without a storyboard) that displays the current city and temperature. I am modifying it to display a 5 day forecast instead of just the current temperature by adding a UITableView, but it is not showing up.
Here is my WeatherView code:
class WeatherView: UIView, UITableViewDelegate, UITableViewDataSource {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
setupViews()
setupConstraints()
}
private func setupViews() {
self.addSubview(mainStack)
conditionsImageStack.addArrangedSubview(conditionsImageView)
mainStack.addArrangedSubview(conditionsImageStack)
// forecastTable.register(UITableView.self, forCellReuseIdentifier: "forecast")
forecastTable.register(UITableViewCell.self, forCellReuseIdentifier: "forecast")
forecastTable.delegate = self
forecastTable.dataSource = self
mainStack.addArrangedSubview(centerContentStack)
// centerContentStack.addArrangedSubview(temperatureLabel)
centerContentStack.addArrangedSubview(forecastTable)
centerContentStack.addArrangedSubview(cityAndConditionsStack)
cityAndConditionsStack.addArrangedSubview(cityLabel)
cityAndConditionsStack.addArrangedSubview(conditionsLabel)
mainStack.addArrangedSubview(buttonsStack)
buttonsStack.addArrangedSubview(celsiusButton)
buttonsStack.addArrangedSubview(fahrenheitButton)
buttonsStack.addArrangedSubview(UIView(frame: .zero))
}
private func setupConstraints() {
mainStack.pinEdges(to: self)
}
let mainStack: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 10
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = UIEdgeInsets(top: 10, left: 30, bottom: 30, right: 30)
return stackView
}()
let conditionsImageStack: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.alignment = .trailing
stackView.spacing = 10
return stackView
}()
let cityAndConditionsStack: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.spacing = 10
return stackView
}()
let centerContentStack: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.alignment = .center
stackView.spacing = 60
return stackView
}()
// TABLE CODE HERE
var animalArray : [String] = ["elephant", "pig", "goat"]
var forecastTable: UITableView = {
let tableView = UITableView()
let estimatedHeight = tableView.numberOfRows(inSection: 3) //You may need to modify as necessary
// let width = parentView.frame.size.width
let width = estimatedHeight
tableView.frame = CGRect(x: 0, y: 0, width: width, height: estimatedHeight)
return tableView
}()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = forecastTable.dequeueReusableCell(withIdentifier: "forecast")
cell?.textLabel!.text = "Success"
return cell!
}
let temperatureLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 144)
label.textColor = .white
label.textAlignment = .center
label.text = "18°"
return label
}()
let cityLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 36)
label.textColor = .white
label.textAlignment = .center
label.text = "Atlanta"
return label
}()
let conditionsLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 20)
label.textColor = .white
label.textAlignment = .center
label.text = "Sunny"
return label
}()
let conditionsImageView: UIImageView = {
let image = UIImage(named: "sun")
let imageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFit
imageView.widthAnchor.constraint(equalToConstant: image!.size.width).isActive = true
imageView.heightAnchor.constraint(equalToConstant: image!.size.height).isActive = true
return imageView
}()
let celsiusButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("°C", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 73)
button.setTitleColor(.white, for: .normal)
return button
}()
let fahrenheitButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("°F", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 73)
button.setTitleColor(.white, for: .normal)
return button
}()
let buttonsStack: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.axis = .horizontal
stackView.distribution = .equalCentering
stackView.spacing = 10
return stackView
}()
}
Here is my ViewController code:
class WeatherViewController: UIViewController {
var mainView: WeatherView! { return self.view as! WeatherView }
let presenter: WeatherPresenter!
init(with presenter: WeatherPresenter){
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init coder not implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
updateBackground()
}
func updateBackground() {
self.mainView.updateGradient(presenter.backgroundColor)
}
override func loadView() {
self.view = WeatherView(frame: UIScreen.main.bounds)
}
}
Here is my UIView + Constraints code:
extension UIView {
func pinEdges(to view: UIView){
self.translatesAutoresizingMaskIntoConstraints = false
self.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
self.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
I have tried forcing it to display by setting the dimensions with tableView.frame = CGRect(x: 0, y: 0, width: width, height: estimatedHeight) but that did not work.
I am registering the table in the view class instead of of the view controller class, but I am not sure if this is the problem or how to modify it correctly.
You're doing a couple things wrong...
First, in your var forecastTable: UITableView = {...} declaration, you have a line:
let estimatedHeight = tableView.numberOfRows(inSection: 3)
But, at that point, the table view has no dataSource -- even if it did, your dataSource has only section. So the value returned is undefined. If Iprint(esttmatedHeight)I get9223372036854775807. So you are trying to set the frame of your table view to9223372036854775807 x 9223372036854775807`
Next, when you add a table view to a stack view, the stack view will try to arrange it based on the stack view's properties and the table view's intrinsic size. However, the table view has no intrinsic size at that point - you must use auto-layout properties.
So, remove the frame setting for your table view, and after you've added it to the stack view, use:
// I'm guessing you want a height based on number of rows * row height?
// so, something like:
let estimatedHeight = CGFloat(3 * 44)
forecastTable.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
forecastTable.widthAnchor.constraint(equalTo: centerContentStack.widthAnchor),
forecastTable.heightAnchor.constraint(equalToConstant: estimatedHeight),
])
That will make the table view the same width as the centerContentStack that holds it, and give it a height.
At this point, you should see your table.
You are adding mainStack to the superView but not specifying it's dimensions. You need to set constrains to mainStack similar this,
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
mainStack.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
mainStack.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
mainStack.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
Above code fills the entire super view with mainStack. You can modify this code according to your requirement.

Swift Visual Format Language four buttons in centre

I would like to arrange four buttons with Visual Format Language around the central X an Y of a view without hard coding any points, preferring to scale with constraints.
I can only achieve a cluster of buttons to align to the bottom margin, how do I centre them with the spacing you see (e.g. ~20 points) without resorting to NSLayoutConstraint?
I did not place them in a stack, they are all separate buttons.
I read that stacks were not a good idea, but it seems like the logical way, otherwise they stretch out vertically.
Ideally I would like to use VFL to make a calculator UI but am trying this first.
#IBDesignable class images_and_constraints: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
calcButtons()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
calcButtons()
}
private func calcButtons() {
let calcPlus = UIButton()
calcPlus.translatesAutoresizingMaskIntoConstraints = false
calcPlus.setTitle("+", for: .normal)
calcPlus.setTitleColor(UIColor.black, for: .normal)
calcPlus.setTitleColor(UIColor.white, for: .highlighted)
calcPlus.backgroundColor = UIColor.orange
addSubview(calcPlus)
let calcSubtract = UIButton()
calcSubtract.translatesAutoresizingMaskIntoConstraints = false
calcSubtract.setTitle("-", for: .normal)
calcSubtract.setTitleColor(UIColor.black, for: .normal)
calcSubtract.setTitleColor(UIColor.white, for: .highlighted)
calcSubtract.backgroundColor = UIColor.orange
addSubview(calcSubtract)
let calcMultiply = UIButton()
calcMultiply.translatesAutoresizingMaskIntoConstraints = false
calcMultiply.setTitle("x", for: .normal)
calcMultiply.setTitleColor(UIColor.black, for: .normal)
calcMultiply.setTitleColor(UIColor.white, for: .highlighted)
calcMultiply.backgroundColor = UIColor.orange
addSubview(calcMultiply)
let calcDivide = UIButton()
calcDivide.translatesAutoresizingMaskIntoConstraints = false
calcDivide.setTitle("/", for: .normal)
calcDivide.setTitleColor(UIColor.black, for: .normal)
calcDivide.setTitleColor(UIColor.white, for: .highlighted)
calcDivide.backgroundColor = UIColor.orange
addSubview(calcDivide)
let views = ["calcPlus": calcPlus,
"calcSubtract": calcSubtract,
"calcMultiply": calcMultiply,
"calcDivide": calcDivide]
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcPlus]-[calcSubtract(==calcPlus)]-|",
options: .alignAllBottom,
metrics: nil,
views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcMultiply]-[calcDivide(==calcMultiply)]-|",
options: .alignAllTop,
metrics: nil,
views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:[calcSubtract]-[calcDivide(==calcSubtract)]-|",
options: .alignAllCenterX,
metrics: nil,
views: views))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:[calcSubtract]",
options: .alignAllCenterX,
metrics: nil,
views: views))
}
}
Using VFL to center views requires trickery.
Look at this question and particularly this answer for the trick.
For the kind of layout you want, VFL is just not a good fit.
Just one NSLayoutConstraint in addition to VFL would solve it but since you're only interested in VFL, I would suggest you use the trick to center a container view that holds your buttons.
Solution:
func calcButtons() {
//1. Create a container view that will contain your operator buttons
let buttonContainerView = UIView()
buttonContainerView.backgroundColor = UIColor.lightGray
buttonContainerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(buttonContainerView)
//Place it vertically in the center of the superview
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:[superview]-(<=1)-[childView]",
options: .alignAllCenterX,
metrics: nil,
views: ["superview" : self,
"childView" : buttonContainerView]))
//Place it horizontally in the center of the superview + equal widths to superview
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:[superview]-(<=1)-[childView(==superview)]",
options: .alignAllCenterY,
metrics: nil,
views: ["superview" : self,
"childView" : buttonContainerView]))
//2. Create your buttons as you were:
//DRY Fix: Helper function to create button and add it to `buttonContainerView`
func addButton(title: String, selector: Selector? = nil) -> UIButton {
let button = UIButton()
button.backgroundColor = UIColor.orange
button.setTitle(title, for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
button.setTitleColor(UIColor.white, for: .highlighted)
//You might need this later cuz a button gotta do wat a button gotta do
if let selector = selector {
button.addTarget(self, action: selector, for: UIControlEvents.touchUpInside)
}
button.translatesAutoresizingMaskIntoConstraints = false
buttonContainerView.addSubview(button)
return button
}
let calcPlus = addButton(title: "+", selector: #selector(CalculatorView.add))
let calcSubtract = addButton(title: "-")
let calcMultiply = addButton(title: "x")
let calcDivide = addButton(title: "/")
let views = ["calcPlus": calcPlus,
"calcSubtract": calcSubtract,
"calcMultiply": calcMultiply,
"calcDivide": calcDivide]
//Same as before
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcPlus]-[calcSubtract(==calcPlus)]-|",
options: .alignAllBottom,
metrics: nil,
views: views))
//Same as before
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[calcMultiply]-[calcDivide(==calcMultiply)]-|",
options: .alignAllTop,
metrics: nil,
views: views))
/*
Same as before but this time we give a top constraint too
i.e.
"V:|-[calcSubtract]..."
instead of
"V:[calcSubtract]..."
*/
//
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[calcSubtract]-[calcDivide(==calcSubtract)]-|",
options: .alignAllCenterX,
metrics: nil,
views: views))
}
In the end I decided on NSLayoutConstraint.activate of which each button would be reliant on the one before it (rows), with the leading (far left for left-to-right readers) button constrained to the one above it.
calculatriceButtons["7"]!.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 1.0),
calculatriceButtons["7"]!.topAnchor.constraint(equalTo: calculatriceButtons["C"]!.bottomAnchor, constant: 1.0),
This was the best way to assure the buttons scaled on all devices.
There is a new alternative to using VFL which is what I use in code now.
Layout Anchors
Each view has different anchors. leading, trailing, top, bottom, etc...
You can use these to create constraints for you...
NSLayoutConstraint.activate([
viewB.leadingAnchor.constraint(equalTo: viewA.leadingAnchor, constant: 20),
viewA.widthAnchor.constraint(equalTo: viewB.widthAnchor)
])
for example.
Stack View
In addition to that there is an even more modern approach which is to use UIStackView. This is a really useful view that takes away the need to add constraints and does it for you.
let stackView = UIStackView(arrangedSubViews: [viewA, viewB])
stackView.spacing = 20
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fillEqually
You can also nest stack views to create more complex layouts.
Definitely worth looking in to...
https://developer.apple.com/documentation/uikit/uistackview?changes=_6
Creating your layout
let upperStackView = UIStackView(arrangedSubviews: [topLeft, topRight])
upperStackView.axis = .horizontal
upperStackView.distribution = .fillEqually
upperStackView.spacing = 20
let lowerStackView = UIStackView(arrangedSubviews: [bottomLeft, bottomRight])
lowerStackView.axis = .horizontal
lowerStackView.distribution = .fillEqually
lowerStackView.spacing = 20
let mainStackView = UIStackView(arrangedSubviews: [upperStackView, lowerStackView])
mainStackView.axis = .vertical
mainStackView.distribution = .fillEqually
mainStackView.spacing = 20
view.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
mainStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
mainStackView.widthAnchor.constraint(equalToConstant: 200),
mainStackView.heightAnchor.constraint(equalToConstant: 200),
])
Why not VFL?
While VFL was a nice first attempt at AutoLayout, I feel that Apple has moved away from it now and are moving towards these more succinct methods of creating AutoLayout constraints.
It still allows you to think in constraints while writing code but provides a slightly more modern approach.
Of course... you can also create UIStackView in Interface Builder also :D

How do I add a label on top of a gradient?

I have a very basic UICollectionView. Inside my CollectionViewCell, I have a "thumbnailImage" which is just an image. I would like to have a gradient layer that fades from black, to a clear color BUT I would also like to have a UILabel ON TOP of this CAGradient and not underneath. The label is the "MovieTitle". I am programmatically doing everything, including the constraints. How do I perform this? Here is my code
let myView: UIView = {
let view = UIView()
return view
}()
let gradientView: CAGradientLayer = {
let grad = CAGradientLayer()
grad.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
grad.locations = [0.7, 1.2]
return grad
}()
func setupViews() {
thumbnailImageView.addSubview(movieTitle)
addSubview(thumbnailImageView)
thumbnailImageView.addSubview(dividerLine)
thumbnailImageView.addSubview(myView)
myView.layer.addSublayer(gradientView)
addConstrainsWithFormat(format: "H:|[v0]|", views: thumbnailImageView)
addConstrainsWithFormat(format: "V:|[v0]|", views: thumbnailImageView)
thumbnailImageView.addConstrainsWithFormat(format: "H:|[v0]|", views: myView)
thumbnailImageView.addConstrainsWithFormat(format: "V:|[v0]|", views: myView)
thumbnailImageView.addConstrainsWithFormat(format: "H:|[v0]|", views: dividerLine)
thumbnailImageView.addConstrainsWithFormat(format: "V:[v0(0.75)]|", views: dividerLine)
thumbnailImageView.addConstrainsWithFormat(format: "H:|-16-[v0]-16-|", views: movieTitle)
thumbnailImageView.addConstrainsWithFormat(format: "V:[v0(25)]-8-|", views: movieTitle)
}
Try to change the order of your views so that label appears above the view with gradient inside:
myView.layer.addSublayer(gradientView)
thumbnailImageView.addSubview(myView)
thumbnailImageView.addSubview(movieTitle)
thumbnailImageView.addSubview(dividerLine)
addSubview(thumbnailImageView)
You can also insert layers and views below or above already existing layers/views in your hierarchy:
view.insertSubview(subview, at: 0)
view.insertSubview(subview, belowSubview: existingView)
view.insertSubview(subview, aboveSubview: existingView)
layer.insertSublayer(gradientLayer, at: 0)
layer.insertSublayer(gradientLayer, below: anotherLayer)
layer.insertSublayer(gradientLayer, above: anotherLayer)

Programmatic layout : UIStackView alignment doesn't seem to work

I have created a UIViewController with a UIStackView (vertical axis) wrapped in a UIScrollView that's pinned to the edges of the root View with auto-layout constraints.
I have also generated a number of UIButtons and added to the arranged subviews of the UIStackView.
I have tried to no avail to centre align the UIButtons in the UIStackView.
I'm not certain what i'm doing wrong.
Here's the code:
import UIKit
class ViewController: UIViewController {
var scrollView: UIScrollView!
var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.whiteColor()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scrollView]|", options: .AlignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scrollView]|", options: .AlignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .Vertical
stackView.alignment = .Center
scrollView.addSubview(stackView)
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[stackView]|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: ["stackView": stackView]))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[stackView]|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: ["stackView": stackView]))
for _ in 1 ..< 100 {
let vw = UIButton(type: UIButtonType.System)
vw.setTitle("Button", forState: .Normal)
stackView.addArrangedSubview(vw)
}
}
}
An extra equal width constraint between the scrollView and the stackView was needed. Like this:
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[stackView(==scrollView)]", options: .AlignAllCenterX, metrics: nil, views: ["stackView": stackView, "scrollView": scrollView]))
That did it for me.
How about:
stackView.Alignment = UIStackViewAlignment.Center
?