UIScrollView 与其他 UIElement 重叠,因此它们不再可点击

UIScrollView overlaps other UIElements so they are not clickable anymore

亲爱的 Whosebug 社区,

我目前正在做一个项目,我需要在其中制作一个完全动态的用户界面。 为此,我以编程方式构建所有内容,以使其尽可能简单。 我现在遇到了一个问题,当我用 UIScrollView 包装我的 contentView(UIStackView) 以使其可滚动时,scrollView 位于所有其他 UIElement 的前面,因此我只能滚动。我无法与按钮、滑块、开关或任何东西交互,不会触发任何事件。

我确实做了我能想到的任何事情(为这个问题工作了 DAYS),但无论是在 google 上还是在堆栈溢出或苹果论坛上都找不到任何合适的答案。

我很确定这是一个我无法想到的很小的变化。非常感谢您的帮助。

结构是这样的: ViewController > UIScrollView > UIStackView > Item Wrappers (for example contains a UISwitch and a Describing Label) > Single UIElement

在用户交互(例如选择不同的模式)时,包装器被删除 and/or 根据用户需要添加到视图中。 我只做了 post 与这个问题相关的代码(在我看来)。如果您需要任何进一步的信息,请随时询问。

我可能需要补充: 一旦我删除 UIScrollView 并将 StackView(在代码中命名为 contentView)添加到主视图,一切正常,我无法滚动它,一旦我有超过 5 个元素,这就是一个大问题附加到视图的包装器。

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

问题是您没有给 "wrapper" 视图任何高度,因此控件被放置在其父视图的边界之外。

您可以通过两种方式确认...

1) 在你的

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

阻止,添加:

sliderWrapper.backgroundColor = .green

(当然是在创建 sliderWrapper 视图之后)。当您 运行 应用程序时,您不会看到绿色背景,因为 sliderWrapper 的高度为零:

2) 和/或添加:

sliderWrapper.clipsToBounds = true

而且您根本看不到控件:

要解决这个问题,您可以添加约束:

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

现在,背景是可见的...控件是可见的...并且可以与控件进行交互: