简体   繁体   中英

Swift - vertical stack elements overlaying each other

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:

在此处输入图像描述

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

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.

在此处输入图像描述

This is after I made adjustments.

在此处输入图像描述

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)

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:

在此处输入图像描述

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

在此处输入图像描述

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:

在此处输入图像描述

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),
    ])
}

在此处输入图像描述

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):

在此处输入图像描述

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM