1

Dear StackOverflowCommunity,

im currently working on a project, where I need to make a fully dynamic user interface. For that, im building all of it programatically to make it as easy as possible. I've now come to a problem which is as soon as I wrap my contentView(UIStackView) with an UIScrollView to make it scrollable, the scrollView is in front of all the other UIElements so that I can only scroll. I can not interact with Buttons, Sliders, Switches or anything, no event will get triggered.

I've literally done anything I could think of (working on that Problem for DAYS) but couldn't find any suiting answer whether on google directly nor on stack overflow or apple forums.

I'm pretty sure its a pretty small change that I just wasn't capable of thinking of. I really appreciate any of your help.

Structure is like this: ViewController > UIScrollView > UIStackView > Item Wrappers (for example contains a UISwitch and a Describing Label) > Single UIElement

On User Interaction (for example choosing a different mode) the wrappers get removed and/or added to the view as the user needs. I did only post the code that is somehow relevant for this problem (in my opinion). If you need any further information feel free to ask.

I maybe need to add: As soon as I remove the UIScrollView and just add the StackView (named contentView in code) to the main view it all works just fine, I just can't scroll it which is a big problem as soon as I have more than like 5 element wrappers attached to the view.

    var wrappers : Dictionary<String, UIView> = [:]
var elements : Dictionary<String, [UIView]> = [:]
var constraints : Dictionary = [String: [[NSLayoutConstraint]]]()
let contentView = UIStackView()

let states = [
    "state_operating_hours",
    "state_dim",
    "state_brightness_sensor",
    "state_operating_voltage",
    "state_circuit_voltage",
    "state_load_current_led",
    "state_output_power",
    "state_temperature",
    "state_error",
    "state_sw_version",
    "state_hw_version"
]

let checkboxes = [
    "summertime_wintertime",
    "dali",
    "error_output"
]

let sliders = [
    "immediate_sensitivity",
    "immediate_dawn",
    "immediate_dim",
    "immediate_day",
    "immediate_dusk",
    "immediate_night",
    "aging"
]

let textInputs = [
    "module_name",
    "switch_delay"
]

let dropdowns = [
    "mode",
    "bt_state",
    "config_output"
]

let timePickers = [
    "phase_0",
    "phase_1",
    "phase_2",
    "phase_3"
]

let buttons = [
    "state_trigger",
    "reset_trigger",
]

let l_kind_criteria = [
    "immediate_dawn",
    "immediate_day",
    "immediate_dusk",
    "immediate_night",
    "immediate_sensitivity"
]

let d_kind_criteria = [
    "immediate_dim"
]

let t_kind_criteria = [
    "phase_0",
    "phase_1",
    "phase_2",
    "phase_3"
]

let m_kind_criteria = [
    "immediate_dawn",
    "immediate_day",
    "immediate_dusk",
    "immediate_night",
    "immediate_sensitivity",
    "phase_0",
    "phase_1"
]

let user_criteria = [
    //"access",
    //"state_trigger",
    //"reset_trigger",
    "mode",
    "summertime_wintertime"
]

let service_criteria = [
    "module_name",
    //"access",
    "state_trigger",
    "reset_trigger",
    "mode",
    "bt_state",
    "config_output",
    "aging",
    "switch_delay",
    "summertime_wintertime",
    "error_output",
    "dali"
]

override func viewDidLoad() {
    bleService.delegate = self
    bleService.requestAuthMode()
    view.backgroundColor = .lightGray
    bleService.send(aText: "c28r:#")
    bleService.send(aText: "c05r:#")
    Toast.show(message: "Statuswerte werden abgerufen..." , controller: self)
    buildLayout()
}

// Class - Functions

