基于条件切换UIStackView视图位置时的异常问题求助
动态切换UIStackView中子视图位置异常问题解决
问题描述
在iOS项目中,使用包含多个子视图的UIStackView,希望点击切换按钮时动态交换名为FIRST和SECOND的两个文本框的上下位置,但出现每两次点击才会正确切换一次的异常。
问题演示:
原代码
import UIKit import Combine import SnapKit class ViewController: UIViewController { private var toggleSubject = PassthroughSubject<Bool, Never>() private var toggle = false private var store = Set<AnyCancellable>() private lazy var topField: AppTextField = { let view = AppTextField(title: "First") view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var bottomField: AppTextField = { let view = AppTextField(title: "Second") view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var lineView: UIImageView = { let view = UIImageView(frame: .zero) view.backgroundColor = .red view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var stackView: UIStackView = { let view = UIStackView(arrangedSubviews: [topField, lineView, bottomField, toggleButton]) view.axis = .vertical view.spacing = 4.0 view.distribution = .fill view.alignment = .fill view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var toggleButton: UIButton = { let view = UIButton(frame: .zero) view.setTitle("Toggle", for: .normal) view.setTitleColor(.white, for: .normal) view.setTitleColor(.systemYellow, for: .highlighted) view.backgroundColor = .darkGray view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var selectedTopFieldLabel: UILabel = { let view = UILabel(frame: .zero) view.textAlignment = .center view.translatesAutoresizingMaskIntoConstraints = false return view }() override func viewDidLoad() { super.viewDidLoad() self.view.addSubview(stackView) self.view.addSubview(selectedTopFieldLabel) toggleSubject.send(toggle) stackView.snp.makeConstraints { make in make.leading.trailing.centerY.equalToSuperview().inset(16) } lineView.snp.makeConstraints { make in make.height.equalTo(0.5) } selectedTopFieldLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(200) } toggleButton.addTarget(self, action: #selector(toggleButtonAction), for: .touchUpInside) toggleSubject.sink { toggle in print("---> Changed Toggle",toggle) self.updateUI(toggle: toggle) UIView.animate(withDuration: 0.25) { self.stackView.layoutIfNeeded() } }.store(in: &store) self.updateUI(toggle: toggle) } @objc func toggleButtonAction() { toggle.toggle() toggleSubject.send(toggle) } private func updateUI(toggle: Bool) { let arrangedSubviews = self.stackView.arrangedSubviews guard var first = arrangedSubviews[0] as? AppTextField, var second = arrangedSubviews[2] as? AppTextField else {return} stackView.arrangedSubviews.forEach({$0.removeFromSuperview()}) let changedViews = toggle ? [first, lineView, second, toggleButton] : [second, lineView, first , toggleButton] guard let firstTextField = changedViews.first as? AppTextField, let firstfieldPlaceholder = firstTextField.placeholder else {return} self.selectedTopFieldLabel.text = "Selected On Top " + " -> " + firstfieldPlaceholder changedViews.forEach { view in self.stackView.addArrangedSubview(view) } // Update constraints UIView.animate(withDuration: 0.25) { self.stackView.setNeedsLayout() self.stackView.layoutIfNeeded() } } } class AppTextField: UIView { private lazy var inputField: UITextField = { let textField = UITextField(frame: .zero) textField.backgroundColor = .lightGray.withAlphaComponent(0.15) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() var title: String? { get { return inputField.placeholder } set { inputField.placeholder = newValue } } var placeholder: String? { get { return inputField.placeholder } set { inputField.placeholder = newValue } } var text: String? { get { return inputField.text } set { inputField.text = newValue } } init(title: String) { super.init(frame: .zero) self.title = title setupViews() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { addSubview(inputField) inputField.snp.makeConstraints { make in make.edges.equalToSuperview() make.height.equalTo(48) } } }
问题原因
- 依赖索引获取视图错误:
updateUI方法中硬取arrangedSubviews[0]和arrangedSubviews[2]作为两个文本框,但第一次切换后,stackView的子视图顺序已改变,第二次取到的是交换后的视图,导致逻辑反转。 - 重复操作与动画冲突:
viewDidLoad和sink回调中都调用了updateUI,且两处都做了动画,导致布局更新混乱。 - 不必要的视图移除重加:每次切换都移除所有子视图再重新添加,不仅冗余,还可能引发布局状态异常。
解决方案
修改核心逻辑
- 直接使用类中定义的
topField和bottomField属性(固定引用,不受顺序影响)来构建新的子视图数组。 - 简化布局更新方式,直接重新设置
stackView.arrangedSubviews,无需移除重加。 - 统一动画调用,避免重复。
修改后的代码
import UIKit import Combine import SnapKit class ViewController: UIViewController { private var toggleSubject = PassthroughSubject<Bool, Never>() private var toggle = false private var store = Set<AnyCancellable>() private lazy var topField: AppTextField = { let view = AppTextField(title: "First") view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var bottomField: AppTextField = { let view = AppTextField(title: "Second") view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var lineView: UIImageView = { let view = UIImageView(frame: .zero) view.backgroundColor = .red view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var stackView: UIStackView = { let view = UIStackView(arrangedSubviews: [topField, lineView, bottomField, toggleButton]) view.axis = .vertical view.spacing = 4.0 view.distribution = .fill view.alignment = .fill view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var toggleButton: UIButton = { let view = UIButton(frame: .zero) view.setTitle("Toggle", for: .normal) view.setTitleColor(.white, for: .normal) view.setTitleColor(.systemYellow, for: .highlighted) view.backgroundColor = .darkGray view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var selectedTopFieldLabel: UILabel = { let view = UILabel(frame: .zero) view.textAlignment = .center view.translatesAutoresizingMaskIntoConstraints = false return view }() override func viewDidLoad() { super.viewDidLoad() self.view.addSubview(stackView) self.view.addSubview(selectedTopFieldLabel) stackView.snp.makeConstraints { make in make.leading.trailing.centerY.equalToSuperview().inset(16) } lineView.snp.makeConstraints { make in make.height.equalTo(0.5) } selectedTopFieldLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(200) } toggleButton.addTarget(self, action: #selector(toggleButtonAction), for: .touchUpInside) toggleSubject.sink { [weak self] toggle in guard let self = self else { return } print("---> Changed Toggle",toggle) self.updateUI(toggle: toggle) // 统一在这里做动画 UIView.animate(withDuration: 0.25) { self.stackView.layoutIfNeeded() } }.store(in: &store) // 初始化UI self.updateUI(toggle: toggle) } @objc func toggleButtonAction() { toggle.toggle() toggleSubject.send(toggle) } private func updateUI(toggle: Bool) { // 直接使用固定的topField和bottomField,不受顺序影响 let newArrangedSubviews: [UIView] if toggle { newArrangedSubviews = [bottomField, lineView, topField, toggleButton] selectedTopFieldLabel.text = "Selected On Top -> Second" } else { newArrangedSubviews = [topField, lineView, bottomField, toggleButton] selectedTopFieldLabel.text = "Selected On Top -> First" } // 直接重新设置arrangedSubviews,无需移除重加 stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } newArrangedSubviews.forEach { stackView.addArrangedSubview($0) } stackView.setNeedsLayout() } } class AppTextField: UIView { private lazy var inputField: UITextField = { let textField = UITextField(frame: .zero) textField.backgroundColor = .lightGray.withAlphaComponent(0.15) textField.translatesAutoresizingMaskIntoConstraints = false return textField }() var title: String? { get { return inputField.placeholder } set { inputField.placeholder = newValue } } var placeholder: String? { get { return inputField.placeholder } set { inputField.placeholder = newValue } } var text: String? { get { return inputField.text } set { inputField.text = newValue } } init(title: String) { super.init(frame: .zero) self.title = title setupViews() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { addSubview(inputField) inputField.snp.makeConstraints { make in make.edges.equalToSuperview() make.height.equalTo(48) } } }
关键优化点
- 不再依赖
arrangedSubviews的索引获取文本框,改用类属性的固定引用,确保逻辑正确性。 - 移除重复的动画调用,统一在
sink回调中执行布局动画。 - 简化视图顺序更新逻辑,直接构建新的子视图数组并重新设置。
内容的提问来源于stack exchange,提问作者DRN
相关产品推荐
相关产品推荐

