UITableView中UISwitch单选状态校验的优雅实现方案探讨
实现UITableView单选UISwitch的优雅方案
这是个很常见的单选开关需求,我来分享几个可靠且优雅的实现思路,帮你解决这个问题~
核心思路:用数据源驱动视图,避免依赖Tag
直接依赖UISwitch的tag来管理状态容易出错(比如cell复用导致tag混乱),更优雅的方式是用数据源变量/模型来维护选中状态,让视图根据数据源自动渲染。
方案1:用变量维护选中索引(轻量版)
适合不需要持久化选中状态的场景,我们在QuestionListTableViewCell里加一个变量记录当前选中的开关位置,再通过刷新对应行来同步状态。
步骤1:添加状态变量
在QuestionListTableViewCell中新增:
// 记录当前选中的开关索引,nil表示无选中 private var selectedSwitchIndex: Int? // 假设你的answers数组是这个类型,替换成你实际的类型 private var answers: [String] = ["SI", "NO"]
步骤2:修改cellForRow方法
根据选中索引设置开关状态,同时修复cell复用导致的重复添加target问题:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "answerListCell", for: indexPath) as! AnswerListTableViewCell cell.separatorInset.right = cell.separatorInset.left // 根据数据源设置开关状态 cell.answerList.optionSwitch.isOn = (selectedSwitchIndex == indexPath.row) // 先移除旧的target,避免复用cell时多次触发回调 cell.answerList.optionSwitch.removeTarget(nil, action: nil, for: .valueChanged) // 添加新的target并设置tag cell.answerList.optionSwitch.tag = indexPath.row cell.answerList.optionSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged) // 设置选项文本 cell.answerList.optionLabel.text = answers[indexPath.row] return cell }
步骤3:处理开关变化事件
更新选中状态并只刷新必要的行:
@objc func switchChanged(_ sender: UISwitch) { let currentIndex = sender.tag let isOn = sender.isOn if isOn { // 记录之前选中的索引,用于刷新旧行 let previousIndex = selectedSwitchIndex // 更新当前选中索引 selectedSwitchIndex = currentIndex // 刷新之前选中的行(如果存在) if let prevIdx = previousIndex { tableView.reloadRows(at: [IndexPath(row: prevIdx, section: 0)], with: .automatic) } } else { // 如果关闭当前开关,清空选中状态 selectedSwitchIndex = nil } }
方案2:用模型维护状态(推荐,符合MVC)
如果需要持久化选中状态,或者你的选项本身是模型对象,推荐直接在模型里添加选中状态属性,让视图完全由模型驱动。
步骤1:定义Answer模型
struct Answer { let optionText: String var isSelected: Bool // 记录该选项是否被选中 }
步骤2:更新数据源与cell渲染
// 在QuestionListTableViewCell中替换answers数组 private var answers: [Answer] = [ Answer(optionText: "SI", isSelected: false), Answer(optionText: "NO", isSelected: false) ] // 修改cellForRow方法 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "answerListCell", for: indexPath) as! AnswerListTableViewCell cell.separatorInset.right = cell.separatorInset.left let answer = answers[indexPath.row] cell.answerList.optionLabel.text = answer.optionText cell.answerList.optionSwitch.isOn = answer.isSelected // 同样处理target复用问题 cell.answerList.optionSwitch.removeTarget(nil, action: nil, for: .valueChanged) cell.answerList.optionSwitch.tag = indexPath.row cell.answerList.optionSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged) return cell }
步骤3:处理开关变化事件
@objc func switchChanged(_ sender: UISwitch) { let currentIndex = sender.tag let isOn = sender.isOn if isOn { // 先将所有选项设为未选中 answers = answers.map { Answer(optionText: $0.optionText, isSelected: false) } // 再将当前选项设为选中 answers[currentIndex].isSelected = true // 刷新所有行?或者只刷新变化的行 tableView.reloadData() // 更高效的方式:找到之前选中的索引,只刷新两行 // if let prevIdx = answers.firstIndex(where: { $0.isSelected }) { // tableView.reloadRows(at: [IndexPath(row: prevIdx, section: 0), IndexPath(row: currentIndex, section: 0)], with: .automatic) // } } else { answers[currentIndex].isSelected = false tableView.reloadRows(at: [IndexPath(row: currentIndex, section: 0)], with: .automatic) } }
进阶优化:用闭包解耦Cell与上层逻辑
上面的方案还是依赖tag,更优雅的方式是给AnswerListTableViewCell添加闭包回调,彻底解耦视图和逻辑:
步骤1:修改AnswerListTableViewCell
class AnswerListTableViewCell: UITableViewCell { @IBOutlet weak var answerList: AnswerList! // 闭包回调,传递开关状态变化 var onSwitchToggled: ((Bool) -> Void)? override func prepareForReuse() { super.prepareForReuse() // 复用cell时重置闭包,避免回调到旧的indexPath onSwitchToggled = nil } } // 修改AnswerList的开关事件 class AnswerList: UIView { @IBOutlet weak var optionSwitch: UISwitch! @IBOutlet weak var optionLabel: UILabel! required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } func commonInit(){ let viewFromXib = Bundle.main.loadNibNamed("AnswerList", owner: self, options: nil)![0] as! UIView viewFromXib.frame = self.bounds addSubview(viewFromXib) } @IBAction func switchChangedState(_ sender: UISwitch) { // 将事件传递给cell的闭包 (superview as? AnswerListTableViewCell)?.onSwitchToggled?(sender.isOn) } }
步骤2:在cellForRow中使用闭包
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "answerListCell", for: indexPath) as! AnswerListTableViewCell cell.separatorInset.right = cell.separatorInset.left let answer = answers[indexPath.row] cell.answerList.optionLabel.text = answer.optionText cell.answerList.optionSwitch.isOn = answer.isSelected // 设置闭包回调,无需依赖tag cell.onSwitchToggled = { [weak self] isOn in guard let self = self else { return } self.handleSwitchToggle(at: indexPath.row, isOn: isOn) } return cell } // 单独处理开关逻辑的方法 private func handleSwitchToggle(at index: Int, isOn: Bool) { if isOn { answers = answers.map { Answer(optionText: $0.optionText, isSelected: false) } answers[index].isSelected = true tableView.reloadData() } else { answers[index].isSelected = false tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) } }
关键注意点
- 避免重复添加Target:cell复用会导致同一个开关被多次添加target,必须在
cellForRow或prepareForReuse中移除旧的target。 - 弱引用避免循环引用:使用闭包时一定要用
[weak self],防止内存泄漏。 - 最小化刷新范围:尽量只刷新状态变化的行,而不是整个表格,提升性能。
内容的提问来源于stack exchange,提问作者LgSus97
相关产品推荐
相关产品推荐

