Abnormality when drawing a view inside of a stack view

Viewed 49

Exploring stackviews I've ran into a problem of incorrect representation if views inside of it. So, to make a long story short... I've made a custom checkbox:

class CheckBox: UIView, CheckBoxProtocol {

required init(frame: CGRect, color: UIColor) {
    super.init(frame: frame)
    
    self.layer.borderWidth = 5
    self.layer.borderColor = color.cgColor
    self.addSubview(checkmark)
    checkmark.tintColor = color
    
    
    let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
    self.addGestureRecognizer(gesture)
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

var isChecked = true

lazy var checkmark: UIImageView = {
    let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height))
    
    imageView.isHidden = false
    imageView.contentMode = .scaleAspectFit
    imageView.image = UIImage(systemName: "checkmark")
    
    return imageView
}()

@objc func toggle() {
    self.isChecked.toggle()
    self.checkmark.isHidden = !self.isChecked
}

In the Controller, when I add this view to the subviews it looks fairly normal and works as it should work (check-uncheck)

However when I add checkbox to the stackview it looses its visible frame and its functionality (does not check-uncheck) - you can see it on the screenshot

screenshot

Here is the code from the ViewController:

class SettingsViewController: UIViewController {

override func loadView() {
    super.loadView()
    self.view.backgroundColor = .white
    
    self.view.addSubview(stackView)
}

override func viewDidLoad() {
    super.viewDidLoad()
    
}

lazy var stackView: UIStackView = {
    let stackView = UIStackView(frame: CGRect(x: 150, y: 150, width: 0, height: 0))
    
    stackView.axis = .horizontal
    stackView.spacing = 50
    stackView.alignment = .fill
    stackView.distribution = .fillEqually
    [redCheckbox,
     greenCheckbox,
     blackCheckbox,
     greyCheckbox,
     brownCheckbox,
     yellowCheckbox,
     purpleCheckbox,
     orangeCheckbox].forEach {stackView.addArrangedSubview($0)} 
    
    return stackView
}()

private let frame = CGRect(x: 0, y: 0, width: 30, height: 30)

lazy var redCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.red)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()

lazy var greenCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.green)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()

lazy var blackCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.black)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()

lazy var greyCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.grey)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()

lazy var brownCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.brown)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()

lazy var yellowCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.yellow)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()

lazy var purpleCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.purple)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()

lazy var orangeCheckbox: CheckBox = {
    let colorFactory = CardViewFactory()
    let color = colorFactory.getViewColor(modelColor: CardColor.orange)
    let checkbox = CheckBox(frame: frame, color: color)
    
    return checkbox
}()
2 Answers

It's because we're working with the lazy property and its life cycle can be a little different. Let's set constraints after the view has loaded. What I would suggest to do:

  1. For each checkbox, change the frame to zero:

     lazy var orangeCheckbox: CheckBox = {
        let colorFactory = CardViewFactory()
        let color = colorFactory.getViewColor(modelColor: CardColor.orange)
        let checkbox = CheckBox(frame: .zero, color: colorFactory)
        checkbox.translatesAutoresizingMaskIntoConstraints = false
        return checkbox
     }()
    
  2. Do the same to the stackView:

     lazy var stackView: UIStackView = {
        let stackView = UIStackView(frame: .zero)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .horizontal
        stackView.spacing = 5
        stackView.alignment = .fill
        stackView.distribution = .fillEqually
        [redCheckbox,
         greenCheckbox,
         blackCheckbox,
         greyCheckbox,
         brownCheckbox,
         yellowCheckbox,
         purpleCheckbox,
         orangeCheckbox].forEach {stackView.addArrangedSubview($0)}
    
        return stackView
     }()
    
  3. Add some constraints on viewDidLoad:

     override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(stackView)
    
        stackView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor, constant: -100).isActive = true
        stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
        stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true
        stackView.heightAnchor.constraint(equalToConstant: 40).isActive = true
    
        [redCheckbox,
         greenCheckbox,
         blackCheckbox,
         greyCheckbox,
         brownCheckbox,
         yellowCheckbox,
         purpleCheckbox,
         orangeCheckbox].forEach {
            $0.heightAnchor.constraint(equalToConstant: 30).isActive = true
            $0.widthAnchor.constraint(equalToConstant: 30).isActive = true
        }
     }
    

