解决UITableViewCell重用导致Block回调参数不正确的问题

一、问题

        需求是:在一个列表中有下载按钮、工具按钮、开关,点击下载按钮后,左边位置换成进度圆环,并不断更新进度,此时用户可以点击工具按钮,进行操作。

        问题:在UITableView不断刷新时,删除的行可能错位,例如明明选择第四行删除,reload后发现删除的是第7行。

        当前写法如下

VC:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "SomeCell") as! SomeCell
    let model = modelArray[indexPath.row]
    cell.setModel(model)
    cell.btnDownloadClickBlock = { [weak self] in
        //Do something
    }
    cell.btnToolClickBlock = { [weak self] type in
        //Do something
    }
    cell.switchChangedBlock = { [weak self] isOn in
        //Do something
    }
    return cell
}

Cell:

class SomeCell: UITableViewCell {
    
    //MARK: - UI声明

    @IBOutlet weak var lblName: UILabel!
    @IBOutlet weak var btnDownload: UIButton!
    @IBOutlet weak var btnTool: UIButton!
    @IBOutlet weak var switchVisible: UISwitch!

    //MARK: - UI事件

    @IBAction func btnDownload_Click(_ sender: Any) {
        btnDownloadClickBlock?()
    }

    @IBAction func btnTool_Click(_ sender: Any) {
        let items: [ToolType] = [.delete]
        let selectView = PopoverSelectView(style: .iconAndText, items: items) { [weak self] result in
            self?.btnToolClickBlock?(result)
            PopoverManager.shared.hide()
        }
        PopoverManager.shared.show(selectView, fromView: btnTool)
    }
    
    @IBAction func visibleValue_Changed(_ sender: Any) {
        switchChangedBlock?(switchVisible.isOn)
    }

    //MARK: - 公开变量

    /// 用户点击下载的回调
    var btnDownloadClickBlock: VoidClosure? = nil

    /// 用户选择工具的回调
    var btnToolClickBlock: ((ToolType) -> Void)? = nil
    
    /// 用户切换开关的回调
    var switchChangedBlock: BoolClosure? = nil
    
    //MARK: - 公开方法

    /// 设置数据源
    func setModel(_ model: SomeModel) {
        lblName.text = model.name
        switchVisible.isOn = model.visible
        btnDownload.isHidden = !model.canDownload
    }
    
}

二、探究

2.1 第一次尝试

        由于测试人员反馈的是删除功能错位,所以在删除的回调里把id和行号打出来,果然有时不正确,但是id和行号是匹配的。

    cell.btnToolClickBlock = { [weak self] type in
        print(model.id)
        print(indexPath.row)
        //Do something
    }

        那么把model传入cell,cell回调时将model一起返回来就ok了。这也是网上很多人给出的解决方案。

        1.增加私有变量

    private var model: SomeModel? = nil

         2.设置数据源时给私有变量赋值

    /// 设置数据源
    func setModel(_ model: SomeModel) {
        self.model = model
        lblName.text = model.name
        switchVisible.isOn = model.visible
        btnDownload.isHidden = !model.canDownload
    }

        3.修改回调参数

    var btnToolClickBlock: ((ToolType, SomeModel?) -> Void)? = nil

        4.返回model

    @IBAction func btnTool_Click(_ sender: Any) {
        let items: [ToolType] = [.delete]
        let selectView = PopoverSelectView(style: .iconAndText, items: items) { [weak self] result in
            self?.btnToolClickBlock?(result, self?.model)
            PopoverManager.shared.hide()
        }
        PopoverManager.shared.show(selectView, fromView: btnTool)
    }

        5.在VC中不用捕获的model,使用返回的model

    cell.btnToolClickBlock = { [weak self] (type, returnModel) in
        guard let returnModel = returnModel else {
            return
        }
        print(model.id)
        print(returnModel.id)
        guard model.id == returnModel.id else {
            return
        }
        //Do something
    }

        此时如果错位就return了,不会误删除,可是原来的也没有被正确的删除。

        分析:网上采用此解决方案的主要是图片的异步加载,当请求完成后给UIImageView设置图片时,该行已经被重用了,导致图片错位,此时return可以解决该问题。当然,SDWebImage的sd_setImageWithURL没有这个问题,重新设置后sd会取消之前的请求。

