Search code examples
iosswiftlayoutuikitsnapkit

Adjusting ContentView size using SnapKit


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


Solution

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