Trying to make sense of UILabel behavior.

105 Views Asked by At

I am trying to replicate Apple's calculator UI layout.

Here is a gif of what I have so far.

The problems that I am encountering mostly have to do with the UILables. As seen in the gif above, I am experiencing the following problems:

  • On device rotation, the labels "L1" and "L2" pop, instead of transitioning smoothly.

  • The labels on the brown colored buttons disappear when transitioning back to portrait.

For the labels "L1" and "L2" I have tried experimenting with the content mode and constraints, however, I still get clunky transitions.

As for the disappearing labels, instead of hiding/unhiding the stack view to make the layout appear and disappear via it's is hidden property, I instead tried using constraints on the stack view to handle the transition, however, the results remain the same.

I have also looked online and tried some suggestions, however, most answers were outdated or simply did not work.

The code is very straight forward, it primarily consists of setting up the views and its constraints.

extension UIStackView {
    convenience init(axis: UILayoutConstraintAxis, distribution: UIStackViewDistribution = .fill) {
        self.init()
        self.axis = axis
        self.distribution = distribution
        self.translatesAutoresizingMaskIntoConstraints = false
    }
}

class Example: UIView {
    let mainStackView = UIStackView(axis: .vertical, distribution: .fill)
    let subStackView = UIStackView(axis: .horizontal, distribution: .fillProportionally)
    let portraitStackView = UIStackView(axis: .vertical, distribution: .fillEqually)
    let landscapeStackView = UIStackView(axis: .vertical, distribution: .fillEqually)

    var containerView: UIView = {
        $0.backgroundColor = .darkGray
        $0.translatesAutoresizingMaskIntoConstraints = false
        return $0
    }(UIView(frame: .zero))

    let mainView: UIView = {
        $0.backgroundColor = .blue
        $0.translatesAutoresizingMaskIntoConstraints = false
        return $0
    }(UIView(frame: .zero))

    let labelView: UIView = {
        $0.backgroundColor = .red
        $0.translatesAutoresizingMaskIntoConstraints = false
        return $0
    }(UIView(frame: .zero))

    var labelOne: UILabel!
    var labelTwo: UILabel!

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .red
        autoresizingMask = [.flexibleWidth, .flexibleHeight]

        labelOne = createLabel(text: "L1")
        labelOne.translatesAutoresizingMaskIntoConstraints = false
        labelOne.backgroundColor = .darkGray
        labelTwo = createLabel(text: "L2")
        labelTwo.translatesAutoresizingMaskIntoConstraints = false
        labelTwo.backgroundColor = .black

        landscapeStackView.isHidden = true

        mainView.addSubview(labelView)
        labelView.addSubview(labelOne)
        labelView.addSubview(labelTwo)
        addSubview(mainStackView)
        mainStackView.addArrangedSubview(mainView)

