Search code examples
swiftuikituistackviewcustom-viewsnapkit

.equalSpacing in UIStackView not working properly with CustomView


I have created a custom view of checkbox / radio button that animates when checked and unchecked. Problem is when I embed the custom view to a stackView that is .equalSpacing the button cannot be tapped anymore, my guess is the borderView(which has the tap gesture) shrink too small so it cannot be tapped.

Strange though is the borderView can be still be seen even if it really shrunk.

If it is .fillEqually and .fillProportionally it works properly.

I am using SnapKit for arranging constraints.

Let me know if I should paste the whole code here.

Custom View

public final class CustomView: UIView {
    
    // MARK: - Properties
    
    private let borderedView: UIView = {
        let view = UIView()
        view.layer.borderWidth = 1
        return view
    }()
    
    private var errorStackView: UIStackView = {
        let sv = UIStackView()
        sv.isHidden = true
        return sv
    }()
    
    private var borderStackView: UIStackView = {
        let sv = UIStackView()
        return sv
    }()
    
    private var parentStackView: UIStackView = {
        let sv = UIStackView()
        return sv
    }()

.
.
.
.
.


// MARK: - Lifecycle
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupUI()
    }
    
    public init(button: ButtonType) {
        super.init(frame: CGRect.zero)
        setupUI()
    }
    
    public override func draw(_ rect: CGRect) {
        setNeedsDisplay()
    }

.
.
.
.
.
.

     private func setupUI() {
        backgroundColor = .clear
        borderedView.addSubview(disabledImageView)
        
        borderedView.snp.makeConstraints { make in
            make.width.height.equalTo(borderSize)
        }
        
        borderStackView = UIStackView(arrangedSubviews: [borderedView, label])
        borderStackView.spacing = 12
        borderStackView.distribution = .fillProportionally
        
        borderStackView.snp.makeConstraints { make in
            make.height.width.equalTo(borderSize)
        }

        
        errorStackView = UIStackView(arrangedSubviews: [errorIconImageView, inlineErrorLabel])
        errorStackView.spacing = 7
        errorStackView.distribution = .fillProportionally
        
        parentStackView = UIStackView(arrangedSubviews: [borderStackView, errorStackView])
        parentStackView.axis = .vertical
        parentStackView.distribution = .fillProportionally
        addSubview(parentStackView)

        parentStackView.snp.makeConstraints { make in
            make.centerY.equalTo(self.snp.centerY)
            make.width.equalToSuperview()
        }
        
        disabledImageView.snp.makeConstraints { make in
            make
                .leading
                .trailing
                .top
                .bottom
                .height
                .width
                .equalToSuperview()
        }
        
        parentStackView.sizeToFit()
        layoutIfNeeded()
    }

.
.
.
.
.
.
    // MARK: - Action
    
    public func addAction(action: @escaping ((Bool?) -> Void)) {
        borderedView.addGestureRecognizerOnView(target: self, #selector(handleTapGesture(sender:)))
        status = action
    }
}

View Controller


class ViewController: UIViewController {
    
    // MARK: - Properties
    
    private lazy var checkboxDefault: CustomView = {
        let checkbox = CustomView()
        checkbox.addAction { [weak self] checkboxStatus in
            self?.handleUnchecked()
        }
        return checkbox
    }()
    
    private lazy var checkboxEnabledAndChecked: CustomView = {
        let checkbox = CustomView()
        checkbox.addAction { [weak self] checkboxStatus in
            self?.handleChecked()
        }
        return checkbox
    }()
    
    private lazy var checkboxDisableAndUnchecked: CustomView = {
        let checkbox = CustomView()
        checkbox.addAction { [weak self] checkboxStatus in
            self?.handleDisabledAndUnchecked()
        }
        return checkbox
    }()
    
    private lazy var checkboxDisableAndChecked: CustomView = {
        let checkbox = CustomView()
        checkbox.addAction { [weak self] checkboxStatus in
            self?.handleDisabledAndChecked()
        }
        return checkbox
    }()
    
    private lazy var checkboxError: CustomView = {
        let checkbox = CustomView()
        checkbox.addAction { [weak self] checkboxStatus in
            self?.handleError()
        }
        return checkbox
    }()
    
    private lazy var checkboxMultilineError: CustomView = {
        let checkbox = CustomView()
        checkbox.addAction { [weak self] checkboxStatus in
            self?.handleMultiError()
        }
        return checkbox
    }()

.
.
.
.
.
.
 override func viewDidLoad() {

        let stackview = UIStackView(
            arrangedSubviews: [
                checkboxDefault,
                checkboxEnabledAndChecked,
                checkboxDisableAndUnchecked,
                checkboxDisableAndChecked,
                checkboxError,
                checkboxMultilineError
            ])
        
        stackview.distribution = .equalSpacing
        stackview.axis = .vertical

        view.addSubview(stackview)

        stackview.snp.makeConstraints { make in
            make.center.equalTo(view.snp.center)
            make.width.equalTo(300)
            make.height.equalTo(300)
        }
}

Solution

  • I managed to fix this by adding intrinsicContentSize in Custom View

    and adding invalidateIntrinsicContentSize() everytime the view should be updated. In my case it was in handleTapGesture

    // MARK: - Lifecycle
    
        public override var intrinsicContentSize: CGSize {
           return CGSize(width: 300, height: 50)
        }
    .
    .
    .
    .
    .
    // MARK: - Action
        
        @objc private func handleTapGesture(sender: UITapGestureRecognizer) {
            invalidateIntrinsicContentSize() // Call this when custom view needs to be updated
        }