TransWikia.com

Swift - vertical stack elements overlaying each other

Stack Overflow Asked by DevB1 on December 11, 2020

I am having difficulty getting the desired effect with UIStackView. Here is my setup:

Field element with textfield:

class NewEditableFuelSheetField: UIView {
    
    var titleText: String?
    
    var textFieldText: String?
    
    init(titleText: String, textFieldText: String) {
        
        super.init(frame: .zero)
        self.titleText = titleText
        self.textFieldText = textFieldText
        
        self.addSubview(editableField)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private lazy var editableField: UIStackView = {
        let title = UILabel()
        title.text = self.titleText
        
        let textField = UITextField()
        textField.isEnabled = false
        
        let stack = UIStackView(arrangedSubviews: [title, textField])
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        return stack
    }()
    
    private func configureAutoLayout() {
        NSLayoutConstraint.activate([
        editableField.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

Field with fixed values:

class NewFixedFuelSheetField: UIView {
    
    var title: String?
    
    var detail: String?
    
    init(title: String, detail: String) {
        
        super.init(frame: .zero)
        
        self.title = title
        
        self.detail = detail
        configureAutoLayout()
        
        self.addSubview(fixedField)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private lazy var fixedField: UIStackView = {
        
        let title = UILabel()
        let detail = UILabel()
        
        title.text = self.title
        detail.text = self.detail
        
        let stack = UIStackView(arrangedSubviews: [title, detail])
        stack.axis = .vertical
        stack.distribution = .fillEqually
        
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        self.addSubview(stack)
        
        return stack
    }()
    
    private func configureAutoLayout() {
        NSLayoutConstraint.activate([
        fixedField.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

Header view containing a stack of non editable fields:

class NewFuelSheetHeaderView: UIView {
    
    // MARK:  Init

    override init(frame: CGRect) {
        super.init(frame: .zero)
        self.addSubview(fuelSheetHeaderStack)
        configureAutoLayout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK:  Properties

    // 'detail' text will be brought in from API in next ticket
    
    private lazy var flightNumber: NewFixedFuelSheetField = {
        return NewFixedFuelSheetField(title: "Flight number", detail: "VS0101")
    }()
    
    private lazy var aircraftReg: NewFixedFuelSheetField = {
        return NewFixedFuelSheetField(title: "Aircraft reg", detail: "GAAAA")
    }()
    
    private lazy var date: NewFixedFuelSheetField = {
       return NewFixedFuelSheetField(title: "Date", detail: "01.01.21")
    }()
    
    private lazy var time: NewFixedFuelSheetField = {
       return NewFixedFuelSheetField(title: "Time", detail: "12:01")
    }()
    
    private let supplier: NewFixedFuelSheetField = {
       return NewFixedFuelSheetField(title: "Supplier", detail: "i6Staging, BAPCO")
    }()
    
    private let fuelGrade: NewFixedFuelSheetField = {
       return NewFixedFuelSheetField(title: "Fuel grade", detail: "Jet A")
    }()
    
    private let freezePoint: NewFixedFuelSheetField = {
       return NewFixedFuelSheetField(title: "Freeze point", detail: "-40")
    }()
    
    private let specificGravity: NewFixedFuelSheetField = {
       return NewFixedFuelSheetField(title: "Specific gravity", detail: "0.793")
    }()
    
    private lazy var fuelSheetHeaderFirstRow: UIStackView = {
        let stack = UIStackView(arrangedSubviews: [
            flightNumber,
            aircraftReg,
            date,
            time
        ])
        
        stack.axis = .horizontal
        stack.distribution = .fillEqually
        return stack
    }()
    
    private lazy var fuelSheetHeaderSecondRow: UIStackView = {
        let stack = UIStackView(arrangedSubviews: [
            supplier,
            fuelGrade,
            freezePoint,
            specificGravity
        ])
        
        stack.axis = .horizontal
        stack.distribution = .fillEqually
        return stack
    }()
    
    private lazy var fuelSheetHeaderStack: UIStackView = {
       let stack = UIStackView(arrangedSubviews: [fuelSheetHeaderFirstRow, fuelSheetHeaderSecondRow])
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()
    
    // MARK:  Configuration
    
    private func configureAutoLayout() {
        NSLayoutConstraint.activate([
            fuelSheetHeaderStack.topAnchor.constraint(equalTo: self.topAnchor, constant: 50),
            fuelSheetHeaderStack.leftAnchor.constraint(equalTo: leftAnchor),
            fuelSheetHeaderStack.rightAnchor.constraint(equalTo: rightAnchor),
            fuelSheetHeaderStack.heightAnchor.constraint(equalToConstant: 200)
        ])
    }
}

Second view which ultimately needs to be placed beneath the header:

class NewFuelSheetRefuelInfoView: UIView {

    // MARK:  Init
    
    override init(frame: CGRect) {
        super.init(frame: .zero)
        self.addSubview(refuelStackView)
        configureAutoLayout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK:  Properties
    
    private lazy var preRefuel: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "A. Pre-refuel FOB", textFieldText: "")
    }()
    
    private lazy var requiredDepartureFuel: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "B. Required departure fuel", textFieldText: "")
    }()
    
    private lazy var requiredUplift: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "C. Required uplift (B - A)", textFieldText: "")
    }()
    
    private lazy var actualUplift: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "D. Actual uplift", textFieldText: "")
    }()
    
    private lazy var actualDepartureFuel: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "E. Actual departure fuel", textFieldText: "")
    }()
    
    private lazy var refuelStackView: UIStackView = {
        let stack = UIStackView(arrangedSubviews: [
            preRefuel,
            requiredDepartureFuel,
            requiredUplift,
            actualUplift,
            actualDepartureFuel
        ])
        
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()
    
    // MARK:  Config

    private func configureAutoLayout() {
        NSLayoutConstraint.activate([
            refuelStackView.topAnchor.constraint(equalTo: topAnchor, constant: 50),
            refuelStackView.leftAnchor.constraint(equalTo: leftAnchor),
            refuelStackView.rightAnchor.constraint(equalTo: rightAnchor),
            refuelStackView.heightAnchor.constraint(equalToConstant: 300)
        ])
    }
}

Then I have a main view to bring these elements together:

class NewFuelSheetMainView: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: .zero)
        self.addSubview(mainStack)
        configureAutoLayout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private lazy var flightDetailsHeader: NewFuelSheetHeaderView = {
       return NewFuelSheetHeaderView()
    }()
    
    private lazy var refuelView: NewFuelSheetRefuelInfoView = {
        return NewFuelSheetRefuelInfoView()
    }()
    
    private lazy var mainStack: UIStackView = {
        let stack = UIStackView(arrangedSubviews: [flightDetailsHeader, refuelView])
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()

    private func configureAutoLayout() {
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: topAnchor, constant: 30),
            mainStack.leftAnchor.constraint(equalTo: leftAnchor),
            mainStack.rightAnchor.constraint(equalTo: rightAnchor),
            mainStack.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
}

And finally a VC to display the main view:

class DataEntryViewController: I6ViewController {
    
    // MARK:  Init

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nil, bundle: nil)
        configureAutoLayout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK:  Lifecycle

    override func viewDidLoad() {
        view.backgroundColor = .white
    }
    
    // MARK:  Properties

    private lazy var mainView: NewFuelSheetMainView = {
        let mainView = NewFuelSheetMainView()
        mainView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(mainView)
        return mainView
    }()
    
    // MARK:  Configuration
    
    private func configureAutoLayout() {
        NSLayoutConstraint.activate([
            mainView.leftAnchor.constraint(equalTo: view.leftAnchor),
            mainView.rightAnchor.constraint(equalTo: view.rightAnchor),
        ])
    } 
}

In my mind, (and clearly my logic is flawed as it’s not working!!) the key part here is in the main view where I present the stack of the two smaller views. Here I am clearly setting the stack as .vertical and I’m pinning this vertical stack to the top and bottom of the main view. However, rather than the second view appearing beneath the first which is what I would have expected, they are simply appearing one over the top of the other:

enter image description here

Clearly I’m missing a key point here but I can’t see where. Any help would be greatly appreciated.

2 Answers

As you could guess it's to do with your constraints. This is how your views look currently. UIstackViews are self-sizing to their content. UIViews need to be given height and width information.

enter image description here

This is after I made adjustments.

enter image description here

DataEntryViewController
mainView.topAnchor.constraint(equalTo: view.topAnchor),
mainView.leftAnchor.constraint(equalTo: view.leftAnchor),
mainView.rightAnchor.constraint(equalTo: view.rightAnchor),
mainView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

NewFuelSheetMainView
mainStack.topAnchor.constraint(equalTo: view.topAnchor),
mainStack.leftAnchor.constraint(equalTo: view.leftAnchor),
mainStack.rightAnchor.constraint(equalTo: view.rightAnchor),
mainStack.bottomAnchor.constraint(equalTo: view.bottomAnchor),

NewFuelSheetRefuelInfoView
refuelStackView.leftAnchor.constraint(equalTo: leftAnchor),
refuelStackView.rightAnchor.constraint(equalTo: rightAnchor),
refuelStackView.heightAnchor.constraint(equalToConstant: 300)

NewFuelSheetHeaderView
fuelSheetHeaderStack.leftAnchor.constraint(equalTo: leftAnchor),
fuelSheetHeaderStack.rightAnchor.constraint(equalTo: rightAnchor),
fuelSheetHeaderStack.heightAnchor.constraint(equalToConstant: 200)

Correct answer by Waylan Sands on December 11, 2020

A couple tips during development:

  • give your UI elements contrasting background colors to make it easy to see frames at run-time
  • work on one element at a time
  • set self.clipsToBounds = true for all UIView sub-classes - if their subviews are not visible, you know you have constraint problems

For example, let's start with your NewEditableFuelSheetField...

Create a development / scratch view controller:

class ScratchViewController: UIViewController {
    
    override func viewDidLoad() {
        view.backgroundColor = UIColor(red: 0.5, green: 0.75, blue: 1.0, alpha: 1.0)
        
        let v = NewEditableFuelSheetField(titleText: "A. Pre-refuel FOB", textFieldText: "")
        view.addSubview(v)
        v.translatesAutoresizingMaskIntoConstraints = false
        
        // respect safe-area
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            v.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
        ])
        
    }

}