func buildLayout() {
    // Building the Basic Layout

    let topView = UIView()
    topView.backgroundColor = .purple
    self.view.addSubview(topView)
    topView.translatesAutoresizingMaskIntoConstraints = false

    let logoImageView = UIImageView(image: UIImage(named: "placeholder"))
    logoImageView.translatesAutoresizingMaskIntoConstraints = false
    logoImageView.frame = CGRect(x: 0, y: 0, width: view.frame.width/1.8, height: 30)
    topView.addSubview(logoImageView)

    logoImageView.leftAnchor.constraint(greaterThanOrEqualTo: view.leftAnchor, constant: 20).isActive = true
    logoImageView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 30).isActive = true

    topView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    topView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    topView.heightAnchor.constraint(equalToConstant: view.frame.height/3).isActive = true
    topView.centerYAnchor.constraint(equalTo: view.topAnchor).isActive = true
    topView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true

    //Generate and add Scroll View to Main Window

    let scrollView = UIScrollView()
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(scrollView)

    NSLayoutConstraint.activate([
        scrollView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: 20),
        scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
    ])

    //Add Content Stack to Scroll View

    contentView.axis = .vertical
    contentView.alignment = .fill
    contentView.spacing = 150
    contentView.distribution = .fill
    contentView.backgroundColor = .blue
    contentView.translatesAutoresizingMaskIntoConstraints = false
    scrollView.addSubview(contentView)

    NSLayoutConstraint.activate([
        contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
        contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 20),
        contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -20),
        contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
        contentView.widthAnchor.constraint(equalToConstant: scrollView.frame.width),
        contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
    ])

    // programmatically creating layout elements without constraints
    // Elements that change a value are always last in their respective array

    for (index, dropdownName) in dropdowns.enumerated() {
        constraints[dropdownName] = [[]]
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = dropdownName

        let leadAnch = label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        constraints[dropdownName]!.append([leadAnch])

        let textField = UITextField()
        textField.delegate = self
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.backgroundColor = .white
        textField.layer.cornerRadius = 5

        var trailAnch = textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        var widthAnch = textField.widthAnchor.constraint(equalToConstant: view.frame.width / 6)
        constraints[dropdownName]!.append([trailAnch, widthAnch])

        let pickerView = UIPickerView()
        pickerView.backgroundColor = .white
        pickerView.translatesAutoresizingMaskIntoConstraints = false
        pickerView.delegate = self
        pickerView.isHidden = true
        pickerView.dataSource = self

        trailAnch = pickerView.trailingAnchor.constraint(equalTo: textField.trailingAnchor)
        widthAnch = pickerView.widthAnchor.constraint(equalTo: textField.widthAnchor)
        constraints[dropdownName]!.append([trailAnch, widthAnch])

        let dropdownWrapper = UIView()
        dropdownWrapper.translatesAutoresizingMaskIntoConstraints = false

        dropdownWrapper.addSubview(label)
        dropdownWrapper.addSubview(textField)
        dropdownWrapper.addSubview(pickerView)

        wrappers[dropdownName] = dropdownWrapper
        elements[dropdownName] = [label, textField, pickerView]

        let commandID = bleService.getCommand(commandName: dropdownName)
        bleService.send(aText: "c\(commandID)r:#")
    }

    for (index, sliderName) in sliders.enumerated() {
        constraints[sliderName] = [[]]
        let descLabel = UILabel()
        descLabel.translatesAutoresizingMaskIntoConstraints = false
        descLabel.text = sliderName

        var leadAnch = descLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        constraints[sliderName]!.append([leadAnch])

        let valueLabel = UILabel()
        valueLabel.translatesAutoresizingMaskIntoConstraints = false
        valueLabel.text = "0"
        valueLabel.backgroundColor = .white

        let widthAnch = valueLabel.widthAnchor.constraint(equalToConstant: view.frame.width/6)
        var trailAnch = valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        constraints[sliderName]!.append([trailAnch, widthAnch])

        let slider = UISlider()
        slider.translatesAutoresizingMaskIntoConstraints = false
        slider.isContinuous = false
        slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)

        leadAnch = slider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        trailAnch = slider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        let topAnch = slider.topAnchor.constraint(equalTo: descLabel.bottomAnchor, constant: 5)
        constraints[sliderName]!.append([trailAnch, leadAnch, topAnch])

        let sliderWrapper = UIView()
        sliderWrapper.translatesAutoresizingMaskIntoConstraints = false

        sliderWrapper.addSubview(descLabel)
        sliderWrapper.addSubview(valueLabel)
        sliderWrapper.addSubview(slider)

        wrappers[sliderName] = sliderWrapper
        elements[sliderName] = [descLabel, valueLabel, slider]

        let commandID = bleService.getCommand(commandName: sliderName)
        bleService.send(aText: "c\(commandID)r:#")
    }

    for (index, checkboxName) in checkboxes.enumerated() {
        constraints[checkboxName] = [[]]
        let cbLabel = UILabel()
        cbLabel.translatesAutoresizingMaskIntoConstraints = false
        cbLabel.text = checkboxName

        let leadAnch = cbLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        constraints[checkboxName]!.append([leadAnch])

        let checkbox = UISwitch()
        checkbox.translatesAutoresizingMaskIntoConstraints = false
        checkbox.addTarget(self, action: #selector(checkboxClicked), for: .valueChanged)

        let trailAnch = checkbox.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        constraints[checkboxName]!.append([trailAnch])

        let checkboxWrapper = UIView()
        checkboxWrapper.translatesAutoresizingMaskIntoConstraints = false

        checkboxWrapper.addSubview(cbLabel)
        checkboxWrapper.addSubview(checkbox)

        wrappers[checkboxName] = checkboxWrapper
        elements[checkboxName] = [cbLabel, checkbox]

        let commandID = bleService.getCommand(commandName: checkboxName)
        bleService.send(aText: "c\(commandID)r:#")
    }

    for (index, textInputName) in textInputs.enumerated() {
        constraints[textInputName] = [[]]
        let textLabel = UILabel()
        textLabel.translatesAutoresizingMaskIntoConstraints = false
        textLabel.text = textInputName

        var leadAnch = textLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        constraints[textInputName]!.append([leadAnch])

        let inputField = UITextField()
        inputField.layer.cornerRadius = 5
        inputField.translatesAutoresizingMaskIntoConstraints = false
        inputField.placeholder = textInputs[index]
        inputField.backgroundColor = .white
        inputField.addTarget(self, action: #selector(textfieldChanged), for: .valueChanged)

        let topAnch = inputField.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 5)
        let widthAnch = inputField.widthAnchor.constraint(equalToConstant: view.safeAreaLayoutGuide.layoutFrame.width/1.1)
        leadAnch = inputField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        constraints[textInputName]!.append([topAnch, widthAnch, leadAnch])

        let inputWrapper = UIView()
        inputWrapper.translatesAutoresizingMaskIntoConstraints = false

        inputWrapper.addSubview(textLabel)
        inputWrapper.addSubview(inputField)

        wrappers[textInputName] = inputWrapper
        elements[textInputName] = [textLabel, inputField]

        let commandID = bleService.getCommand(commandName: textInputName)
        bleService.send(aText: "c\(commandID)r:#")
    }

    for(index, phase) in timePickers.enumerated() {
        constraints[phase] = [[]]
        let descLabel = UILabel()
        descLabel.translatesAutoresizingMaskIntoConstraints = false
        descLabel.text = "Zeitschaltung \(index+1)"

        var leadAnch = descLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        constraints[phase]!.append([leadAnch])

        let enabledSwitch = UISwitch()
        enabledSwitch.translatesAutoresizingMaskIntoConstraints = false
        enabledSwitch.addTarget(self, action: #selector(changeTimerState), for: .valueChanged)

        var topAnch = enabledSwitch.topAnchor.constraint(equalTo: descLabel.topAnchor)
        var trailAnch = enabledSwitch.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        constraints[phase]!.append([trailAnch, topAnch])

        let showPickerButton = UIButton()
        showPickerButton.translatesAutoresizingMaskIntoConstraints = false
        showPickerButton.setTitle("Zeit auswählen", for: .normal)
        showPickerButton.backgroundColor = .darkGray
        showPickerButton.layer.cornerRadius = 5
        showPickerButton.addTarget(self, action: #selector(showTimePicker), for: .touchUpInside)

        topAnch = showPickerButton.topAnchor.constraint(equalTo: enabledSwitch.bottomAnchor, constant: 4)
        trailAnch = showPickerButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        constraints[phase]!.append([topAnch, trailAnch])

        let timePicker = UIDatePicker()
        timePicker.backgroundColor = .white
        timePicker.isHidden = true
        timePicker.translatesAutoresizingMaskIntoConstraints = false
        timePicker.datePickerMode = .time
        timePicker.addTarget(self, action: #selector(changeTimer), for: .valueChanged)

        topAnch = timePicker.bottomAnchor.constraint(equalTo: enabledSwitch.bottomAnchor)
        trailAnch = timePicker.trailingAnchor.constraint(equalTo: enabledSwitch.trailingAnchor)
        constraints[phase]!.append([topAnch, trailAnch])

        //Brightness Slider Value Label

        let sliderValLabel = UILabel()
        sliderValLabel.translatesAutoresizingMaskIntoConstraints = false
        sliderValLabel.text = "0"
        sliderValLabel.backgroundColor = .white

        trailAnch = sliderValLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        topAnch = sliderValLabel.topAnchor.constraint(equalTo: showPickerButton.bottomAnchor, constant: 10)
        var widthAnch = sliderValLabel.widthAnchor.constraint(equalToConstant: view.frame.width / 6)
        constraints[phase]!.append([trailAnch, topAnch, widthAnch])

        //Brightness Slider

        let valueSlider = UISlider()
        valueSlider.isContinuous = false
        valueSlider.translatesAutoresizingMaskIntoConstraints = false

        topAnch = valueSlider.topAnchor.constraint(equalTo: sliderValLabel.bottomAnchor, constant: 10)
        leadAnch = valueSlider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
        trailAnch = valueSlider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        constraints[phase]!.append([topAnch, leadAnch, trailAnch])

        let timePickerWrapper = UIView()
        //timePickerWrapper.translatesAutoresizingMaskIntoConstraints = false

        timePickerWrapper.addSubview(descLabel)
        timePickerWrapper.addSubview(enabledSwitch)
        timePickerWrapper.addSubview(showPickerButton)
        timePickerWrapper.addSubview(timePicker)
        timePickerWrapper.addSubview(valueSlider)
        timePickerWrapper.addSubview(sliderValLabel)

        wrappers[phase] = timePickerWrapper
        elements[phase] = [descLabel, showPickerButton, enabledSwitch, timePicker, sliderValLabel, valueSlider]

        let commandID = bleService.getCommand(commandName: phase)
        bleService.send(aText: "c\(commandID)r:#")
    }

    for buttonName in buttons {
        constraints[buttonName] = [[]]
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false

        let widthAnch = button.widthAnchor.constraint(equalToConstant: contentView.frame.width/1.1)
        let xAnch = button.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        constraints[buttonName]!.append([widthAnch, xAnch])

        let buttonWrapper = UIView()
        buttonWrapper.translatesAutoresizingMaskIntoConstraints = false

        wrappers[buttonName] = buttonWrapper
        elements[buttonName] = [button]
    }
}        

func changeContent(criteria: [String]) {
        for item in criteria {
            if(!contentView.contains(wrappers[item]!)) {
                contentView.addArrangedSubview(wrappers[item]!)
                for singleView in constraints[item]! {
                    for singleViewConstraint in singleView {
                        singleViewConstraint.isActive = true
                    }
                }
            }
        }
    }

func removeContent() {
    var criteria = [String]()
    switch(previousSetupMode) {
    case "d":
        criteria = d_kind_criteria
        break
    case "l":
        criteria = l_kind_criteria
        break
    case "m":
        criteria = m_kind_criteria
        break
    case "t":
        criteria = t_kind_criteria
        break
    default:
        break
    }
    for item in criteria {
        wrappers[item]!.removeFromSuperview()
    }
}

func changeView() {
    if(previousSetupMode != activeSetupMode) {
        removeContent()
    }
    switch(activeSetupMode) {
    case "d":
        changeContent(criteria: d_kind_criteria)
        break
    case "l":
        changeContent(criteria: l_kind_criteria)
        break
    case "t":
        changeContent(criteria: t_kind_criteria)
        break
    case "m":
        changeContent(criteria: m_kind_criteria)
        break
    default:
        break
    }
}
J1297
  • 61
  • 4

1 Answers1

3

The problem is that you are not giving your "wrapper" views any height, so the controls are being placed outside the bounds of their parent views.

You can confirm this two ways...

1) In your

for (index, sliderName) in sliders.enumerated() {

block, add:

sliderWrapper.backgroundColor = .green

(after creating the sliderWrapper view, of course). When you run the app, you won't see the green background, because sliderWrapper has a height of Zero:

enter image description here

2) And / or add:

sliderWrapper.clipsToBounds = true

and you won't see the controls at all:

enter image description here

To solve this, you can add constraints:

        let sliderWrapper = UIView()
        sliderWrapper.translatesAutoresizingMaskIntoConstraints = false
        sliderWrapper.backgroundColor = .green

        sliderWrapper.addSubview(descLabel)
        sliderWrapper.addSubview(valueLabel)
        sliderWrapper.addSubview(slider)

        // add a topAnchor constraint from the top of descLabel to the top of sliderWrapper
        // center valueLabel vertically to descLabel
        // and a bottomAnchor from the bottom of slider to the bottom of sliderWrapper (negative if you want "padding")
        NSLayoutConstraint.activate([
            descLabel.topAnchor.constraint(equalTo: sliderWrapper.topAnchor, constant: 8.0),
            valueLabel.centerYAnchor.constraint(equalTo: descLabel.centerYAnchor),
            slider.bottomAnchor.constraint(equalTo: sliderWrapper.bottomAnchor, constant: -8.0),
        ])

Now, background is visible... controls are visible... and controls can be interacted with:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Thank you so much, you helped me a lot, I thought I didn't need to constraint them inside of the container, just relative to each other so the compiler knows where to place the items. Unfortunately as you told me, this wasn't the case :D. You saved days of try-and-error because this was the last thing I would've searched for. Thank you so so much! I wish you all the best. – J1297 Oct 09 '19 at 05:37