        setButtonStackView()
        setStackViewConstriants()
        setDisplayViewConstriants()
    }

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

    func setStackViewConstriants() {
        mainStackView.translatesAutoresizingMaskIntoConstraints = false
        mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    }

    func setDisplayViewConstriants() {
        mainView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 288/667).isActive = true

        labelView.heightAnchor.constraint(equalTo: mainView.heightAnchor, multiplier: 128/288).isActive = true
        labelView.centerYAnchor.constraint(equalTo: mainView.centerYAnchor).isActive = true
        labelView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 24).isActive = true
        labelView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor, constant: -24).isActive = true

        labelOne.heightAnchor.constraint(equalTo: labelTwo.heightAnchor, multiplier: 88/32).isActive = true
        labelOne.trailingAnchor.constraint(equalTo: labelView.trailingAnchor).isActive = true
        labelOne.leadingAnchor.constraint(equalTo: labelView.leadingAnchor).isActive = true
        labelOne.topAnchor.constraint(equalTo: labelView.topAnchor).isActive = true

        labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor).isActive = true
        labelTwo.trailingAnchor.constraint(equalTo: labelView.trailingAnchor).isActive = true
        labelTwo.leadingAnchor.constraint(equalTo: labelOne.leadingAnchor).isActive = true
        labelTwo.bottomAnchor.constraint(equalTo: labelView.bottomAnchor).isActive = true
    }

    func createLabel(text: String) -> UILabel {
        let label = UILabel(frame: .zero)
        label.text = text
        label.font = UIFont.init(name: "Arial-BoldMT", size: 60)
        label.textColor = .white
        label.textAlignment = .right
        label.contentMode = .right
        label.minimumScaleFactor = 0.1
        label.adjustsFontSizeToFitWidth = true
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }

    func createButton(text: String) -> UIButton {
        let button = UIButton(type: .custom)
        button.setTitle(text, for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.layer.borderColor = UIColor.white.cgColor
        button.layer.borderWidth = 1
        button.titleLabel?.font = UIFont.init(name: "Arial-BoldMT", size: 60)
        button.titleLabel?.minimumScaleFactor = 0.1
        button.titleLabel?.adjustsFontSizeToFitWidth = true
        button.titleLabel?.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.leadingAnchor.constraint(equalTo: button.leadingAnchor).isActive = true
        button.titleLabel?.trailingAnchor.constraint(equalTo: button.trailingAnchor).isActive = true
        button.titleLabel?.topAnchor.constraint(equalTo: button.topAnchor).isActive = true
        button.titleLabel?.bottomAnchor.constraint(equalTo: button.bottomAnchor).isActive = true
        button.titleLabel?.textAlignment = .center
        button.titleLabel?.contentMode = .scaleAspectFill
        button.titleLabel?.numberOfLines = 0
        return button
    }

    func setButtonStackView() {
        for _ in 1...5 {
            let stackView = UIStackView(axis: .horizontal, distribution: .fillEqually)
            for _ in 1...4 {
                let button = createButton(text: "0")
                button.backgroundColor = .brown
                stackView.addArrangedSubview(button)
            }
            landscapeStackView.addArrangedSubview(stackView)
        }

        for _ in 1...5 {
            let stackView = UIStackView(axis: .horizontal, distribution: .fillEqually)
            for _ in 1...4 {
                let button = createButton(text: "0")
                button.backgroundColor = .purple
                stackView.addArrangedSubview(button)
            }
            portraitStackView.addArrangedSubview(stackView)
        }

        subStackView.addArrangedSubview(landscapeStackView)
        subStackView.addArrangedSubview(portraitStackView)
        mainStackView.addArrangedSubview(subStackView)
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        if UIDevice.current.orientation.isLandscape && landscapeStackView.isHidden == true {
            self.landscapeStackView.isHidden = false
        }
        if UIDevice.current.orientation.isPortrait && landscapeStackView.isHidden == false {
            self.landscapeStackView.isHidden = true
        }
        self.layoutIfNeeded()
    }
}
1

There are 1 best solutions below

1
On

Overview:

  • Do things incrementally with separate components / view controllers (easier to debug)
  • The below solution is only for labels L1 and L2.
  • For the calculator buttons, it would be best to use a UICollectionViewController. (I haven't implemented it, add as a child view controller)

Code:

private func setupLabels() {
    
    view.backgroundColor = .red
    
    let stackView           = UIStackView()
    stackView.axis          = .vertical
    stackView.alignment     = .fill
    stackView.distribution  = .fill
    
    stackView.translatesAutoresizingMaskIntoConstraints = false
    
    view.addSubview(stackView)
    
    stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
    
    
    let label1 = UILabel()
    label1.text             = "L1"
    label1.textColor        = .white
    label1.backgroundColor  = .darkGray
    label1.textAlignment    = .right
    label1.font = UIFont.preferredFont(forTextStyle: .title1)
    
    
    let label2 = UILabel()
    label2.text             = "L2"
    label2.textColor        = .white
    label2.backgroundColor  = .black
    label2.textAlignment    = .right
    label2.font = UIFont.preferredFont(forTextStyle: .caption1)
    
    stackView.addArrangedSubview(label1)
    stackView.addArrangedSubview(label2)
}