With NO changes to your NewEditableFuelSheetField class, this is the output:

enter image description here

The text field is there, but we don't know that from looking at the output. So, let's make a couple changes to your class:

class NewEditableFuelSheetField: UIView {
    
    var titleText: String?
    
    var textFieldText: String?
    
    init(titleText: String, textFieldText: String) {
        
        super.init(frame: .zero)
        self.titleText = titleText
        self.textFieldText = textFieldText
        
        self.addSubview(editableField)
        
        // this was missing from the code in your question
        configureAutoLayout()

        // so we can see the frame at run-time
        self.backgroundColor = .red
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private lazy var editableField: UIStackView = {
        let title = UILabel()
        title.text = self.titleText
        
        let textField = UITextField()
        textField.isEnabled = false
        
        // so we can see the frames at run-time
        title.backgroundColor = .yellow
        textField.backgroundColor = .green
        //
    
        let stack = UIStackView(arrangedSubviews: [title, textField])
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        return stack
    }()
    
    private func configureAutoLayout() {
        NSLayoutConstraint.activate([
            editableField.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

enter image description here

OK... now we see the frames for the label and field... but we constrained the view leading/trailing with 20-pts on each side. So, why don't we see the red view background?

Let's add clipsToBounds in init:

    self.addSubview(editableField)
    
    // this was missing from the code in your question
    configureAutoLayout()
    
    // so we can see the frames at run-time
    self.backgroundColor = .red
    
    // set clipsToBounds
    self.clipsToBounds = true

the new output:

enter image description here

Hmm... obviously not what we want. If we use Debug View Hierarchy we can see that the instance of NewEditableFuelSheetField has a height and width of Zero, and its contents were showing "out-of-bounds."

You've added a label and a field to a vertical stack view, added that stack view to self, and set its height to 50... but you didn't give the stack view any constraints relative to its superview.

Let's fix that:

private func configureAutoLayout() {
    NSLayoutConstraint.activate([
        editableField.heightAnchor.constraint(equalToConstant: 50),
        
        // constraints relative to superview (self)
        editableField.topAnchor.constraint(equalTo: topAnchor),
        editableField.leadingAnchor.constraint(equalTo: leadingAnchor),
        editableField.bottomAnchor.constraint(equalTo: bottomAnchor),
    ])
}

enter image description here

Woo Hoo! Looks like we've made some progress.

Now let's add 5 NewEditableFuelSheetField instances to a vertical stack view (with spacing of 8 to make it clear):

class ScratchViewController: UIViewController {
    
    private lazy var preRefuel: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "A. Pre-refuel FOB", textFieldText: "")
    }()
    
    private lazy var requiredDepartureFuel: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "B. Required departure fuel", textFieldText: "")
    }()
    
    private lazy var requiredUplift: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "C. Required uplift (B - A)", textFieldText: "")
    }()
    
    private lazy var actualUplift: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "D. Actual uplift", textFieldText: "")
    }()
    
    private lazy var actualDepartureFuel: NewEditableFuelSheetField = {
        return NewEditableFuelSheetField(titleText: "E. Actual departure fuel", textFieldText: "")
    }()
    
    private lazy var refuelStackView: UIStackView = {
        let stack = UIStackView(arrangedSubviews: [
            preRefuel,
            requiredDepartureFuel,
            requiredUplift,
            actualUplift,
            actualDepartureFuel
        ])
        
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()
    
    override func viewDidLoad() {
        view.backgroundColor = UIColor(red: 0.5, green: 0.75, blue: 1.0, alpha: 1.0)
        
        view.addSubview(refuelStackView)
        
        // for visual example
        refuelStackView.spacing = 8
        
        // respect safe-area
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            refuelStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            refuelStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            refuelStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
        ])
        
    }

}

Result (I added a dark-blue dashed-outline to show the frame of the stack view):

enter image description here

If you follow that development process with each of your UIView subclasses (starting with the most "inside" views), you should be on your way.


As a side note: be careful when giving size (height or width) constraints to views you are adding to a stack view (vertical / horizontal respectively), and then ALSO giving the stack view .distribution = .fillEqually AND its own height / width constraint. You can end up saying:

make each of 5 arranged subviews 50-pts in height
make the stack view 300-pts in height
fill equally

and you get 5 * 50 = 250 ... which will conflict with stack view height = 300

Answered by DonMag on December 11, 2020

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP