When using Auto Layout my code would look like this:
let safeAreaLayoutGuide = contentView.safeAreaLayoutGuide
let bottomAnchor = userImage.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16.0)
bottomAnchor.priority = .required - 1
NSLayoutConstraint.activate([
userImage.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 16),
userImage.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor
, constant: 16),
userImage.widthAnchor.constraint(equalToConstant: 90),
userImage.heightAnchor.constraint(equalToConstant: 90),
userName.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 16),
userName.leadingAnchor.constraint(equalTo: userImage.trailingAnchor, constant: 16),
userName.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
userStatus.topAnchor.constraint(equalTo: userName.bottomAnchor, constant: 16),
userStatus.leadingAnchor.constraint(equalTo: userImage.trailingAnchor, constant: 16),
userStatus.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
textField.topAnchor.constraint(equalTo: userName.bottomAnchor, constant: 48),
textField.leadingAnchor.constraint(equalTo: userImage.trailingAnchor, constant: 16),
textField.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
textField.heightAnchor.constraint(equalToConstant: 32),
showStatusButton.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 16),
showStatusButton.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16),
showStatusButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
bottomAnchor
])
The bottomAnchor
constraint is made to let contentView
adjust to its content size. Now I'm trying to make the same UI but now using SnapKit
. Here are the constraints for all the objects on the contentView
:
userImage.snp.makeConstraints { make in
make.top.equalTo(self.safeAreaInsets.top).offset(16)
make.left.equalTo(self.safeAreaInsets.left).offset(16)
make.height.width.equalTo(userImage.layer.cornerRadius * 2)
}
userName.snp.makeConstraints { make in
make.top.equalTo(self.safeAreaInsets.top).offset(16)
make.left.equalTo(userImage.snp.right).offset(16)
make.right.equalTo(self.safeAreaInsets.right).inset(-16)
}
userStatus.snp.makeConstraints { make in
make.top.equalTo(userName.snp.bottom).offset(16)
make.left.equalTo(userImage.snp.right).offset(16)
make.right.equalTo(self.safeAreaInsets.right).offset(-16)
}
textField.snp.makeConstraints { make in
make.top.equalTo(userStatus.snp.bottom).offset(40)
make.left.equalTo(self.safeAreaInsets.left).offset(16)
make.right.equalTo(self.safeAreaInsets.right).offset(-16)
make.height.equalTo(32)
}
showStatusButton.snp.makeConstraints { make in
make.top.equalTo(textField.snp.bottom).offset(16)
make.left.equalTo(self.safeAreaInsets.left).offset(16)
make.right.equalTo(self.safeAreaInsets.right).offset(-16)
}
I can't figure out what to replace my bottomAnchor
constraint with. How do I make contentView adjust to what I have? (or basically position it relative to bottom anchor of showStatusButton
.
Just in case, the whole project is here and the file I'm changing is ProfileHeadevView.swift
Instead of trying to set constraints to self.safeAreaInsets
, set them to the contentView
itself...
Here is your original ProfileHeaderView
- using NSLayoutConstraints
- with a couple edits since I don't have your me.login
/ textColor
/ accentColor
/ etc:
class ContraintsProfileHeaderView: UITableViewHeaderFooterView {
public var user: String = "User" {
didSet {
userName.text = user
}
}
// MARK: - Subviews
private var statusText: String = ""
lazy var userImage: UIImageView = {
// let imageView = UIImageView(image: UIImage(named: me.login))
let imageView = UIImageView(image: UIImage(named: "ProfilePicture"))
imageView.layer.cornerRadius = 45
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.isUserInteractionEnabled = true
return imageView
}()
private lazy var userName: UILabel = {
let userName = UILabel()
userName.text = "User Name" //me.login
userName.font = UIFont.systemFont(ofSize: 18, weight: .bold)
userName.textColor = .red // textColor
userName.sizeToFit()
userName.translatesAutoresizingMaskIntoConstraints = false
return userName
}()
private lazy var userStatus: UILabel = {
let userStatus = UILabel()
userStatus.text = "Waiting for something..."
userStatus.font = UIFont.systemFont(ofSize: 14, weight: .regular)
userStatus.textColor = .gray
userStatus.sizeToFit()
userStatus.lineBreakMode = .byWordWrapping
userStatus.textAlignment = .left
userStatus.translatesAutoresizingMaskIntoConstraints = false
return userStatus
}()
private lazy var showStatusButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemBlue // accentColor
button.setTitle("Set status", for: .normal)
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOffset = CGSize(width: 4, height: 4)
button.layer.shadowOpacity = 0.7
button.layer.shadowRadius = 4
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
return button
}()
private lazy var textField: UITextField = {
let textField = UITextField()
textField.placeholder = "Hello, world"
textField.backgroundColor = .white
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
textField.textColor = .black
textField.layer.cornerRadius = 12
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.black.cgColor
textField.translatesAutoresizingMaskIntoConstraints = false
textField.addTarget(self, action: #selector(statusTextChanged(_:)), for: .editingChanged)
return textField
}()
// MARK: - Lifecycle
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
addSuviews()
setupConstraints()
changeBackgroundColor()
}
// MARK: - Actions
@objc func buttonPressed(_ sender: UIButton) {
userStatus.text = statusText
}
@objc func statusTextChanged(_ textField: UITextField) {
if let text = textField.text {
statusText = text
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSuviews()
setupConstraints()
changeBackgroundColor()
}
// MARK: - Private
func changeBackgroundColor() {
#if DEBUG
contentView.backgroundColor = backgroundColor
#else
contentView.backgroundColor = secondaryColor
#endif
}
private func addSuviews() {
contentView.addSubview(userImage)
contentView.addSubview(userName)
contentView.addSubview(userStatus)
contentView.addSubview(textField)
contentView.addSubview(showStatusButton)
}
private func setupConstraints() {
let safeAreaLayoutGuide = contentView.safeAreaLayoutGuide
let bottomAnchor = showStatusButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16.0)
bottomAnchor.priority = .required - 1
NSLayoutConstraint.activate([
userImage.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 16),
userImage.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor
, constant: 16),
userImage.widthAnchor.constraint(equalToConstant: 90),
userImage.heightAnchor.constraint(equalToConstant: 90),
userName.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 16),
userName.leadingAnchor.constraint(equalTo: userImage.trailingAnchor, constant: 16),
userName.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
userStatus.topAnchor.constraint(equalTo: userName.bottomAnchor, constant: 16),
userStatus.leadingAnchor.constraint(equalTo: userImage.trailingAnchor, constant: 16),
userStatus.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
textField.topAnchor.constraint(equalTo: userName.bottomAnchor, constant: 48),
textField.leadingAnchor.constraint(equalTo: userImage.trailingAnchor, constant: 16),
textField.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
textField.heightAnchor.constraint(equalToConstant: 32),
showStatusButton.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 16),
showStatusButton.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16),
showStatusButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16),
bottomAnchor
])
}
}
Here's that same ProfileHeaderView
, but using SnapKit for the constraints:
class SnapsProfileHeaderView: UITableViewHeaderFooterView {
public var user: String = "User" {
didSet {
userName.text = user
}
}
// MARK: - Subviews
private var statusText: String = ""
lazy var userImage: UIImageView = {
// let imageView = UIImageView(image: UIImage(named: me.login))
let imageView = UIImageView(image: UIImage(named: "ProfilePicture"))
imageView.layer.cornerRadius = 45
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.isUserInteractionEnabled = true
return imageView
}()
private lazy var userName: UILabel = {
let userName = UILabel()
userName.text = "User Name" //me.login
userName.font = UIFont.systemFont(ofSize: 18, weight: .bold)
userName.textColor = .red // textColor
userName.sizeToFit()
userName.translatesAutoresizingMaskIntoConstraints = false
return userName
}()
private lazy var userStatus: UILabel = {
let userStatus = UILabel()
userStatus.text = "Waiting for something..."
userStatus.font = UIFont.systemFont(ofSize: 14, weight: .regular)
userStatus.textColor = .gray
userStatus.sizeToFit()
userStatus.lineBreakMode = .byWordWrapping
userStatus.textAlignment = .left
userStatus.translatesAutoresizingMaskIntoConstraints = false
return userStatus
}()
private lazy var showStatusButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemBlue // accentColor
button.setTitle("Set status", for: .normal)
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOffset = CGSize(width: 4, height: 4)
button.layer.shadowOpacity = 0.7
button.layer.shadowRadius = 4
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
return button
}()
private lazy var textField: UITextField = {
let textField = UITextField()
textField.placeholder = "Hello, world"
textField.backgroundColor = .white
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
textField.textColor = .black
textField.layer.cornerRadius = 12
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.black.cgColor
textField.translatesAutoresizingMaskIntoConstraints = false
textField.addTarget(self, action: #selector(statusTextChanged(_:)), for: .editingChanged)
return textField
}()
// MARK: - Lifecycle
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
addSuviews()
setupConstraints()
changeBackgroundColor()
}
// MARK: - Actions
@objc func buttonPressed(_ sender: UIButton) {
userStatus.text = statusText
}
@objc func statusTextChanged(_ textField: UITextField) {
if let text = textField.text {
statusText = text
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSuviews()
setupConstraints()
changeBackgroundColor()
contentView.backgroundColor = .green
}
// MARK: - Private
func changeBackgroundColor() {
#if DEBUG
contentView.backgroundColor = backgroundColor
#else
contentView.backgroundColor = secondaryColor
#endif
}
private func addSuviews() {
contentView.addSubview(userImage)
contentView.addSubview(userName)
contentView.addSubview(userStatus)
contentView.addSubview(textField)
contentView.addSubview(showStatusButton)
}
private func setupConstraints() {
let g = contentView
userImage.snp.makeConstraints { make in
make.top.equalTo(g.snp.top).offset(16)
make.left.equalTo(g.snp.left).offset(16)
make.height.width.equalTo(userImage.layer.cornerRadius * 2)
}
userName.snp.makeConstraints { make in
make.top.equalTo(g.snp.top).offset(16)
make.left.equalTo(userImage.snp.right).offset(16)
make.right.equalTo(g.snp.right).inset(-16)
}
userStatus.snp.makeConstraints { make in
make.top.equalTo(userName.snp.bottom).offset(16)
make.left.equalTo(userImage.snp.right).offset(16)
make.right.equalTo(g.snp.right).offset(-16)
}
textField.snp.makeConstraints { make in
make.top.equalTo(userName.snp.bottom).offset(48)
make.left.equalTo(userImage.snp.right).offset(16)
make.right.equalTo(g.snp.right).offset(-16)
make.height.equalTo(32)
}
showStatusButton.snp.makeConstraints { make in
make.top.equalTo(textField.snp.bottom).offset(16)
make.left.equalTo(g.snp.left).offset(16)
make.right.equalTo(g.snp.right).offset(-16)
make.bottom.equalTo(g.snp.bottom).offset(-16)
}
}
}
and here's a sample view controller that puts one table view using ContraintsProfileHeaderView
above a second table view that uses SnapsProfileHeaderView
:
class ProTestVC: UIViewController {
let tb1 = UITableView()
let tb2 = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
[tb1, tb2].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
v.dataSource = self
v.delegate = self
v.register(UITableViewCell.self, forCellReuseIdentifier: "c")
}
tb1.register(ContraintsProfileHeaderView.self, forHeaderFooterViewReuseIdentifier: "chv")
tb2.register(SnapsProfileHeaderView.self, forHeaderFooterViewReuseIdentifier: "shv")
let g = view.safeAreaLayoutGuide
tb1.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(g).inset(20.0)
make.height.equalTo(300.0)
}
tb2.snp.makeConstraints { make in
make.top.equalTo(tb1.snp.bottom).offset(20.0)
make.leading.trailing.equalTo(g).inset(20.0)
make.height.equalTo(300.0)
}
}
}
extension ProTestVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
c.textLabel?.text = "Row: \(indexPath.row)"
return c
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0, tableView == tb1 {
if let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: "chv") as? ContraintsProfileHeaderView {
v.user = "Constraints"
return v
}
}
if section == 0, tableView == tb2 {
if let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: "shv") as? SnapsProfileHeaderView {
v.user = "SnapKit"
return v
}
}
return nil
}
}
Looks like this:
Worth noting: you may want to make use of the contentView.layoutMarginsGuide
- docs - to get the "recommended amount of padding for content inside of a view"
Edit
When UIKit lays out the table view elements - header / footer / section header/footers / etc - it is possible, in fact usual, that multiple auto-layout "passes" are made to calculate the framing.
If all the cells use the default cell height, we don't get the constraint warning/error messages for its layout.
If some of the cells are NOT the default height, auto-layout initially sets the section header height to its default of 17.6667-points
(on a @3x device)... which causes constraint conflicts... then re-process the layout and adjusts the header height.
We often see this in dynamic tableview cells on their own (particularly when multiline labels are embedded in vertical stack views).
By setting the priority on the bottom constraint to less-than-required, we tell auto-layout to go ahead and break that constraint if necessary, and not complain about it.
That's why the common "fix" is to use:
let bottomAnchor = showStatusButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16.0)
bottomAnchor.priority = .required - 1
bottomAnchor.isActive = true
and we no longer see the messages.
If you want to do that with SnapKit, add the priority modifier:
make.bottom.equalTo(contentView.snp.bottom).offset(-16).priority(999)