What you can do to the image inside the checkBox to work fine:

  1. translatesAutoresizingMaskIntoConstraints = false

     lazy var checkmark: UIImageView = {
        let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.isHidden = false
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(systemName: "checkmark")
        return imageView
     }()
    
  2. on your required init:

     required init(frame: CGRect, color: UIColor) {
        super.init(frame: frame)
    
        self.layer.borderWidth = 5
        self.layer.borderColor = color.cgColor
        self.addSubview(checkmark)
        checkmark.tintColor = color
    
        checkmark.topAnchor.constraint(equalTo: topAnchor).isActive = true
        checkmark.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        checkmark.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        checkmark.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    
        let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
        self.addGestureRecognizer(gesture)
        setNeedsDisplay()
     }
    

To offer some additional info...

You can save yourself a lot of duplicate coding.

Take a look at this...

First, slight modifications to your Checkbox class:

class CheckBox: UIView {//, CheckBoxProtocol {
    
    var checkmark: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(systemName: "checkmark")
        return imageView
    }()
    
    var isChecked = true {
        didSet {
            checkmark.isHidden = !isChecked
        }
    }
    
    required init(frame: CGRect, color: UIColor) {
        super.init(frame: frame)
        commonInit(color)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit(.white)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit(.white)
    }
    
    func commonInit(_ color: UIColor) {
        
        self.layer.borderWidth = 5
        self.layer.borderColor = color.cgColor

        self.addSubview(checkmark)
        checkmark.tintColor = color
        
        checkmark.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            // constrain image view to all 4 sides
            checkmark.topAnchor.constraint(equalTo: topAnchor),
            checkmark.leadingAnchor.constraint(equalTo: leadingAnchor),
            checkmark.trailingAnchor.constraint(equalTo: trailingAnchor),
            checkmark.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
        
        let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
        self.addGestureRecognizer(gesture)
    }
    
    @objc func toggle() {
        self.isChecked.toggle()
    }

}

We've used auto-layout to keep the image view the same size as the view itself.

And, by implementing the var isChecked block we have a more "automated" way of setting the image view's hidden state.

In addition, we can now get the "state" of the checkbox in the controller like this:

if thisCheckBox.isChecked {
    // do something
}

Now the view controller... we'll define an array of colors, loop through them to create the CheckBox objects, and add them to the stack view:

class SettingsViewController: UIViewController {
    
    var stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .horizontal
        // desired spacing
        stackView.spacing = 12
        stackView.alignment = .fill
        stackView.distribution = .fillEqually
        return stackView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .white
        
        self.view.addSubview(stackView)
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            // stack view Top constraint
            stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 150.0),
            // centered horizontally
            stackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            // explicit Height
            stackView.heightAnchor.constraint(equalToConstant: 30.0),
        ])
    
        let colors: [UIColor] = [
            .red, .green, .black, .gray,
            .brown, .yellow, .purple, .orange,
        ]

        // loop through the colors, creating a new
        //  CheckBox object for each color
        //  and add it to the stack view
        colors.forEach { c in
            let checkbox = CheckBox(frame: .zero, color: c)
            // 1:1 aspect ratio
            checkbox.widthAnchor.constraint(equalTo: checkbox.heightAnchor).isActive = true
            stackView.addArrangedSubview(checkbox)
        }
        
    }
    
}

As you can see, we've eliminated the need for all of the individual

lazy var redCheckbox: CheckBox = { ...
lazy var greenCheckbox: CheckBox = { ...
// etc

code blocks.

Related