Search code examples
swiftuiviewuistackview

Abnormality when drawing a view inside of a stack view


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
}()

Solution

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