2.2 第二次尝试

        忽然想到,为什么下载按钮和开关没有错位,只有工具按钮弹窗后返回的删除事件错位了呢。于是我在这里打出了id。第一次是正确的,第二次是错误的!!!

    @IBAction func btnTool_Click(_ sender: Any) {
        guard let oldModel = model else {
            return 
        }
        //这里的id是正确的行
        print(oldModel.id)
        let items: [ToolType] = [.delete]
        let selectView = PopoverSelectView(style: .iconAndText, items: items) { [weak self] result in
            guard let newModel = self?.model else {
                return 
            }
            //这里的id是错误的行
            print(newModel.id)
            self?.btnToolClickBlock?(result, self?.model)
            PopoverManager.shared.hide()
        }
        PopoverManager.shared.show(selectView, fromView: btnTool)
    }

        试着将弹出选择框的代码放在VC中,一切都正确了。

三、最终代码

VC:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "SomeCell") as! SomeCell
    let model = modelArray[indexPath.row]
    cell.setModel(model)
    cell.btnDownloadClickBlock = { [weak self] in
        //Do something
    }
    cell.btnToolClickBlock = { [weak self] btnTool in
        let items: [ToolType] = [.delete]
        let selectView = PopoverSelectView(style: .iconAndText, items: items) { [weak self] result in
            //这里直接用捕获的model,是正确的model,不需要cell再传model出来
            self?.selectTool(model: model, type: result)
            PopoverManager.shared.hide()
        }
        PopoverManager.shared.show(selectView, fromView: btnTool)
    }
    cell.switchChangedBlock = { [weak self] isOn in
        //Do something
    }
    return cell
}

Cell:

class SomeCell: UITableViewCell {
    
    //MARK: - UI声明

    @IBOutlet weak var lblName: UILabel!
    @IBOutlet weak var btnDownload: UIButton!
    @IBOutlet weak var btnTool: UIButton!
    @IBOutlet weak var switchVisible: UISwitch!

    //MARK: - UI事件

    @IBAction func btnDownload_Click(_ sender: Any) {
        btnDownloadClickBlock?()
    }

    @IBAction func btnTool_Click(_ sender: Any) {
        btnToolClickBlock?(btnTool)
    }
    
    @IBAction func visibleValue_Changed(_ sender: Any) {
        switchChangedBlock?(switchVisible.isOn)
    }

    //MARK: - 公开变量

    /// 用户点击下载的回调
    var btnDownloadClickBlock: VoidClosure? = nil

    /// 用户选择工具的回调
    var btnToolClickBlock: ((UIButton) -> Void)? = nil
    
    /// 用户切换开关的回调
    var switchChangedBlock: BoolClosure? = nil
    
    //MARK: - 公开方法

    /// 设置数据源
    func setModel(_ model: SomeModel) {
        lblName.text = model.name
        switchVisible.isOn = model.visible
        btnDownload.isHidden = !model.canDownload
    }
    
}

四、分析

        为了了解该问题的原因,做了以下两个测试。

4.1 异步延迟

        该现象应该是由于更新下载进度时不断刷新TableView(因为可以同时下载很多数据,不会只刷新一行),这时许多Cell被重用了,导致改变了block和cell一一对应的关系。

        只要是异步延迟都可能会出现该情况,例如把indexPath传入cell,在cell中仅做一个延迟。

    @IBAction func btnTool_Click(_ sender: Any) {
        //这里打出的行号是正确的
        print(indexPath.row)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: { [weak self] in
            //这里打出的行号是错误的
            print(indexPath.row)
        })
    }

          如果在VC中进行异步延迟操作,是没有问题的。  

    cell.btnToolClickBlock = { [weak self] btnTool in
        //这里打出的行号是正确的
        print(indexPath.row)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: { [weak self] in
            //这里打出的行号也是正确的
            print(indexPath.row)
        })
    }

4.2 delegate与block

        既然是block的问题,那么delegate有没有问题呢?

Delegate:

protocol SomeCellDelegate {
    func btnToolClick(indexPath: IndexPath?)
}

Cell:

    @IBAction func btnTool_Click(_ sender: Any) {
        //打出来的行号是正确的
        print(indexPath.row)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: { [weak self] in
            self?.delegate?.btnToolClick?(indexPath: self?.indexPath)
        })
    }

VC:

    func btnToolClick(indexPath: IndexPath?) {
        guard let indexPath = indexPath else {
            return
        }
        //打出来的行号是错误的
        print(indexPath.row)
    }

        证明delegate和block一样,都会出现匹配不上的问题。

4.3 总结

        综上,无论是delegate还是block,在UITableViewCell刷新时,若在cell内部做了异步的操作,都可能导致VC收到的数据和用户操作的数据不匹配。

        应该把异步操作回调到上层处理(ViewController),不要把业务代码放到Cell(View)里。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值