Home > Software engineering >  Abnormality when drawing a view inside of a stack view
Abnormality when drawing a view inside of a stack view

Time:09-08

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

CodePudding user response:

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