如何在UIKit中实现带双状态的可扩展水平UIStackView按钮
核心思路
通过手动控制系统UIButton的标题显示/隐藏,结合自动布局实现按钮选中展开(显示图片+标题)、未选中收缩(仅显示图片)的效果,同时维护单一选中状态,全程无需自定义按钮子类。
分步实现
1. 创建按钮与水平StackView
先初始化所有按钮,配置基础样式(图片、默认隐藏标题),并添加到水平StackView中:
import UIKit class ViewController: UIViewController { private var buttons = [UIButton]() private var selectedButton: UIButton? override func viewDidLoad() { super.viewDidLoad() setupStackView() // 默认选中第一个按钮 buttons.first.map(selectButton) } private func setupStackView() { let stackView = UIStackView() stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = 8 stackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stackView) // 固定StackView居中 NSLayoutConstraint.activate([ stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) // 按钮配置数组(示例用SF Symbols,可替换为自定义图片) let buttonConfigs = [ (image: UIImage(systemName: "house.fill")!, title: "首页"), (image: UIImage(systemName: "person.fill")!, title: "我的"), (image: UIImage(systemName: "cart.fill")!, title: "购物车"), (image: UIImage(systemName: "heart.fill")!, title: "收藏"), (image: UIImage(systemName: "gear")!, title: "设置") ] for config in buttonConfigs { let button = UIButton(type: .custom) button.setImage(config.image, for: .normal) button.setTitle(config.title, for: .selected) // 把标题存到selected状态,方便后续取出 button.setTitleColor(.label, for: .normal) button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) // 图片和标题间距 button.contentHorizontalAlignment = .leading // 图片左对齐,标题紧跟 // 未选中时隐藏标题 button.setTitle(nil, for: .normal) // 设置未选中时的方形约束 button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true button.heightAnchor.constraint(equalTo: button.widthAnchor).isActive = true button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) buttons.append(button) stackView.addArrangedSubview(button) } } }
2. 实现选中状态切换逻辑
编写点击事件处理方法,控制按钮的显示状态,确保同一时间只有一个按钮处于展开状态:
extension ViewController { @objc private func buttonTapped(_ sender: UIButton) { guard sender != selectedButton else { return } // 点击已选中按钮不做处理 // 取消上一个选中按钮的状态 selectedButton.map(deselectButton) // 选中当前按钮 selectButton(sender) } private func selectButton(_ button: UIButton) { // 取出存好的标题并显示 button.setTitle(button.title(for: .selected), for: .normal) selectedButton = button // 添加平滑动画 UIView.animate(withDuration: 0.3) { button.layoutIfNeeded() self.view.layoutIfNeeded() } } private func deselectButton(_ button: UIButton) { // 隐藏标题,恢复方形 button.setTitle(nil, for: .normal) UIView.animate(withDuration: 0.3) { button.layoutIfNeeded() self.view.layoutIfNeeded() } } }
3. 细节优化
- 样式自定义:可以在
selectButton和deselectButton中添加背景色、圆角等样式调整,比如:// 选中时设置蓝色背景,未选中时灰色 private func selectButton(_ button: UIButton) { // ... 原有代码 button.backgroundColor = .systemBlue button.layer.cornerRadius = 8 } private func deselectButton(_ button: UIButton) { // ... 原有代码 button.backgroundColor = .systemGray5 } - 图片颜色适配:如果需要选中时图片变色,可以给按钮的
.normal和.selected状态设置不同图片,或者修改tintColor:button.tintColor = .systemGray // 选中时修改tintColor private func selectButton(_ button: UIButton) { // ... 原有代码 button.tintColor = .white }
关于changesSelectionAsPrimaryAction的说明
该属性会自动给按钮添加系统默认的选中样式(背景色、勾选图标),确实不符合需求。我们的方案完全规避了这个属性,通过手动控制标题显示来实现尺寸变化,不需要自定义UIButton子类就能达到设计要求。
内容的提问来源于stack exchange,提问作者WebStormer
相关产品推荐
相关产品推荐

