UICollectionViewListCell always sets a height of 44 that conflicts with anything else I set in AutoLayout

Viewed 834

I'm playing around with the new UICollectionViewListCell and UIContentConfigurations and they are awesome, but I can't figure out how to change the default cell height of 44 that UIKit automatically sets for these cells. Here's an example cell:


class TextFieldCell: UICollectionViewListCell {

    struct ContentConfiguration: UIContentConfiguration, Hashable {

        struct Handlers: ContentConfigurationHandlers {
            var onTextChanged: ((_ newText: String) -> Void)?
        }

        var handlers: Handlers?

        var text: String?
        var placeholder: String?
        var keyboardType: UIKeyboardType?

        func makeContentView() -> UIView & UIContentView {
            ContentView(configuration: self)
        }

        func updated(for state: UIConfigurationState) -> TextFieldCell.ContentConfiguration {
            guard let state = state as? UICellConfigurationState else { return self }
            var updatedConfiguration = self
            return updatedConfiguration
        }

    }

    class ContentView: UIView, UIContentView, UITextFieldDelegate {
        private var appliedConfiguration: TextFieldCell.ContentConfiguration!
        var configuration: UIContentConfiguration {
            get {
                appliedConfiguration
            }
            set {
                guard let newConfiguration = newValue as? TextFieldCell.ContentConfiguration else {
                    return
                }

                apply(configuration: newConfiguration)
            }
        }

        let nameTextField: UITextField = UITextField()

        init(configuration: TextFieldCell.ContentConfiguration) {
            super.init(frame: .zero)
            setupInternalViews()
            apply(configuration: configuration)
        }

        required init?(coder: NSCoder) {
            fatalError()
        }

        private func setupInternalViews() {
            addSubview(nameTextField)

            nameTextField.delegate = self
            nameTextField.clearButtonMode = .always
            nameTextField.autocapitalizationType = .sentences
            
            nameTextField.addAction(UIAction(handler: { [unowned self] action in
                textChanged()
            }), for: .editingChanged)

            NSLayoutConstraint.activate([
                nameTextField.leadingAnchor.constraint(equalTo: leadingAnchor),
                nameTextField.topAnchor.constraint(equalTo: topAnchor),
                nameTextField.trailingAnchor.constraint(equalTo: trailingAnchor),
                nameTextField.bottomAnchor.constraint(equalTo: bottomAnchor),
                nameTextField.heightAnchor.constraint(equalToConstant: 200) // Example height
            ])
        }

        /// Apply a new configuration.
        /// - Parameter configuration: The new configuration
        private func apply(configuration: TextFieldCell.ContentConfiguration) {
            guard appliedConfiguration != configuration else { return }
            appliedConfiguration = configuration
            
            nameTextField.text = appliedConfiguration.text
            nameTextField.placeholder = appliedConfiguration.placeholder
            nameTextField.keyboardType = appliedConfiguration.keyboardType ?? .default
            nameTextField.autocapitalizationType = .sentences
        }
        
        private func textChanged() {
            appliedConfiguration.handlers?.onTextChanged?(nameTextField.text ?? "")
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            textField.resignFirstResponder()
            return true
        }
    }
}

The important part is setting the layout constraints for the text field:

NSLayoutConstraint.activate([
    nameTextField.leadingAnchor.constraint(equalTo: leadingAnchor),
    nameTextField.topAnchor.constraint(equalTo: topAnchor),
    nameTextField.trailingAnchor.constraint(equalTo: trailingAnchor),
    nameTextField.bottomAnchor.constraint(equalTo: bottomAnchor),
    nameTextField.heightAnchor.constraint(equalToConstant: 200) // Example height
])

I set the height to 200 (as an example). When I run this, I get this:

2020-11-08 11:12:01.805610+0100 Sophia[11924:720407] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
    (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSAutoresizingMaskLayoutConstraint:0x600001111f40 h=--& v=--& _TtCC6Sophia13TextFieldCell11ContentView:0x7fc0ed27a1c0.height == 44   (active)>",
    "<NSLayoutConstraint:0x6000011198b0 UITextField:0x7fc0ed27a370.height == 200   (active)>",
    "<NSLayoutConstraint:0x600001119810 V:|-(0)-[UITextField:0x7fc0ed27a370]   (active, names: '|':_TtCC6Sophia13TextFieldCell11ContentView:0x7fc0ed27a1c0 )>",
    "<NSLayoutConstraint:0x600001119b80 UITextField:0x7fc0ed27a370.bottom == _TtCC6Sophia13TextFieldCell11ContentView:0x7fc0ed27a1c0.bottom   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x6000011198b0 UITextField:0x7fc0ed27a370.height == 200   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

As you see, there's an auto-generated height constraint setting the height to 44, which creates a conflict. Why is that there? And how can I change it?

1 Answers

I ran into the same issue. The best way I can describe this for myself right now is, that the constraints involving the UIContentView need to have "more than 1 satisfiable solution", where the solution you want it to have is only the "best" out of all possible ones.

So in a case pretty much the same as yours, I solved this by lowering the priority of the bottom constraint to .defaultHigh. Initially I thought maybe setting the height's priority to a lower value would be enough, but it then used that view's intrinsicContentSize instead (in my case it was a UIImageView). Which is not what I wanted.

So in summary:

let bottomConstraint = nameTextField.bottomAnchor.constraint(equalTo: bottomAnchor)
bottomConstraint.priority = .defaultHigh
NSLayoutConstraint.activate([
    nameTextField.leadingAnchor.constraint(equalTo: leadingAnchor),
    nameTextField.topAnchor.constraint(equalTo: topAnchor),
    nameTextField.trailingAnchor.constraint(equalTo: trailingAnchor),
    bottomConstraint,
    nameTextField.heightAnchor.constraint(equalToConstant: 200) // Example height
])

should fix it.

Related