Adjusting ContentView size using SnapKit

222 views Asked by At

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

1

There are 1 answers

6
DonMag On BEST ANSWER

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:

enter image description here


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)