You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

关键注意点

  1. 避免重复添加Target:cell复用会导致同一个开关被多次添加target,必须在cellForRow或prepareForReuse中移除旧的target。
  2. 弱引用避免循环引用:使用闭包时一定要用[weak self],防止内存泄漏。
  3. 最小化刷新范围:尽量只刷新状态变化的行,而不是整个表格,提升性能。

内容的提问来源于stack exchange,提问作者LgSus97

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 06:51:34